2 Command-line interface to the Nominatim functions for import, update,
3 database administration and querying.
9 from pathlib import Path
11 from .config import Configuration
12 from .tools.exec_utils import run_legacy_script, run_api_script
14 from .indexer.indexer import Indexer
16 def _num_system_cpus():
18 cpus = len(os.sched_getaffinity(0))
19 except NotImplementedError:
22 return cpus or os.cpu_count()
25 class CommandlineParser:
26 """ Wraps some of the common functions for parsing the command line
27 and setting up subcommands.
29 def __init__(self, prog, description):
30 self.parser = argparse.ArgumentParser(
32 description=description,
33 formatter_class=argparse.RawDescriptionHelpFormatter)
35 self.subs = self.parser.add_subparsers(title='available commands',
38 # Arguments added to every sub-command
39 self.default_args = argparse.ArgumentParser(add_help=False)
40 group = self.default_args.add_argument_group('Default arguments')
41 group.add_argument('-h', '--help', action='help',
42 help='Show this help message and exit')
43 group.add_argument('-q', '--quiet', action='store_const', const=0,
44 dest='verbose', default=1,
45 help='Print only error messages')
46 group.add_argument('-v', '--verbose', action='count', default=1,
47 help='Increase verboseness of output')
48 group.add_argument('--project-dir', metavar='DIR', default='.',
49 help='Base directory of the Nominatim installation (default:.)')
50 group.add_argument('-j', '--threads', metavar='NUM', type=int,
51 help='Number of parallel threads to use')
54 def add_subcommand(self, name, cmd):
55 """ Add a subcommand to the parser. The subcommand must be a class
56 with a function add_args() that adds the parameters for the
57 subcommand and a run() function that executes the command.
59 parser = self.subs.add_parser(name, parents=[self.default_args],
60 help=cmd.__doc__.split('\n', 1)[0],
61 description=cmd.__doc__,
62 formatter_class=argparse.RawDescriptionHelpFormatter,
64 parser.set_defaults(command=cmd)
67 def run(self, **kwargs):
68 """ Parse the command line arguments of the program and execute the
69 appropriate subcommand.
71 args = self.parser.parse_args(args=kwargs.get('cli_args'))
73 if args.subcommand is None:
74 self.parser.print_help()
77 for arg in ('module_dir', 'osm2pgsql_path', 'phplib_dir', 'data_dir', 'phpcgi_path'):
78 setattr(args, arg, Path(kwargs[arg]))
79 args.project_dir = Path(args.project_dir)
81 logging.basicConfig(stream=sys.stderr,
82 format='%(asctime)s: %(message)s',
83 datefmt='%Y-%m-%d %H:%M:%S',
84 level=max(4 - args.verbose, 1) * 10)
86 args.config = Configuration(args.project_dir, args.data_dir / 'settings')
88 return args.command.run(args)
90 ##### Subcommand classes
92 # Each class needs to implement two functions: add_args() adds the CLI parameters
93 # for the subfunction, run() executes the subcommand.
95 # The class documentation doubles as the help text for the command. The
96 # first line is also used in the summary when calling the program without
99 # No need to document the functions each time.
100 # pylint: disable=C0111
105 Create a new Nominatim database from an OSM file.
109 def add_args(parser):
110 group_name = parser.add_argument_group('Required arguments')
111 group = group_name.add_mutually_exclusive_group(required=True)
112 group.add_argument('--osm-file',
113 help='OSM file to be imported.')
114 group.add_argument('--continue', dest='continue_at',
115 choices=['load-data', 'indexing', 'db-postprocess'],
116 help='Continue an import that was interrupted')
117 group = parser.add_argument_group('Optional arguments')
118 group.add_argument('--osm2pgsql-cache', metavar='SIZE', type=int,
119 help='Size of cache to be used by osm2pgsql (in MB)')
120 group.add_argument('--reverse-only', action='store_true',
121 help='Do not create tables and indexes for searching')
122 group.add_argument('--enable-debug-statements', action='store_true',
123 help='Include debug warning statements in SQL code')
124 group.add_argument('--no-partitions', action='store_true',
125 help="""Do not partition search indices
126 (speeds up import of single country extracts)""")
127 group.add_argument('--no-updates', action='store_true',
128 help="""Do not keep tables that are only needed for
129 updating the database later""")
130 group = parser.add_argument_group('Expert options')
131 group.add_argument('--ignore-errors', action='store_true',
132 help='Continue import even when errors in SQL are present')
133 group.add_argument('--index-noanalyse', action='store_true',
134 help='Do not perform analyse operations during index')
139 params = ['setup.php']
141 params.extend(('--all', '--osm-file', args.osm_file))
143 if args.continue_at == 'load-data':
144 params.append('--load-data')
145 if args.continue_at in ('load-data', 'indexing'):
146 params.append('--index')
147 params.extend(('--create-search-indices', '--create-country-names',
149 if args.osm2pgsql_cache:
150 params.extend(('--osm2pgsql-cache', args.osm2pgsql_cache))
151 if args.reverse_only:
152 params.append('--reverse-only')
153 if args.enable_debug_statements:
154 params.append('--enable-debug-statements')
155 if args.no_partitions:
156 params.append('--no-partitions')
158 params.append('--drop')
159 if args.ignore_errors:
160 params.append('--ignore-errors')
161 if args.index_noanalyse:
162 params.append('--index-noanalyse')
164 return run_legacy_script(*params, nominatim_env=args)
169 Make database read-only.
171 About half of data in the Nominatim database is kept only to be able to
172 keep the data up-to-date with new changes made in OpenStreetMap. This
173 command drops all this data and only keeps the part needed for geocoding
176 This command has the same effect as the `--no-updates` option for imports.
180 def add_args(parser):
185 return run_legacy_script('setup.php', '--drop', nominatim_env=args)
188 class SetupSpecialPhrases:
190 Maintain special phrases.
194 def add_args(parser):
195 group = parser.add_argument_group('Input arguments')
196 group.add_argument('--from-wiki', action='store_true',
197 help='Pull special phrases from the OSM wiki.')
198 group = parser.add_argument_group('Output arguments')
199 group.add_argument('-o', '--output', default='-',
200 type=argparse.FileType('w', encoding='UTF-8'),
201 help="""File to write the preprocessed phrases to.
202 If omitted, it will be written to stdout.""")
206 if args.output.name != '<stdout>':
207 raise NotImplementedError('Only output to stdout is currently implemented.')
208 return run_legacy_script('specialphrases.php', '--wiki-import', nominatim_env=args)
211 class UpdateReplication:
213 Update the database using an online replication service.
217 def add_args(parser):
218 group = parser.add_argument_group('Arguments for initialisation')
219 group.add_argument('--init', action='store_true',
220 help='Initialise the update process')
221 group.add_argument('--no-update-functions', dest='update_functions',
222 action='store_false',
223 help="""Do not update the trigger function to
224 support differential updates.""")
225 group = parser.add_argument_group('Arguments for updates')
226 group.add_argument('--check-for-updates', action='store_true',
227 help='Check if new updates are available and exit')
228 group.add_argument('--once', action='store_true',
229 help="""Download and apply updates only once. When
230 not set, updates are continuously applied""")
231 group.add_argument('--no-index', action='store_false', dest='do_index',
232 help="""Do not index the new data. Only applicable
233 together with --once""")
237 params = ['update.php']
239 params.append('--init-updates')
240 if not args.update_functions:
241 params.append('--no-update-functions')
242 elif args.check_for_updates:
243 params.append('--check-for-updates')
246 params.append('--import-osmosis')
248 params.append('--import-osmosis-all')
249 if not args.do_index:
250 params.append('--no-index')
252 return run_legacy_script(*params, nominatim_env=args)
257 Add additional data from a file or an online source.
259 Data is only imported, not indexed. You need to call `nominatim-update index`
260 to complete the process.
264 def add_args(parser):
265 group_name = parser.add_argument_group('Source')
266 group = group_name.add_mutually_exclusive_group(required=True)
267 group.add_argument('--file', metavar='FILE',
268 help='Import data from an OSM file')
269 group.add_argument('--diff', metavar='FILE',
270 help='Import data from an OSM diff file')
271 group.add_argument('--node', metavar='ID', type=int,
272 help='Import a single node from the API')
273 group.add_argument('--way', metavar='ID', type=int,
274 help='Import a single way from the API')
275 group.add_argument('--relation', metavar='ID', type=int,
276 help='Import a single relation from the API')
277 group.add_argument('--tiger-data', metavar='DIR',
278 help='Add housenumbers from the US TIGER census database.')
279 group = parser.add_argument_group('Extra arguments')
280 group.add_argument('--use-main-api', action='store_true',
281 help='Use OSM API instead of Overpass to download objects')
286 os.environ['NOMINATIM_TIGER_DATA_PATH'] = args.tiger_data
287 return run_legacy_script('setup.php', '--import-tiger-data', nominatim_env=args)
289 params = ['update.php']
291 params.extend(('--import-file', args.file))
293 params.extend(('--import-diff', args.diff))
295 params.extend(('--import-node', args.node))
297 params.extend(('--import-way', args.way))
299 params.extend(('--import-relation', args.relation))
300 if args.use_main_api:
301 params.append('--use-main-api')
302 return run_legacy_script(*params, nominatim_env=args)
307 Reindex all new and modified data.
311 def add_args(parser):
312 group = parser.add_argument_group('Filter arguments')
313 group.add_argument('--boundaries-only', action='store_true',
314 help="""Index only administrative boundaries.""")
315 group.add_argument('--no-boundaries', action='store_true',
316 help="""Index everything except administrative boundaries.""")
317 group.add_argument('--minrank', '-r', type=int, metavar='RANK', default=0,
318 help='Minimum/starting rank')
319 group.add_argument('--maxrank', '-R', type=int, metavar='RANK', default=30,
320 help='Maximum/finishing rank')
324 indexer = Indexer(args.config.get_libpq_dsn(),
325 args.threads or _num_system_cpus() or 1)
327 if not args.no_boundaries:
328 indexer.index_boundaries(args.minrank, args.maxrank)
329 if not args.boundaries_only:
330 indexer.index_by_rank(args.minrank, args.maxrank)
332 if not args.no_boundaries and not args.boundaries_only:
333 indexer.update_status_table()
340 Recompute auxiliary data used by the indexing process.
342 These functions must not be run in parallel with other update commands.
346 def add_args(parser):
347 group = parser.add_argument_group('Data arguments')
348 group.add_argument('--postcodes', action='store_true',
349 help='Update postcode centroid table')
350 group.add_argument('--word-counts', action='store_true',
351 help='Compute frequency of full-word search terms')
352 group.add_argument('--address-levels', action='store_true',
353 help='Reimport address level configuration')
354 group.add_argument('--functions', action='store_true',
355 help='Update the PL/pgSQL functions in the database')
356 group.add_argument('--wiki-data', action='store_true',
357 help='Update Wikipedia/data importance numbers.')
358 group.add_argument('--importance', action='store_true',
359 help='Recompute place importances (expensive!)')
360 group.add_argument('--website', action='store_true',
361 help='Refresh the directory that serves the scripts for the web API')
362 group = parser.add_argument_group('Arguments for function refresh')
363 group.add_argument('--no-diff-updates', action='store_false', dest='diffs',
364 help='Do not enable code for propagating updates')
365 group.add_argument('--enable-debug-statements', action='store_true',
366 help='Enable debug warning statements in functions')
371 run_legacy_script('update.php', '--calculate-postcodes',
372 nominatim_env=args, throw_on_fail=True)
374 run_legacy_script('update.php', '--recompute-word-counts',
375 nominatim_env=args, throw_on_fail=True)
376 if args.address_levels:
377 run_legacy_script('update.php', '--update-address-levels',
378 nominatim_env=args, throw_on_fail=True)
380 params = ['setup.php', '--create-functions', '--create-partition-functions']
382 params.append('--enable-diff-updates')
383 if args.enable_debug_statements:
384 params.append('--enable-debug-statements')
385 run_legacy_script(*params, nominatim_env=args, throw_on_fail=True)
387 run_legacy_script('setup.php', '--import-wikipedia-articles',
388 nominatim_env=args, throw_on_fail=True)
389 # Attention: importance MUST come after wiki data import.
391 run_legacy_script('update.php', '--recompute-importance',
392 nominatim_env=args, throw_on_fail=True)
394 run_legacy_script('setup.php', '--setup-website',
395 nominatim_env=args, throw_on_fail=True)
398 class AdminCheckDatabase:
400 Check that the database is complete and operational.
404 def add_args(parser):
409 return run_legacy_script('check_import_finished.php', nominatim_env=args)
414 Warm database caches for search and reverse queries.
418 def add_args(parser):
419 group = parser.add_argument_group('Target arguments')
420 group.add_argument('--search-only', action='store_const', dest='target',
422 help="Only pre-warm tables for search queries")
423 group.add_argument('--reverse-only', action='store_const', dest='target',
425 help="Only pre-warm tables for reverse queries")
429 params = ['warm.php']
430 if args.target == 'reverse':
431 params.append('--reverse-only')
432 if args.target == 'search':
433 params.append('--search-only')
434 return run_legacy_script(*params, nominatim_env=args)
439 Export addresses as CSV file from the database.
443 def add_args(parser):
444 group = parser.add_argument_group('Output arguments')
445 group.add_argument('--output-type', default='street',
446 choices=('continent', 'country', 'state', 'county',
447 'city', 'suburb', 'street', 'path'),
448 help='Type of places to output (default: street)')
449 group.add_argument('--output-format',
450 default='street;suburb;city;county;state;country',
451 help="""Semicolon-separated list of address types
452 (see --output-type). Multiple ranks can be
453 merged into one column by simply using a
454 comma-separated list.""")
455 group.add_argument('--output-all-postcodes', action='store_true',
456 help="""List all postcodes for address instead of
457 just the most likely one""")
458 group.add_argument('--language',
459 help="""Preferred language for output
460 (use local name, if omitted)""")
461 group = parser.add_argument_group('Filter arguments')
462 group.add_argument('--restrict-to-country', metavar='COUNTRY_CODE',
463 help='Export only objects within country')
464 group.add_argument('--restrict-to-osm-node', metavar='ID', type=int,
465 help='Export only children of this OSM node')
466 group.add_argument('--restrict-to-osm-way', metavar='ID', type=int,
467 help='Export only children of this OSM way')
468 group.add_argument('--restrict-to-osm-relation', metavar='ID', type=int,
469 help='Export only children of this OSM relation')
474 params = ['export.php',
475 '--output-type', args.output_type,
476 '--output-format', args.output_format]
477 if args.output_all_postcodes:
478 params.append('--output-all-postcodes')
480 params.extend(('--language', args.language))
481 if args.restrict_to_country:
482 params.extend(('--restrict-to-country', args.restrict_to_country))
483 if args.restrict_to_osm_node:
484 params.extend(('--restrict-to-osm-node', args.restrict_to_osm_node))
485 if args.restrict_to_osm_way:
486 params.extend(('--restrict-to-osm-way', args.restrict_to_osm_way))
487 if args.restrict_to_osm_relation:
488 params.extend(('--restrict-to-osm-relation', args.restrict_to_osm_relation))
490 return run_legacy_script(*params, nominatim_env=args)
493 ('street', 'housenumber and street'),
494 ('city', 'city, town or village'),
495 ('county', 'county'),
497 ('country', 'country'),
498 ('postalcode', 'postcode')
502 ('addressdetails', 'Include a breakdown of the address into elements.'),
503 ('extratags', """Include additional information if available
504 (e.g. wikipedia link, opening hours)."""),
505 ('namedetails', 'Include a list of alternative names.')
509 ('addressdetails', 'Include a breakdown of the address into elements.'),
510 ('keywords', 'Include a list of name keywords and address keywords.'),
511 ('linkedplaces', 'Include a details of places that are linked with this one.'),
512 ('hierarchy', 'Include details of places lower in the address hierarchy.'),
513 ('group_hierarchy', 'Group the places by type.'),
514 ('polygon_geojson', 'Include geometry of result.')
519 Execute API search query.
523 def add_args(parser):
524 group = parser.add_argument_group('Query arguments')
525 group.add_argument('--query',
526 help='Free-form query string')
527 for name, desc in STRUCTURED_QUERY:
528 group.add_argument('--' + name, help='Structured query: ' + desc)
530 group = parser.add_argument_group('Output arguments')
531 group.add_argument('--format', default='jsonv2',
532 choices=['xml', 'json', 'jsonv2', 'geojson', 'geocodejson'],
533 help='Format of result')
534 for name, desc in EXTRADATA_PARAMS:
535 group.add_argument('--' + name, action='store_true', help=desc)
537 group.add_argument('--lang', '--accept-language', metavar='LANGS',
538 help='Preferred language order for presenting search results')
539 group.add_argument('--polygon-output',
540 choices=['geojson', 'kml', 'svg', 'text'],
541 help='Output geometry of results as a GeoJSON, KML, SVG or WKT.')
542 group.add_argument('--polygon-threshold', type=float, metavar='TOLERANCE',
543 help="""Simplify output geometry.
544 Parameter is difference tolerance in degrees.""")
546 group = parser.add_argument_group('Result limitation')
547 group.add_argument('--countrycodes', metavar='CC,..',
548 help='Limit search results to one or more countries.')
549 group.add_argument('--exclude_place_ids', metavar='ID,..',
550 help='List of search object to be excluded')
551 group.add_argument('--limit', type=int,
552 help='Limit the number of returned results')
553 group.add_argument('--viewbox', metavar='X1,Y1,X2,Y2',
554 help='Preferred area to find search results')
555 group.add_argument('--bounded', action='store_true',
556 help='Strictly restrict results to viewbox area')
558 group = parser.add_argument_group('Other arguments')
559 group.add_argument('--no-dedupe', action='store_false', dest='dedupe',
560 help='Do not remove duplicates from the result list')
566 params = dict(q=args.query)
568 params = {k : getattr(args, k) for k, _ in STRUCTURED_QUERY if getattr(args, k)}
570 for param, _ in EXTRADATA_PARAMS:
571 if getattr(args, param):
573 for param in ('format', 'countrycodes', 'exclude_place_ids', 'limit', 'viewbox'):
574 if getattr(args, param):
575 params[param] = getattr(args, param)
577 params['accept-language'] = args.lang
578 if args.polygon_output:
579 params['polygon_' + args.polygon_output] = '1'
580 if args.polygon_threshold:
581 params['polygon_threshold'] = args.polygon_threshold
583 params['bounded'] = '1'
585 params['dedupe'] = '0'
587 return run_api_script('search', args.project_dir,
588 phpcgi_bin=args.phpcgi_path, params=params)
592 Execute API reverse query.
596 def add_args(parser):
597 group = parser.add_argument_group('Query arguments')
598 group.add_argument('--lat', type=float, required=True,
599 help='Latitude of coordinate to look up (in WGS84)')
600 group.add_argument('--lon', type=float, required=True,
601 help='Longitude of coordinate to look up (in WGS84)')
602 group.add_argument('--zoom', type=int,
603 help='Level of detail required for the address')
605 group = parser.add_argument_group('Output arguments')
606 group.add_argument('--format', default='jsonv2',
607 choices=['xml', 'json', 'jsonv2', 'geojson', 'geocodejson'],
608 help='Format of result')
609 for name, desc in EXTRADATA_PARAMS:
610 group.add_argument('--' + name, action='store_true', help=desc)
612 group.add_argument('--lang', '--accept-language', metavar='LANGS',
613 help='Preferred language order for presenting search results')
614 group.add_argument('--polygon-output',
615 choices=['geojson', 'kml', 'svg', 'text'],
616 help='Output geometry of results as a GeoJSON, KML, SVG or WKT.')
617 group.add_argument('--polygon-threshold', type=float, metavar='TOLERANCE',
618 help="""Simplify output geometry.
619 Parameter is difference tolerance in degrees.""")
624 params = dict(lat=args.lat, lon=args.lon)
625 if args.zoom is not None:
626 params['zoom'] = args.zoom
628 for param, _ in EXTRADATA_PARAMS:
629 if getattr(args, param):
632 params['format'] = args.format
634 params['accept-language'] = args.lang
635 if args.polygon_output:
636 params['polygon_' + args.polygon_output] = '1'
637 if args.polygon_threshold:
638 params['polygon_threshold'] = args.polygon_threshold
640 return run_api_script('reverse', args.project_dir,
641 phpcgi_bin=args.phpcgi_path, params=params)
646 Execute API reverse query.
650 def add_args(parser):
651 group = parser.add_argument_group('Query arguments')
652 group.add_argument('--id', metavar='OSMID',
653 action='append', required=True, dest='ids',
654 help='OSM id to lookup in format <NRW><id> (may be repeated)')
656 group = parser.add_argument_group('Output arguments')
657 group.add_argument('--format', default='jsonv2',
658 choices=['xml', 'json', 'jsonv2', 'geojson', 'geocodejson'],
659 help='Format of result')
660 for name, desc in EXTRADATA_PARAMS:
661 group.add_argument('--' + name, action='store_true', help=desc)
663 group.add_argument('--lang', '--accept-language', metavar='LANGS',
664 help='Preferred language order for presenting search results')
665 group.add_argument('--polygon-output',
666 choices=['geojson', 'kml', 'svg', 'text'],
667 help='Output geometry of results as a GeoJSON, KML, SVG or WKT.')
668 group.add_argument('--polygon-threshold', type=float, metavar='TOLERANCE',
669 help="""Simplify output geometry.
670 Parameter is difference tolerance in degrees.""")
675 params = dict(osm_ids=','.join(args.ids))
677 for param, _ in EXTRADATA_PARAMS:
678 if getattr(args, param):
681 params['format'] = args.format
683 params['accept-language'] = args.lang
684 if args.polygon_output:
685 params['polygon_' + args.polygon_output] = '1'
686 if args.polygon_threshold:
687 params['polygon_threshold'] = args.polygon_threshold
689 return run_api_script('lookup', args.project_dir,
690 phpcgi_bin=args.phpcgi_path, params=params)
695 Execute API lookup query.
699 def add_args(parser):
700 group = parser.add_argument_group('Query arguments')
701 objs = group.add_mutually_exclusive_group(required=True)
702 objs.add_argument('--node', '-n', type=int,
703 help="Look up the OSM node with the given ID.")
704 objs.add_argument('--way', '-w', type=int,
705 help="Look up the OSM way with the given ID.")
706 objs.add_argument('--relation', '-r', type=int,
707 help="Look up the OSM relation with the given ID.")
708 objs.add_argument('--place_id', '-p', type=int,
709 help='Database internal identifier of the OSM object to look up.')
710 group.add_argument('--class', dest='object_class',
711 help="""Class type to disambiguated multiple entries
712 of the same object.""")
714 group = parser.add_argument_group('Output arguments')
715 for name, desc in DETAILS_SWITCHES:
716 group.add_argument('--' + name, action='store_true', help=desc)
717 group.add_argument('--lang', '--accept-language', metavar='LANGS',
718 help='Preferred language order for presenting search results')
723 params = dict(osmtype='N', osmid=args.node)
725 params = dict(osmtype='W', osmid=args.node)
727 params = dict(osmtype='R', osmid=args.node)
729 params = dict(place_id=args.place_id)
730 if args.object_class:
731 params['class'] = args.object_class
732 for name, _ in DETAILS_SWITCHES:
733 params[name] = '1' if getattr(args, name) else '0'
735 return run_api_script('details', args.project_dir,
736 phpcgi_bin=args.phpcgi_path, params=params)
741 Execute API status query.
745 def add_args(parser):
746 group = parser.add_argument_group('API parameters')
747 group.add_argument('--format', default='text', choices=['text', 'json'],
748 help='Format of result')
752 return run_api_script('status', args.project_dir,
753 phpcgi_bin=args.phpcgi_path,
754 params=dict(format=args.format))
757 def nominatim(**kwargs):
759 Command-line tools for importing, updating, administrating and
760 querying the Nominatim database.
762 parser = CommandlineParser('nominatim', nominatim.__doc__)
764 parser.add_subcommand('import', SetupAll)
765 parser.add_subcommand('freeze', SetupFreeze)
766 parser.add_subcommand('replication', UpdateReplication)
768 parser.add_subcommand('check-database', AdminCheckDatabase)
769 parser.add_subcommand('warm', AdminWarm)
771 parser.add_subcommand('special-phrases', SetupSpecialPhrases)
773 parser.add_subcommand('add-data', UpdateAddData)
774 parser.add_subcommand('index', UpdateIndex)
775 parser.add_subcommand('refresh', UpdateRefresh)
777 parser.add_subcommand('export', QueryExport)
779 if kwargs.get('phpcgi_path'):
780 parser.add_subcommand('search', APISearch)
781 parser.add_subcommand('reverse', APIReverse)
782 parser.add_subcommand('lookup', APILookup)
783 parser.add_subcommand('details', APIDetails)
784 parser.add_subcommand('status', APIStatus)
786 parser.parser.epilog = 'php-cgi not found. Query commands not available.'
788 return parser.run(**kwargs)