mirror of
https://github.com/osm-search/Nominatim.git
synced 2026-02-26 11:08:13 +00:00
implement command line status call in Python
This commit is contained in:
@@ -1,9 +1,10 @@
|
|||||||
[mypy]
|
[mypy]
|
||||||
|
plugins = sqlalchemy.ext.mypy.plugin
|
||||||
|
|
||||||
[mypy-icu.*]
|
[mypy-icu.*]
|
||||||
ignore_missing_imports = True
|
ignore_missing_imports = True
|
||||||
|
|
||||||
[mypy-osmium.*]
|
[mypy-asyncpg.*]
|
||||||
ignore_missing_imports = True
|
ignore_missing_imports = True
|
||||||
|
|
||||||
[mypy-datrie.*]
|
[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.tools.exec_utils import run_api_script
|
||||||
from nominatim.errors import UsageError
|
from nominatim.errors import UsageError
|
||||||
from nominatim.clicmd.args import NominatimArgs
|
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.
|
# Do not repeat documentation of subcommand classes.
|
||||||
# pylint: disable=C0111
|
# pylint: disable=C0111
|
||||||
@@ -264,7 +267,7 @@ class APIDetails:
|
|||||||
|
|
||||||
|
|
||||||
class APIStatus:
|
class APIStatus:
|
||||||
"""\
|
"""
|
||||||
Execute API status query.
|
Execute API status query.
|
||||||
|
|
||||||
This command works exactly the same as if calling the /status endpoint on
|
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:
|
def add_args(self, parser: argparse.ArgumentParser) -> None:
|
||||||
|
formats = formatting.create(StatusResult).list_formats()
|
||||||
group = parser.add_argument_group('API parameters')
|
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')
|
help='Format of result')
|
||||||
|
|
||||||
|
|
||||||
def run(self, args: NominatimArgs) -> int:
|
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
|
import yaml
|
||||||
|
|
||||||
from dotenv import dotenv_values
|
from dotenv import dotenv_values
|
||||||
|
from psycopg2.extensions import parse_dsn
|
||||||
|
|
||||||
from nominatim.typing import StrPath
|
from nominatim.typing import StrPath
|
||||||
from nominatim.errors import UsageError
|
from nominatim.errors import UsageError
|
||||||
@@ -51,7 +52,7 @@ class Configuration:
|
|||||||
Nominatim uses dotenv to configure the software. Configuration options
|
Nominatim uses dotenv to configure the software. Configuration options
|
||||||
are resolved in the following order:
|
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 .env file in the project directory of the installation
|
||||||
* from the default installation in the configuration directory
|
* from the default installation in the configuration directory
|
||||||
|
|
||||||
@@ -164,6 +165,18 @@ class Configuration:
|
|||||||
return dsn
|
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:
|
def get_import_style_file(self) -> Path:
|
||||||
""" Return the import style file as a path object. Translates the
|
""" Return the import style file as a path object. Translates the
|
||||||
name of the standard styles automatically into a file in 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)
|
||||||
Reference in New Issue
Block a user