implement command line status call in Python

This commit is contained in:
Sarah Hoffmann 2022-11-18 16:11:31 +01:00
parent 860c6ecbcc
commit 45c675bd78
9 changed files with 249 additions and 5 deletions

View File

@ -1,9 +1,10 @@
[mypy]
plugins = sqlalchemy.ext.mypy.plugin
[mypy-icu.*]
ignore_missing_imports = True
[mypy-osmium.*]
[mypy-asyncpg.*]
ignore_missing_imports = True
[mypy-datrie.*]

59
nominatim/api.py Normal file
View File

@ -0,0 +1,59 @@
# SPDX-License-Identifier: GPL-2.0-only
#
# This file is part of Nominatim. (https://nominatim.org)
#
# Copyright (C) 2022 by the Nominatim developer community.
# For a full list of authors see the git log.
"""
Implementation of classes for API access via libraries.
"""
from typing import Mapping, Optional, TypeVar, Callable, Any
import functools
import asyncio
from pathlib import Path
from sqlalchemy.engine.url import URL
from sqlalchemy.ext.asyncio import create_async_engine
from nominatim.typing import StrPath
from nominatim.config import Configuration
from nominatim.apicmd.status import get_status, StatusResult
class NominatimAPIAsync:
""" API loader asynchornous version.
"""
def __init__(self, project_dir: Path,
environ: Optional[Mapping[str, str]] = None) -> None:
self.config = Configuration(project_dir, environ)
dsn = self.config.get_database_params()
dburl = URL.create(
'postgresql+asyncpg',
database=dsn.get('dbname'),
username=dsn.get('user'), password=dsn.get('password'),
host=dsn.get('host'), port=int(dsn['port']) if 'port' in dsn else None,
query={k: v for k, v in dsn.items()
if k not in ('user', 'password', 'dbname', 'host', 'port')})
self.engine = create_async_engine(dburl,
connect_args={"server_settings": {"jit": "off"}},
future=True)
async def status(self) -> StatusResult:
""" Return the status of the database.
"""
return await get_status(self.engine)
class NominatimAPI:
""" API loader, synchronous version.
"""
def __init__(self, project_dir: Path,
environ: Optional[Mapping[str, str]] = None) -> None:
self.async_api = NominatimAPIAsync(project_dir, environ)
def status(self) -> StatusResult:
return asyncio.get_event_loop().run_until_complete(self.async_api.status())

View File

View File

@ -0,0 +1,66 @@
# SPDX-License-Identifier: GPL-2.0-only
#
# This file is part of Nominatim. (https://nominatim.org)
#
# Copyright (C) 2022 by the Nominatim developer community.
# For a full list of authors see the git log.
"""
Classes and function releated to status call.
"""
from typing import Optional, cast
import datetime as dt
import sqlalchemy as sqla
from sqlalchemy.ext.asyncio.engine import AsyncEngine, AsyncConnection
import asyncpg
from nominatim import version
class StatusResult:
""" Result of a call to the status API.
"""
def __init__(self, status: int, msg: str):
self.status = status
self.message = msg
# XXX versions really should stay tuples here
self.software_version = version.version_str()
self.data_updated: Optional[dt.datetime] = None
self.database_version: Optional[str] = None
async def _get_database_date(conn: AsyncConnection) -> Optional[dt.datetime]:
""" Query the database date.
"""
sql = sqla.text('SELECT lastimportdate FROM import_status LIMIT 1')
result = await conn.execute(sql)
for row in result:
return cast(dt.datetime, row[0])
return None
async def _get_database_version(conn: AsyncConnection) -> Optional[str]:
sql = sqla.text("""SELECT value FROM nominatim_properties
WHERE property = 'database_version'""")
result = await conn.execute(sql)
for row in result:
return cast(str, row[0])
return None
async def get_status(engine: AsyncEngine) -> StatusResult:
""" Execute a status API call.
"""
status = StatusResult(0, 'OK')
try:
async with engine.begin() as conn:
status.data_updated = await _get_database_date(conn)
status.database_version = await _get_database_version(conn)
except asyncpg.PostgresError as err:
return StatusResult(700, str(err))
return status

View File

@ -14,6 +14,9 @@ import logging
from nominatim.tools.exec_utils import run_api_script
from nominatim.errors import UsageError
from nominatim.clicmd.args import NominatimArgs
from nominatim.api import NominatimAPI
from nominatim.apicmd.status import StatusResult
import nominatim.result_formatter.v1 as formatting
# Do not repeat documentation of subcommand classes.
# pylint: disable=C0111
@ -264,7 +267,7 @@ class APIDetails:
class APIStatus:
"""\
"""
Execute API status query.
This command works exactly the same as if calling the /status endpoint on
@ -274,10 +277,13 @@ class APIStatus:
"""
def add_args(self, parser: argparse.ArgumentParser) -> None:
formats = formatting.create(StatusResult).list_formats()
group = parser.add_argument_group('API parameters')
group.add_argument('--format', default='text', choices=['text', 'json'],
group.add_argument('--format', default=formats[0], choices=formats,
help='Format of result')
def run(self, args: NominatimArgs) -> int:
return _run_api('status', args, dict(format=args.format))
status = NominatimAPI(args.project_dir).status()
print(formatting.create(StatusResult).format(status, args.format))
return 0

View File

@ -17,6 +17,7 @@ import json
import yaml
from dotenv import dotenv_values
from psycopg2.extensions import parse_dsn
from nominatim.typing import StrPath
from nominatim.errors import UsageError
@ -51,7 +52,7 @@ class Configuration:
Nominatim uses dotenv to configure the software. Configuration options
are resolved in the following order:
* from the OS environment (or the dirctionary given in `environ`
* from the OS environment (or the dictionary given in `environ`)
* from the .env file in the project directory of the installation
* from the default installation in the configuration directory
@ -164,6 +165,18 @@ class Configuration:
return dsn
def get_database_params(self) -> Mapping[str, str]:
""" Get the configured parameters for the database connection
as a mapping.
"""
dsn = self.DATABASE_DSN
if dsn.startswith('pgsql:'):
return dict((p.split('=', 1) for p in dsn[6:].split(';')))
return parse_dsn(dsn)
def get_import_style_file(self) -> Path:
""" Return the import style file as a path object. Translates the
name of the standard styles automatically into a file in the

View File

View File

@ -0,0 +1,63 @@
# SPDX-License-Identifier: GPL-2.0-only
#
# This file is part of Nominatim. (https://nominatim.org)
#
# Copyright (C) 2022 by the Nominatim developer community.
# For a full list of authors see the git log.
"""
Helper classes and function for writing result formatting modules.
"""
from typing import Type, TypeVar, Dict, Mapping, List, Callable, Generic, Any
from collections import defaultdict
T = TypeVar('T') # pylint: disable=invalid-name
FormatFunc = Callable[[T], str]
class ResultFormatter(Generic[T]):
""" This class dispatches format calls to the appropriate formatting
function previously defined with the `format_func` decorator.
"""
def __init__(self, funcs: Mapping[str, FormatFunc[T]]) -> None:
self.functions = funcs
def list_formats(self) -> List[str]:
""" Return a list of formats supported by this formatter.
"""
return list(self.functions.keys())
def format(self, result: T, fmt: str) -> str:
""" Convert the given result into a string using the given format.
The format is expected to be in the list returned by
`list_formats()`.
"""
return self.functions[fmt](result)
class FormatDispatcher:
""" A factory class for result formatters.
"""
def __init__(self) -> None:
self.format_functions: Dict[Type[Any], Dict[str, FormatFunc[Any]]] = defaultdict(dict)
def format_func(self, result_class: Type[T],
fmt: str) -> Callable[[FormatFunc[T]], FormatFunc[T]]:
""" Decorator for a function that formats a given type of result into the
selected format.
"""
def decorator(func: FormatFunc[T]) -> FormatFunc[T]:
self.format_functions[result_class][fmt] = func
return func
return decorator
def __call__(self, result_class: Type[T]) -> ResultFormatter[T]:
""" Create an instance of a format class for the given result type.
"""
return ResultFormatter(self.format_functions[result_class])

View File

@ -0,0 +1,36 @@
# SPDX-License-Identifier: GPL-2.0-only
#
# This file is part of Nominatim. (https://nominatim.org)
#
# Copyright (C) 2022 by the Nominatim developer community.
# For a full list of authors see the git log.
"""
Output formatters for API version v1.
"""
from typing import Dict, Any
from collections import OrderedDict
import json
from nominatim.result_formatter.base import FormatDispatcher
from nominatim.apicmd.status import StatusResult
create = FormatDispatcher()
@create.format_func(StatusResult, 'text')
def _format_status_text(result: StatusResult) -> str:
return result.message
@create.format_func(StatusResult, 'json')
def _format_status_json(result: StatusResult) -> str:
# XXX write a simple JSON serializer
out: Dict[str, Any] = OrderedDict()
out['status'] = result.status
out['message'] = result.message
if result.data_updated is not None:
out['data_updated'] = result.data_updated
out['software_version'] = result.software_version
if result.database_version is not None:
out['database_version'] = result.database_version
return json.dumps(out)