mirror of
https://github.com/osm-search/Nominatim.git
synced 2024-08-17 07:00:34 +03:00
implement command line status call in Python
This commit is contained in:
parent
860c6ecbcc
commit
45c675bd78
@ -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
59
nominatim/api.py
Normal 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())
|
0
nominatim/apicmd/__init__.py
Normal file
0
nominatim/apicmd/__init__.py
Normal file
66
nominatim/apicmd/status.py
Normal file
66
nominatim/apicmd/status.py
Normal 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
|
@ -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
|
||||
|
@ -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
|
||||
|
0
nominatim/result_formatter/__init__.py
Normal file
0
nominatim/result_formatter/__init__.py
Normal file
63
nominatim/result_formatter/base.py
Normal file
63
nominatim/result_formatter/base.py
Normal 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])
|
36
nominatim/result_formatter/v1.py
Normal file
36
nominatim/result_formatter/v1.py
Normal 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)
|
Loading…
Reference in New Issue
Block a user