diff --git a/pyproject.toml b/pyproject.toml index 25713f2..979992c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,7 @@ classifiers = [ ] dependencies = [ "biothings[web_extra]==1.0.2", + "pycurl", "GitPython>=3.1.43", "bmt>=1.4.5", ] diff --git a/src/nodenorm/__main__.py b/src/nodenorm/__main__.py index fb10e2b..42140ea 100644 --- a/src/nodenorm/__main__.py +++ b/src/nodenorm/__main__.py @@ -3,13 +3,12 @@ """ import logging -import sys -import pathlib -import importlib.resources from tornado.options import define, options -from nodenorm.application import NodeNormalizationAPILauncher +from nodenorm.application import NodeNormalizationAPI +from nodenorm.namespace import NodeNormalizationAPINamespace +from nodenorm.server import NodeNormalizationWebServer logger = logging.getLogger(__name__) @@ -19,18 +18,13 @@ # Web Server Settings # -------------------------- -define("address", default="localhost", help="web server host ipv4 address. Defaults to localhost") -define("port", default=8000, help="web server host ipv4 port. Defaults to 8000") -define("autoreload", default=False, help="toggle web server auto reload when file changes are detected") +define("host", default=None, help="web server host ipv4 address") +define("port", default=None, help="web server host ipv4 port") # Configuration Settings # -------------------------- define("conf", default=None, help="override configuration file for settings configuration") -# Logger Settings -# -------------------------- -define("debug", default=False, help="toggle web server logging preferences to increase logging output") - def main(): """ @@ -41,16 +35,11 @@ def main(): We only have one "plugin" in this case to load, so we can short-cut some of the logic used from the pending.api application that assumes more than one """ - use_curl = False - app_settings = {"static_path": "static"} - options.parse_command_line() - breakpoint() - - # with open( - - launcher = NodeNormalizationAPILauncher(options, app_settings, use_curl) - launcher.start(host=launcher.host, port=options.port) + configuration_namespace = NodeNormalizationAPINamespace(options) + application_instance = NodeNormalizationAPI.get_app(configuration_namespace) + webserver = NodeNormalizationWebServer(application_instance, configuration_namespace) + webserver.start() if __name__ == "__main__": diff --git a/src/nodenorm/application.py b/src/nodenorm/application.py index 5b21c00..7e03378 100644 --- a/src/nodenorm/application.py +++ b/src/nodenorm/application.py @@ -6,149 +6,25 @@ """ import logging -from pprint import pformat -from types import SimpleNamespace +from typing import override -from biothings import __version__ -from biothings.web import connections from biothings.web.applications import TornadoBiothingsAPI -from biothings.web.services.metadata import BiothingsESMetadata -import tornado.httpserver -import tornado.ioloop -import tornado.log -import tornado.options -import tornado.web from nodenorm.handlers import build_handlers +from nodenorm.namespace import NodeNormalizationAPINamespace logger = logging.getLogger(__name__) -class NodeNormalizationAPILauncher: - def __init__( - self, options: tornado.options.OptionParser, app_handlers: list[tuple], app_settings: dict, use_curl: bool - ): - logging.info("Biothings API %s", __version__) - self.handlers = app_handlers - self.host = options.address - self.settings = self.configure_settings(options, app_settings) - self.config = load_configuration(options.conf) - self.configure_logging() - - self.application = NodeNormalizationAPI.get_app(self.config, self.settings, self.handlers) - - if use_curl: - self.enable_curl_httpclient() - - def configure_settings(self, options: tornado.options.OptionParser, app_settings: dict) -> dict: - """ - Configure the `settings` attribute for the launcher - """ - app_settings.update(debug=options.debug) - app_settings.update(autoreload=options.autoreload) - return app_settings - - def configure_logging(self): - root_logger = logging.getLogger() - - logging.getLogger("urllib3").setLevel(logging.ERROR) - logging.getLogger("elasticsearch").setLevel(logging.WARNING) - - if self.settings["debug"]: - root_logger.setLevel(logging.DEBUG) - else: - root_logger.setLevel(logging.INFO) - - @staticmethod - def enable_curl_httpclient(): - """ - Use curl implementation for tornado http clients. - More on https://www.tornadoweb.org/en/stable/httpclient.html - """ - curl_httpclient_option = "tornado.curl_httpclient.CurlAsyncHTTPClient" - tornado.httpclient.AsyncHTTPClient.configure(curl_httpclient_option) - - def start(self, host: str = None, port: int = None): - """ - Starts the HTTP server and IO loop used for running - the pending.api backend - """ - - if host is None: - host = "0.0.0.0" - - if port is None: - port = 8000 - - port = str(port) - - http_server = tornado.httpserver.HTTPServer(self.application, xheaders=True) - http_server.listen(port, host) - - logger.info( - "nodenormalization-api web server is running on %s:%s ...\n nodenormalization handlers:\n%s", - host, - port, - pformat(self.application.biothings.handlers, width=200), - ) - loop = tornado.ioloop.IOLoop.instance() - loop.start() - - -class NodeNormalizationNamespace: - """Simplied namespace instance for our NodeNormalization API. - - The namespace loads our configuration for the web API - """ - - def __init__(self, configuration: dict): - self.config = configuration - self.handlers = {} - self.elasticsearch: SimpleNamespace = SimpleNamespace() - self.configure_elasticsearch() - - def configure_elasticsearch(self): - """Main configuration method for generating our elasticsearch client instance(s). - - Simplified significantly compared to the base namespace as we don't need any infrastructure - for querying as we handle that in the handlers - """ - self.elasticsearch = SimpleNamespace() - - self.elasticsearch.client = connections.es.get_client(self.config.ES_HOST, **self.config.ES_ARGS) - self.elasticsearch.async_client = connections.es.get_async_client(self.config.ES_HOST, **self.config.ES_ARGS) - - self.elasticsearch.metadata = BiothingsESMetadata( - self.config.ES_INDICES, - self.elasticsearch.async_client, - ) - - class NodeNormalizationAPI(TornadoBiothingsAPI): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + @override @classmethod - def get_app(cls, config, settings=None, handlers=None): - """ - Return the tornado.web.Application defined by this config. - **Additional** settings and handlers are accepted as parameters. - """ - biothings = NodeNormalizationNamespace(config) + def get_app(cls, namespace: NodeNormalizationAPINamespace): + """Generator for the TornadoApplication instance.""" handlers = build_handlers() - app = cls(handlers) - app.biothings = biothings - app.populate_handlers(handlers) + namespace.populate_handlers(handlers) + settings = namespace.config.webserver["SETTINGS"] + app = cls(handlers.values(), settings) + app.biothings = namespace return app - - def populate_handlers(self, handlers): - """Populates the handler routes for the NodeNormalization API. - - These routes take the following form: `(regex, handler_class, options)` tuples - `_. - - Overrides the _get_handlers method provided by TornadoBiothingsAPI as we don't need - the custom implementation for handling how we parse the handler path - """ - for handler in handlers: - self.biothings.handlers[handler[0]] = handler[1] diff --git a/src/nodenorm/handlers/biolink.py b/src/nodenorm/biolink.py similarity index 100% rename from src/nodenorm/handlers/biolink.py rename to src/nodenorm/biolink.py diff --git a/src/nodenorm/config/config.default.json b/src/nodenorm/config/config.default.json index a9fccf2..99e1c26 100644 --- a/src/nodenorm/config/config.default.json +++ b/src/nodenorm/config/config.default.json @@ -1,12 +1,31 @@ { + "webserver": { + "HOST": "localhost", + "PORT": 8000, + "ENABLE_CURL_CLIENT": true, + "SETTINGS": { + "debug": true, + "autoreload": true + } + }, + "api": { + "API_PREFIX": "nodenorm", + "API_VERSION": "" + }, "elasticsearch": { - "ES_HOST": "localhost:9200", - "ES_INDEX": = "nodenorm", - "ES_DOC_TYPE": = "node" + "ES_HOST": "http://localhost:9200", + "ES_INDEX": "nodenorm", + "ES_ALIAS": "nodenorm", + "ES_DOC_TYPE": "node", + "ES_INDICES": {}, + "ES_ARGS": { + "sniff": false, + "request_timeout": 60 + } }, "telemetry": { "OPENTELEMETRY_ENABLED": false, - "OPENTELEMETRY_SERVICE_NAME":"NodeNorm", + "OPENTELEMETRY_SERVICE_NAME": "NodeNorm", "OPENTELEMETRY_JAEGER_HOST": "http://localhost", "OPENTELEMETRY_JAEGER_PORT": 6831 } diff --git a/src/nodenorm/handlers/__init__.py b/src/nodenorm/handlers/__init__.py index 0b60628..308dd6b 100644 --- a/src/nodenorm/handlers/__init__.py +++ b/src/nodenorm/handlers/__init__.py @@ -1,5 +1,9 @@ +import importlib.resources from typing import Callable +import tornado.web + +import nodenorm from nodenorm.handlers.conflations import ValidConflationsHandler from nodenorm.handlers.health import NodeNormHealthHandler from nodenorm.handlers.normalized_nodes import NormalizedNodesHandler @@ -8,14 +12,6 @@ from nodenorm.handlers.version import VersionHandler -API_PREFIX = "nodenorm" -API_VERSION = "" - -ES_HOST = "http://su10.scripps.edu:9200" -ES_INDEX = "nodenorm_20251106_lv86fxt0" -ES_DOC_TYPE = "node" - - def build_handlers() -> dict[str, tuple[str, Callable]]: """Generate our handler mapping for the nodenorm API.""" @@ -27,5 +23,27 @@ def build_handlers() -> dict[str, tuple[str, Callable]]: (r"/status?", NodeNormHealthHandler), (r"/version", VersionHandler), ] + # build static file frontend + package_directory = importlib.resources.files(nodenorm) + webapp_directory = package_directory.joinpath("webapp") + + # This points to all the assets available to use via the webapp for our swaggerui + asset_handler = (r"/webapp/(.*)", tornado.web.StaticFileHandler, {"path": str(webapp_directory)}) + handler_collection.append(asset_handler) + + index_handler = ( + r"/()", + tornado.web.StaticFileHandler, + { + "path": str(webapp_directory), + "default_filename": "index.html", + }, + ) + handler_collection.append(index_handler) + + # This redirect ensures we default so the favicon icon can be found in the webapp directory + favicon_handler = (r"/favicon.ico", tornado.web.RedirectHandler, {"url": "/webapp/swaggerui/favicon-32x32.png"}) + handler_collection.append(favicon_handler) + handlers = {handler[0]: handler for handler in handler_collection} return handlers diff --git a/src/nodenorm/handlers/conflations.py b/src/nodenorm/handlers/conflations.py index 47895ec..0901152 100644 --- a/src/nodenorm/handlers/conflations.py +++ b/src/nodenorm/handlers/conflations.py @@ -1,12 +1,12 @@ import logging -from biothings.web.handlers import BaseAPIHandler +from biothings.web.handlers import BaseHandler logger = logging.getLogger(__name__) -class ValidConflationsHandler(BaseAPIHandler): +class ValidConflationsHandler(BaseHandler): name = "allowed-conflations" async def get(self): diff --git a/src/nodenorm/handlers/curie_prefix.py b/src/nodenorm/handlers/curie_prefix.py index 62ce152..e4a4dc6 100644 --- a/src/nodenorm/handlers/curie_prefix.py +++ b/src/nodenorm/handlers/curie_prefix.py @@ -1,10 +1,10 @@ -from biothings.web.handlers import BaseAPIHandler +from biothings.web.handlers import BaseHandler from tornado.web import HTTPError -from nodenorm.handlers.biolink import toolkit +from nodenorm.biolink import toolkit -class SemanticTypeHandler(BaseAPIHandler): +class SemanticTypeHandler(BaseHandler): """ Mirror implementation to the renci implementation found at https://nodenormalization-sri.renci.org/docs @@ -18,9 +18,9 @@ async def get(self) -> dict: type_aggregation = {"unique_types": {"terms": {"field": "type", "size": 100}}} source_fields = ["type"] try: - index = self.biothings.elasticsearch.metadata.indices["node"] + search_indices = self.biothings.elasticsearch.indices type_aggregation_result = await self.biothings.elasticsearch.async_client.search( - aggregations=type_aggregation, index=index, size=0, source_includes=source_fields + aggregations=type_aggregation, index=search_indices, size=0, source_includes=source_fields ) except Exception as gen_exc: network_error = HTTPError( @@ -42,9 +42,9 @@ async def post(self) -> dict: type_aggregation = {"unique_types": {"terms": {"field": "type", "size": 100}}} source_fields = ["type"] try: - index = self.biothings.elasticsearch.metadata.indices["node"] + search_indices = self.biothings.elasticsearch.indices type_aggregation_result = await self.biothings.elasticsearch.async_client.search( - aggregations=type_aggregation, index=index, size=0, source_includes=source_fields + aggregations=type_aggregation, index=search_indices, size=0, source_includes=source_fields ) except Exception as gen_exc: network_error = HTTPError( diff --git a/src/nodenorm/handlers/health.py b/src/nodenorm/handlers/health.py index c4af276..0955604 100644 --- a/src/nodenorm/handlers/health.py +++ b/src/nodenorm/handlers/health.py @@ -1,11 +1,13 @@ from urllib.parse import urlparse -from biothings.web.handlers import BaseAPIHandler +from elasticsearch import AsyncElasticsearch -from nodenorm.handlers.biolink import BIOLINK_MODEL_VERSION +from biothings.web.handlers import BaseHandler +from nodenorm.biolink import BIOLINK_MODEL_VERSION -class NodeNormHealthHandler(BaseAPIHandler): + +class NodeNormHealthHandler(BaseHandler): """ Important Endpoints * /_cat/nodes @@ -14,7 +16,10 @@ class NodeNormHealthHandler(BaseAPIHandler): name = "health" async def get(self): + async_client: AsyncElasticsearch = self.biothings.elasticsearch.async_client + search_indices = self.biothings.elasticsearch.indices + biothings_metadata = await async_client.indices.get(search_indices) compendia_url = self.biothings.metadata.biothing_metadata["node"]["src"]["nodenorm"]["url"] parsed_compendia_url = urlparse(compendia_url) babel_version = parsed_compendia_url.path.split("/")[-2] @@ -35,7 +40,7 @@ async def get(self): "uptime,version", ] h_string = ",".join(attributes) - cat_nodes_response = await self.biothings.elasticsearch.async_client.cat.nodes(format="json", h=h_string) + cat_nodes_response = await async_client.cat.nodes(format="json", h=h_string) nodes_status = {node["name"]: node for node in cat_nodes_response} nodes = {"elasticsearch": {"nodes": nodes_status}} except Exception: diff --git a/src/nodenorm/handlers/normalized_nodes.py b/src/nodenorm/handlers/normalized_nodes.py index 12a4105..c5ae18b 100644 --- a/src/nodenorm/handlers/normalized_nodes.py +++ b/src/nodenorm/handlers/normalized_nodes.py @@ -1,13 +1,14 @@ import dataclasses +import json import logging import time from typing import Union -from biothings.web.handlers import BaseAPIHandler -from biothings.web.services.namespace import BiothingsNamespace +from biothings.web.handlers import BaseHandler from tornado.web import HTTPError -from nodenorm.handlers.biolink import toolkit +from nodenorm.biolink import toolkit +from nodenorm.namespace import NodeNormalizationAPINamespace logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) @@ -24,7 +25,7 @@ class NormalizedNode: taxa: list[str] -class NormalizedNodesHandler(BaseAPIHandler): +class NormalizedNodesHandler(BaseHandler): """ Mirror implementation to the renci implementation found at https://nodenormalization-sri.renci.org/docs @@ -131,17 +132,17 @@ async def post(self): } } """ - breakpoint() - normalization_curies = self.args_json.get("curies", []) + post_body: dict = json.loads(self.request.body) + normalization_curies = post_body.get("curies", []) if len(normalization_curies) == 0: raise HTTPError( detail="Missing curie argument, there must be at least one curie to normalize", status_code=400 ) - conflate = self.args_json.get("conflate", True) - drug_chemical_conflate = self.args_json.get("drug_chemical_conflate", False) - description = self.args_json.get("description", False) - individual_types = self.args_json.get("individual_types", False) + conflate = post_body.get("conflate", True) + drug_chemical_conflate = post_body.get("drug_chemical_conflate", False) + description = post_body.get("description", False) + individual_types = post_body.get("individual_types", False) normalized_nodes = await get_normalized_nodes( self.biothings, @@ -161,7 +162,7 @@ async def post(self): async def get_normalized_nodes( - biothings_metadata: BiothingsNamespace, + biothings_metadata: NodeNormalizationAPINamespace, curies: list[str], conflate_gene_protein: bool = False, conflate_chemical_drug: bool = False, @@ -293,7 +294,7 @@ async def create_normalized_node( async def _lookup_curie_metadata( - biothings_metadata: BiothingsNamespace, curies: list[str], conflations: dict + biothings_metadata: NodeNormalizationAPINamespace, curies: list[str], conflations: dict ) -> list[NormalizedNode]: """ Handles the lookup process for the CURIE identifiers within our elasticsearch instance @@ -393,8 +394,6 @@ async def _lookup_curie_metadata( replacement_types = unique_list(replacement_types) - labels = [identifier.get("l", "") for identifier in replacement_identifiers] - node = NormalizedNode( curie=input_curie, canonical_identifier=canonical_identifier, @@ -406,7 +405,6 @@ async def _lookup_curie_metadata( ) nodes.append(node) else: - labels = [identifier.get("l", "") for identifier in identifiers] node = NormalizedNode( curie=input_curie, canonical_identifier=canonical_identifier, @@ -454,16 +452,16 @@ def unique_list(seq) -> list: async def _lookup_equivalent_identifiers( - biothings_metadata: BiothingsNamespace, curies: list[str] + biothings_metadata: NodeNormalizationAPINamespace, curies: list[str] ) -> tuple[list, list]: if len(curies) == 0: return [], [] curie_terms_query = {"bool": {"filter": [{"terms": {"identifiers.i": curies}}]}} source_fields = ["identifiers", "type", "ic", "preferred_name", "taxa"] - index = biothings_metadata.elasticsearch.metadata.indices["node"] + search_indices = biothings_metadata.elasticsearch.indices term_search_result = await biothings_metadata.elasticsearch.async_client.search( - query=curie_terms_query, index=index, size=len(curies), source_includes=source_fields + query=curie_terms_query, index=search_indices, size=len(curies), source_includes=source_fields ) # Post processing to ensure we can identify invalid curies provided by the query diff --git a/src/nodenorm/handlers/semantic_types.py b/src/nodenorm/handlers/semantic_types.py index 4f3ed70..05a3219 100644 --- a/src/nodenorm/handlers/semantic_types.py +++ b/src/nodenorm/handlers/semantic_types.py @@ -1,10 +1,10 @@ -from biothings.web.handlers import BaseAPIHandler +from biothings.web.handlers import BaseHandler from tornado.web import HTTPError -from nodenorm.handlers.biolink import toolkit +from nodenorm.biolink import toolkit -class SemanticTypeHandler(BaseAPIHandler): +class SemanticTypeHandler(BaseHandler): """ Mirror implementation to the renci implementation found at https://nodenormalization-sri.renci.org/docs @@ -18,9 +18,9 @@ async def get(self) -> dict: type_aggregation = {"unique_types": {"terms": {"field": "type", "size": 100}}} source_fields = ["type"] try: - index = self.biothings.elasticsearch.metadata.indices["node"] + search_indices = self.biothings.elasticsearch.indices type_aggregation_result = await self.biothings.elasticsearch.async_client.search( - aggregations=type_aggregation, index=index, size=0, source_includes=source_fields + aggregations=type_aggregation, index=search_indices, size=0, source_includes=source_fields ) except Exception as gen_exc: network_error = HTTPError( diff --git a/src/nodenorm/handlers/set_identifiers.py b/src/nodenorm/handlers/set_identifiers.py index 3fbd5c8..1bb160e 100644 --- a/src/nodenorm/handlers/set_identifiers.py +++ b/src/nodenorm/handlers/set_identifiers.py @@ -1,15 +1,17 @@ # set_id.py # Code related to generating IDs for sets (as in https://github.com/TranslatorSRI/NodeNormalization/issues/256). import dataclasses +import json import logging import uuid from typing import Optional -from biothings.web.handlers import BaseAPIHandler -from biothings.web.services.namespace import BiothingsNamespace +from biothings.web.handlers import BaseHandler + from tornado.web import HTTPError from nodenorm.handlers.normalized_nodes import get_normalized_nodes +from nodenorm.namespace import NodeNormalizationAPINamespace @dataclasses.dataclass() @@ -22,7 +24,7 @@ class SetIDResponse: setid: Optional[str] = None -class SetIdentifierHandler(BaseAPIHandler): +class SetIdentifierHandler(BaseHandler): """ Mirror implementation to the renci implementation found at https://nodenormalization-sri.renci.org/docs @@ -53,18 +55,20 @@ async def get(self): self.finish(set_identifiers) async def post(self): - curie_arguments = self.args_json - if len(curie_arguments) == 0: + post_body: list[dict] = json.loads(self.request.body) + if len(post_body) == 0: raise HTTPError( detail="Missing JSON body, there must be at least one curie to generate a set identifier", status_code=400, ) - set_identifiers = [] - for group in curie_arguments: + # We have to make a minor change to the API to ensure we're avoiding a security concern + # enforced by tornado, so we return a dictionary here instead of a list + set_identifiers = {} + for index, group in enumerate(post_body): curies = group.get("curies", []) conflations = group.get("conflations", []) - set_identifiers.append(await generate_setid(self.biothings, curies, conflations)) + set_identifiers[index] = await generate_setid(self.biothings, curies, conflations) if not set_identifiers: raise HTTPError(detail="Error occurred during processing.", status_code=500) @@ -72,7 +76,9 @@ async def post(self): self.finish(set_identifiers) -async def generate_setid(biothings_metadata: BiothingsNamespace, curies: list[str], conflations: list[str]) -> dict: +async def generate_setid( + biothings_metadata: NodeNormalizationAPINamespace, curies: list[str], conflations: list[str] +) -> dict: """ Generate a SetID for a set of curies. diff --git a/src/nodenorm/handlers/version.py b/src/nodenorm/handlers/version.py index c04744a..b1753cc 100644 --- a/src/nodenorm/handlers/version.py +++ b/src/nodenorm/handlers/version.py @@ -2,12 +2,12 @@ import pathlib import git -from biothings.web.handlers import BaseAPIHandler +from biothings.web.handlers import BaseHandler logger = logging.getLogger(__name__) -class VersionHandler(BaseAPIHandler): +class VersionHandler(BaseHandler): name = "version" def get_github_commit_hash(self): diff --git a/src/nodenorm/namespace.py b/src/nodenorm/namespace.py new file mode 100644 index 0000000..be7811b --- /dev/null +++ b/src/nodenorm/namespace.py @@ -0,0 +1,176 @@ +import importlib.resources +import importlib.util +import json +import logging +import pathlib +import sys +import types + +import tornado.options + +from biothings.web import connections +from biothings.web.services.metadata import BiothingsESMetadata + +import nodenorm + + +logger = logging.getLogger(__name__) + + +class NodeNormalizationAPINamespace: + """Simplied namespace instance for our NodeNormalization API.""" + + def __init__(self, option_configuration: tornado.options.OptionParser): + self.handlers = {} + self.default_configuration = "config.default.json" + self.config: types.SimpleNamespace = self.load_configuration(option_configuration) + self.elasticsearch: types.SimpleNamespace = self.configure_elasticsearch() + if self._is_open_telemetry_configurable(): + self.configure_telemetry() + + def _is_open_telemetry_configurable(self) -> bool: + """Check for verifying if we can configure opentelemetry.""" + opentelemetry_enabled = self.config.telemetry["OPENTELEMETRY_ENABLED"] + + opentelemetry_module = "opentelemetry" + opentelemetry_installed = ( + opentelemetry_module not in sys.modules and importlib.util.find_spec(opentelemetry_module) is None + ) + + if not opentelemetry_enabled: + logger.debug( + "OPENTELEMETRY is disabled. If you wish to enable it, set the OPENTELEMETRY_ENABLED value to " + ) + return False + + if not opentelemetry_installed: + logging.warning( + ( + "`opentelemetry` package not found, unable to enable opentelemetry." + "Use `pip install nodenorm[telemetry]` to install required packages." + ) + ) + return False + + return opentelemetry_enabled and opentelemetry_installed + + def configure_elasticsearch(self) -> types.SimpleNamespace: + """Main configuration method for generating our elasticsearch client instance(s). + + Simplified significantly compared to the base namespace as we don't need any infrastructure + for querying as we perform query building in the handlers themselves directly + """ + elasticsearch_namespace = types.SimpleNamespace() + elasticsearch_configuration = self.config.elasticsearch + + elasticsearch_namespace.client = connections.es.get_client( + elasticsearch_configuration["ES_HOST"], **elasticsearch_configuration["ES_ARGS"] + ) + elasticsearch_namespace.async_client = connections.es.get_async_client( + elasticsearch_configuration["ES_HOST"], **elasticsearch_configuration["ES_ARGS"] + ) + elasticsearch_namespace.indices = self._validate_elasticsearch_index(elasticsearch_namespace) + return elasticsearch_namespace + + def _validate_elasticsearch_index(self, elasticsearch_namespace: types.SimpleNamespace) -> dict: + """Validates the elasticsearch index / alias. + + Ensures we have a valid index pointing to our cluster for + nodenorm. Raises an error if we cannot find a valid index + or alias from the configuration + """ + config_index = self.config.elasticsearch["ES_INDEX"] + config_alias = self.config.elasticsearch["ES_ALIAS"] + + elasticsearch_index = set() + if config_index != "" and elasticsearch_namespace.client.indices.exists(index=config_index): + elasticsearch_index.add(config_index) + elif elasticsearch_namespace.client.indices.exists_alias(name=config_alias): + elasticsearch_index.add(config_alias) + + elasticsearch_index = list(elasticsearch_index) + + if len(elasticsearch_index) == 0: + raise RuntimeError("Unable to validate nodenorm elasticsearch index / alias") + + return elasticsearch_index + + def configure_telemetry(self): + """Configure our opentelemetry for our web API.""" + from opentelemetry.instrumentation.tornado import TornadoInstrumentor # pylint: disable=import-outside-toplevel + from opentelemetry.exporter.jaeger.thrift import JaegerExporter # pylint: disable=import-outside-toplevel + from opentelemetry.sdk.resources import SERVICE_NAME, Resource # pylint: disable=import-outside-toplevel + from opentelemetry.sdk.trace import TracerProvider # pylint: disable=import-outside-toplevel + from opentelemetry.sdk.trace.export import BatchSpanProcessor # pylint: disable=import-outside-toplevel + from opentelemetry import trace # pylint: disable=import-outside-toplevel + + TornadoInstrumentor().instrument() + + trace_exporter = JaegerExporter( + agent_host_name=self.config.telemetry["OPENTELEMETRY_JAEGER_HOST"], + agent_port=self.config.telemetry["OPENTELEMETRY_JAEGER_PORT"], + udp_split_oversized_batches=True, + ) + + trace_provider = TracerProvider( + resource=Resource.create({SERVICE_NAME: self.config.telemetry["OPENTELEMETRY_SERVICE_NAME"]}) + ) + trace_provider.add_span_processor(BatchSpanProcessor(trace_exporter)) + + # Set the trace provider globally + trace.set_tracer_provider(trace_provider) + + def load_configuration(self, option_configuration: tornado.options.OptionParser) -> types.SimpleNamespace: + """Load the json configuration file for our webserver + nodenorm api. + + Loads the default configuration file found in ~/src/nodenorm/config/config.default.json + to create a base structure. It then checks if an optional configuration file was provided + via the command line options. We override the default configuration file with this option + based file. After that, we setup our static path structure for loading the webapp containing + our swaggerui frontend. We check any other command line options and then return our + configuration data structure + """ + configuration = {} + + # note we have to use this format as relative paths were only supported for + # importlib.resources.read_text in python3.13 + default_configuration_path = importlib.resources.files(nodenorm) / "config" / self.default_configuration + with default_configuration_path.open("r", encoding="utf-8") as handle: + default_configuration = json.load(handle) + + configuration.update(default_configuration) + + if option_configuration.conf is not None: + optional_configuration = pathlib.Path(option_configuration.conf).absolute().resolve() + if optional_configuration.exists(): + with open(optional_configuration, "r", encoding="utf-8") as handle: + configuration.update(json.load(handle)) + + # Force the static path to the webapp directory + package_directory = importlib.resources.files(nodenorm) + webapp_directory = package_directory.joinpath("webapp") + configuration["webserver"]["SETTINGS"]["static_path"] = str(webapp_directory) + configuration["webserver"]["SETTINGS"]["static_url_prefix"] = "/" + + configuration_namespace = types.SimpleNamespace(**configuration) + + # override options + if option_configuration.host is not None: + configuration_namespace.websever.HOST = option_configuration.host + + if option_configuration.port is not None: + configuration_namespace.websever.PORT = option_configuration.port + + return configuration_namespace + + def populate_handlers(self, handlers): + """Populates the handler routes for the NodeNormalization API. + + These routes take the following form: `(regex, handler_class, options)` tuples + `_. + + Overrides the _get_handlers method provided by TornadoBiothingsAPI as we don't need + the custom implementation for handling how we parse the handler path + """ + for handler in handlers.values(): + self.handlers[handler[0]] = handler[1:] diff --git a/src/nodenorm/server.py b/src/nodenorm/server.py new file mode 100644 index 0000000..fa727ee --- /dev/null +++ b/src/nodenorm/server.py @@ -0,0 +1,71 @@ +""" +NodeNormalization specific application builder for overriding +the default builder provided by the biothings.api package + +Responsible for generating the tornado.web.Application instance +""" + +import logging +from pprint import pformat + +from biothings import __version__ +import tornado.httpserver +import tornado.ioloop +import tornado.log +import tornado.options +import tornado.web + +from nodenorm.namespace import NodeNormalizationAPINamespace +from nodenorm.application import NodeNormalizationAPI + + +logger = logging.getLogger(__name__) + + +class NodeNormalizationWebServer: + def __init__(self, application: NodeNormalizationAPI, namespace: NodeNormalizationAPINamespace): + logger.info("Biothings API %s", __version__) + self.application = application + self.namespace = namespace + + if self.namespace.config.webserver["ENABLE_CURL_CLIENT"]: + self.enable_curl_httpclient() + + @staticmethod + def enable_curl_httpclient(): + """ + Use curl implementation for tornado http clients. + More on https://www.tornadoweb.org/en/stable/httpclient.html + """ + curl_httpclient_option = "tornado.curl_httpclient.CurlAsyncHTTPClient" + tornado.httpclient.AsyncHTTPClient.configure(curl_httpclient_option) + + def start(self): + """ + Starts the HTTP server and IO loop used for running + the pending.api backend + """ + host = self.namespace.config.webserver["HOST"] + port = int(self.namespace.config.webserver["PORT"]) + try: + http_server = tornado.httpserver.HTTPServer(self.application, xheaders=True) + http_server.listen(port, host) + except Exception as gen_exc: + logger.exception(gen_exc) + logger.error("Unable to create server instance on [%s:%s]", host, port) + + logger.info( + "nodenormalization-api web server is running on %s:%s ...\n nodenormalization handlers:\n%s", + host, + port, + pformat(self.namespace.handlers, width=200), + ) + + try: + loop = tornado.ioloop.IOLoop.instance() + loop.start() + except Exception as gen_exc: + logger.exception(gen_exc) + raise gen_exc + finally: + loop.close() diff --git a/src/nodenorm/settings/telemetry.py b/src/nodenorm/settings/telemetry.py deleted file mode 100644 index c1bd60e..0000000 --- a/src/nodenorm/settings/telemetry.py +++ /dev/null @@ -1,33 +0,0 @@ -import os - - -OPENTELEMETRY_ENABLED = os.getenv("OPENTELEMETRY_ENABLED", OPENTELEMETRY_ENABLED).lower() - - -if OPENTELEMETRY_ENABLED == "true": - OPENTELEMETRY_JAEGER_HOST = os.getenv("OPENTELEMETRY_JAEGER_HOST", OPENTELEMETRY_JAEGER_HOST) - OPENTELEMETRY_JAEGER_PORT = int(os.getenv("OPENTELEMETRY_JAEGER_PORT", OPENTELEMETRY_JAEGER_PORT)) - OPENTELEMETRY_SERVICE_NAME = os.getenv("OPENTELEMETRY_SERVICE_NAME", OPENTELEMETRY_SERVICE_NAME) - - from opentelemetry.instrumentation.tornado import TornadoInstrumentor - - TornadoInstrumentor().instrument() - - # Configure the OpenTelemetry exporter - from opentelemetry.exporter.jaeger.thrift import JaegerExporter - from opentelemetry.sdk.resources import SERVICE_NAME, Resource - from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.sdk.trace.export import BatchSpanProcessor - from opentelemetry import trace - - trace_exporter = JaegerExporter( - agent_host_name=OPENTELEMETRY_JAEGER_HOST, - agent_port=OPENTELEMETRY_JAEGER_PORT, - udp_split_oversized_batches=True, - ) - - trace_provider = TracerProvider(resource=Resource.create({SERVICE_NAME: OPENTELEMETRY_SERVICE_NAME})) - trace_provider.add_span_processor(BatchSpanProcessor(trace_exporter)) - - # Set the trace provider globally - trace.set_tracer_provider(trace_provider) diff --git a/src/nodenorm/webapp/openapi.json b/src/nodenorm/webapp/openapi.json index 1797bbf..418203e 100644 --- a/src/nodenorm/webapp/openapi.json +++ b/src/nodenorm/webapp/openapi.json @@ -29,6 +29,7 @@ "/status": { "get": { "summary": "Status information on this NodeNorm instance", + "tags": ["health"], "description": "Returns information about this NodeNorm instance and the elasticsearch instance backing it.", "operationId": "status", "responses": { @@ -49,6 +50,7 @@ "/get_normalized_nodes": { "get": { "summary": "Get the equivalent identifiers and semantic types for the curie(s) entered.", + "tags": ["translator"], "description": "Returns the equivalent identifiers and semantic types for the curie(s)", "operationId": "get_normalized_node_handler_get_normalized_nodes_get", "parameters": [ @@ -144,6 +146,7 @@ }, "post": { "summary": "Get the equivalent identifiers and semantic types for the curie(s) entered.", + "tags": ["translator"], "description": "Returns the equivalent identifiers and semantic types for the curie(s). Use the `conflate` flag to choose whether to apply conflation.", "operationId": "get_normalized_node_handler_post_get_normalized_nodes_post", "requestBody": { @@ -181,6 +184,7 @@ "/get_setid": { "get": { "summary": "Normalize and deduplicate a set of identifiers and return a single hash that represents this set.", + "tags": ["translator"], "operationId": "get_setid_get_setid_get", "parameters": [ { @@ -250,6 +254,7 @@ }, "post": { "summary": "Normalize and deduplicate a set of identifiers and return a single hash that represents this set.", + "tags": ["translator"], "operationId": "get_setid_get_setid_post", "requestBody": { "content": { @@ -317,6 +322,7 @@ "/get_semantic_types": { "get": { "summary": "Return a list of BioLink semantic types for which normalization has been attempted.", + "tags": ["metadata"], "description": "Returns a distinct set of the semantic types discovered in the compendium data.", "operationId": "get_semantic_types_handler_get_semantic_types_get", "responses": { @@ -335,6 +341,7 @@ }, "/get_allowed_conflations": { "get": { + "tags": ["metadata"], "description": "Returns a list of allowed conflation options", "responses": { "200": { @@ -3178,10 +3185,10 @@ "name": "translator" }, { - "name": "Interfaces" + "name": "health" }, { - "name": "trapi" + "name": "metadata" } ], "servers": [ @@ -3193,7 +3200,7 @@ }, { "description": "Localhost", - "url": "https://localhost:8000/nodenorm", + "url": "http://localhost:8000/", "x-maturity": "development", "x-location": "RENCI" } diff --git a/tests/handlers/test_api_list.py b/tests/handlers/test_api_list.py deleted file mode 100644 index b7269a3..0000000 --- a/tests/handlers/test_api_list.py +++ /dev/null @@ -1,174 +0,0 @@ -""" -Tests for mocking the ApiList handling -""" - -import json -import logging - -import tornado -from tornado.testing import AsyncHTTPTestCase - -from web.handlers import EXTRA_HANDLERS -from web.application import PendingAPI -from web.settings.configuration import load_configuration - - -logger = logging.Logger(__name__) - - -class TestApiListHandler(AsyncHTTPTestCase): - - def get_app(self) -> tornado.web.Application: - configuration = load_configuration("config_web") - app_handlers = EXTRA_HANDLERS - app_settings = {"static_path": "static"} - application = PendingAPI.get_app(configuration, app_settings, app_handlers) - return application - - def test_get_method(self): - """ - Test the GET HTTP method handler for the front page - - Example Response Header Content - [ - ('Connection', 'close') - ('Content-Length', '39180'), - ('Content-Type', 'text/html; charset=UTF-8'), - ('Date', 'Tue, 12 Mar 2024 18:10:46 GMT'), - ('Etag', '"59777fcfe54fb940e50d1c534e02ed8a8c59d52e"'), - ('Server', 'TornadoServer/6.4'), - ] - Example Request Header Content - [ - ('Accept-Encoding', 'gzip') - ('Connection', 'close'), - ('Host', '127.0.0.1:41599'), - ('User-Agent', 'Tornado/6.4'), - ] - """ - api_list_endpoint = r"/api/list" - http_method = "GET" - - # This is likely going to change as the pending.api changes - expected_endpoints = [ - "agr", - "annotator_extra", - "biggim", - "biggim_drugresponse_kp", - "bindingdb", - "biomuta", - "bioplanet_pathway_disease", - "bioplanet_pathway_gene", - "ccle", - "cell_ontology", - "chebi", - "clinicaltrials", - "ddinter", - "denovodb", - "dgidb", - "disbiome", - "diseases", - "doid", - "ebigene2phenotype", - "fda_drugs", - "foodb", - "fooddata", - "geneset", - "gmmad2", - "go", - "go_bp", - "go_cc", - "go_mf", - "gtrx", - "gwascatalog", - "hmdb", - "hmdbv4", - "hpo", - "idisk", - "innatedb", - "kaviar", - "mabs", - "mgigene2phenotype", - "mondo", - "mrcoc", - "multiomics_clinicaltrials_kp", - "multiomics_drug_approvals_kp", - "multiomics_ehr_risk_kp", - "multiomics_wellness_kp", - "ncit", - "node-expansion", - "nodenorm", - "pfocr", - "phewas", - "pseudocap_go", - "pubtator3", - "rare_source", - "repodb", - "rhea", - "semmeddb", - "suppkg", - "tcga_mut_freq_kp", - "text_mining_targeted_association", - "tissues", - "ttd", - "uberon", - "umlschem", - "upheno_ontology", - ] - - response = self.fetch(api_list_endpoint, method=http_method) - - decoded_body = json.loads(response.body.decode("utf-8")) - self.assertEqual(response.code, 200) - - decoded_api_set = set(decoded_body) - expected_set = set(expected_endpoints) - potential_difference = list(decoded_api_set - expected_set) - - if len(potential_difference) > 10: - test_update_message = ( - "Please update this test with latest pending API's before re-running.\n" - f"Difference: {potential_difference}\n" - f"Updated Expected List: {json.dumps(decoded_body, indent=4)}" - ) - assert False, test_update_message - elif len(potential_difference) > 0: - test_update_message = ( - "Minor difference found in the API list\n" - f"Difference: {potential_difference}\n" - f"Updated Expected List: {json.dumps(decoded_body, indent=4)}" - ) - logger.warning(test_update_message) - - self.assertTrue(isinstance(decoded_body, list)) - self.assertTrue(len(decoded_body) > 0) - self.assertEqual(response.reason, "OK") - self.assertFalse(response._error_is_response_code) - - response_headers = response.headers - response_content_type = response_headers.get("Content-Type", None) - response_content_length = response_headers.get("Content-Length", "-10") - response_header_connection = response_headers.get("Connection", None) - - self.assertTrue(isinstance(response_headers, tornado.httputil.HTTPHeaders)) - self.assertEqual(response_content_type, "application/json; charset=UTF-8") - self.assertTrue(int(response_content_length) > 0) - self.assertEqual(response_header_connection, "close") - - get_request = response.request - self.assertEqual(get_request.method, http_method) - self.assertEqual(get_request.body, None) - - get_request_headers = get_request.headers - request_connection = get_request_headers.get("Connection", None) - response_user_agent = get_request_headers.get("User-Agent", None) - request_user_agent = get_request_headers.get("User-Agent", None) - - self.assertEqual(request_connection, "close") - self.assertTrue(response_user_agent) - self.assertTrue(request_user_agent) - self.assertEqual(response_user_agent, request_user_agent) - - response_time = response.start_time - request_time = get_request.start_time - self.assertTrue(response.request_time >= (response_time - request_time)) diff --git a/tests/handlers/test_frontpage.py b/tests/handlers/test_frontpage.py deleted file mode 100644 index 7896490..0000000 --- a/tests/handlers/test_frontpage.py +++ /dev/null @@ -1,122 +0,0 @@ -""" -Tests for mocking the FrontPageHandler front page rendering -""" - -import tornado -from tornado.testing import AsyncHTTPTestCase - -from web.handlers import EXTRA_HANDLERS -from web.application import PendingAPI -from web.settings.configuration import load_configuration - - -class TestFrontPageHandler(AsyncHTTPTestCase): - - def get_app(self) -> tornado.web.Application: - configuration = load_configuration("config_web") - app_handlers = EXTRA_HANDLERS - app_settings = {"static_path": "static"} - application = PendingAPI.get_app(configuration, app_settings, app_handlers) - return application - - def test_get_method(self): - """ - Test the GET HTTP method handler for the front page - - Example Response Header Content - [ - ('Connection', 'close') - ('Content-Length', '39180'), - ('Content-Type', 'text/html; charset=UTF-8'), - ('Date', 'Tue, 12 Mar 2024 18:10:46 GMT'), - ('Etag', '"59777fcfe54fb940e50d1c534e02ed8a8c59d52e"'), - ('Server', 'TornadoServer/6.4'), - ] - Example Request Header Content - [ - ('Accept-Encoding', 'gzip') - ('Connection', 'close'), - ('Host', '127.0.0.1:41599'), - ('User-Agent', 'Tornado/6.4'), - ] - """ - frontpage_endpoint = r"/" - http_method = "GET" - - response = self.fetch(frontpage_endpoint, method=http_method) - self.assertEqual(response.code, 200) - self.assertEqual(response._body, None) - self.assertEqual(response.reason, "OK") - self.assertFalse(response._error_is_response_code) - - response_headers = response.headers - response_content_type = response_headers.get("Content-Type", None) - response_content_length = response_headers.get("Content-Length", "-10") - response_header_connection = response_headers.get("Connection", None) - - self.assertTrue(isinstance(response_headers, tornado.httputil.HTTPHeaders)) - self.assertEqual(response_content_type, "text/html; charset=UTF-8") - self.assertTrue(int(response_content_length) > 0) - self.assertEqual(response_header_connection, "close") - - get_request = response.request - self.assertEqual(get_request.method, http_method) - self.assertEqual(get_request.body, None) - - get_request_headers = get_request.headers - request_connection = get_request_headers.get("Connection", None) - response_user_agent = get_request_headers.get("User-Agent", None) - request_user_agent = get_request_headers.get("User-Agent", None) - - self.assertEqual(request_connection, "close") - self.assertTrue(response_user_agent) - self.assertTrue(request_user_agent) - self.assertEqual(response_user_agent, request_user_agent) - - response_time = response.start_time - request_time = get_request.start_time - self.assertTrue(response.request_time >= (response_time - request_time)) - - def test_head_method(self): - """ - Test the HEAD HTTP method handler for the front page - - The main difference is because we don't return any content generated - from the front page template, the response content length shouldn't - have anything and should always be length 0 - """ - frontpage_endpoint = r"/" - http_method = "HEAD" - response = self.fetch(frontpage_endpoint, method=http_method) - self.assertEqual(response.code, 200) - self.assertEqual(response._body, None) - self.assertEqual(response.reason, "OK") - self.assertFalse(response._error_is_response_code) - - response_headers = response.headers - response_content_type = response_headers.get("Content-Type", None) - response_content_length = response_headers.get("Content-Length", "-10") - response_header_connection = response_headers.get("Connection", None) - - self.assertTrue(isinstance(response_headers, tornado.httputil.HTTPHeaders)) - self.assertEqual(response_content_type, "text/html; charset=UTF-8") - self.assertTrue(int(response_content_length) == 0) - self.assertEqual(response_header_connection, "close") - - head_request = response.request - self.assertEqual(head_request.method, http_method) - self.assertEqual(head_request.body, None) - - head_request_headers = head_request.headers - request_connection = head_request_headers.get("Connection", None) - response_user_agent = head_request_headers.get("User-Agent", None) - request_user_agent = head_request_headers.get("User-Agent", None) - - self.assertEqual(request_connection, "close") - self.assertTrue(response_user_agent) - self.assertTrue(request_user_agent) - self.assertEqual(response_user_agent, request_user_agent) - - response_time = response.start_time - request_time = head_request.start_time - self.assertTrue(response.request_time >= (response_time - request_time)) diff --git a/tests/handlers/test_metadata.py b/tests/handlers/test_metadata.py deleted file mode 100644 index 7f8df90..0000000 --- a/tests/handlers/test_metadata.py +++ /dev/null @@ -1,49 +0,0 @@ -""" -Tests for mocking the metadata handling -""" - -import json - -import tornado -from tornado.testing import AsyncHTTPTestCase -from tornado.httpclient import AsyncHTTPClient - -from web.handlers import EXTRA_HANDLERS -from web.application import PendingAPI -from web.settings.configuration import load_configuration - - -class TestMetadataHandler(AsyncHTTPTestCase): - - def get_app(self) -> tornado.web.Application: - configuration = load_configuration("config_web") - app_handlers = EXTRA_HANDLERS - app_settings = {"static_path": "static"} - application = PendingAPI.get_app(configuration, app_settings, app_handlers) - return application - - def test_plugin_metadata(self): - """ - Tests the plugin metadata responses - - Accumulates all of the API's found through the /api/list endpoint - and then test each one to ensure that the metadata structure is consistent - across all endpoints - - Also ensures that we correctly handle SMARTAPI identifier integration - for plugins that have and don't have it when reporting the metadata - """ - http_client = AsyncHTTPClient() - http_client.fetch(self.get_url("/api/list"), self.stop, method="GET") - api_list_response = self.wait() - - api_list = json.loads(api_list_response.body.decode("utf-8")) - self.assertEqual(api_list_response.code, 200) - - for plugin_api in api_list: - plugin_metadata_endpoint = f"/{plugin_api}/metadata" - http_client = AsyncHTTPClient() - http_client.fetch(self.get_url(plugin_metadata_endpoint), self.stop, method="GET") - metadata_response = self.wait() - breakpoint() - pass diff --git a/tests/handlers/nodenorm/test_normalized_nodes.py b/tests/test_normalized_nodes.py similarity index 94% rename from tests/handlers/nodenorm/test_normalized_nodes.py rename to tests/test_normalized_nodes.py index 52e071e..ba386da 100644 --- a/tests/handlers/nodenorm/test_normalized_nodes.py +++ b/tests/test_normalized_nodes.py @@ -8,20 +8,20 @@ from tornado.testing import AsyncHTTPTestCase, gen_test from tornado.httpclient import AsyncHTTPClient -from web.handlers import EXTRA_HANDLERS -from web.application import PendingAPI -from web.settings.configuration import load_configuration +from nodenorm.namespace import NodeNormalizationAPINamespace +from nodenorm.application import NodeNormalizationAPI class TestNodeNormalizationHandler(AsyncHTTPTestCase): def get_app(self) -> tornado.web.Application: - configuration = load_configuration("config_web/nodenorm.py") - configuration.ES_INDICES = {configuration.ES_DOC_TYPE: "nodenorm_20250507_4ibdxry7"} - configuration.ES_HOST = "http://su10:9200" - app_handlers = EXTRA_HANDLERS - app_settings = {"static_path": "static"} - application = PendingAPI.get_app(configuration, app_settings, app_handlers) + options = tornado.options.OptionParser() + configuration_namespace = NodeNormalizationAPINamespace(options) + + # test configuration + configuration_namespace.elasticsearch["ES_HOST"] = "http://su10:9200" + configuration_namespace.elasticsearch["ES_INDEX"] = "nodenorm_20250507_4ibdxry7" + application = NodeNormalizationAPI.get_app(configuration_namespace) return application @gen_test(timeout=0.50) diff --git a/tests/handlers/nodenorm/test_set_identifiers.py b/tests/test_set_identifiers.py similarity index 76% rename from tests/handlers/nodenorm/test_set_identifiers.py rename to tests/test_set_identifiers.py index 404f7e1..ea96679 100644 --- a/tests/handlers/nodenorm/test_set_identifiers.py +++ b/tests/test_set_identifiers.py @@ -4,26 +4,24 @@ import json -import pytest import tornado from tornado.testing import AsyncHTTPTestCase, gen_test from tornado.httpclient import AsyncHTTPClient -from tornado.ioloop import IOLoop -from web.handlers import EXTRA_HANDLERS -from web.application import PendingAPI -from web.settings.configuration import load_configuration +from nodenorm.namespace import NodeNormalizationAPINamespace +from nodenorm.application import NodeNormalizationAPI class TestSetIdentifierHandlerGet(AsyncHTTPTestCase): def get_app(self) -> tornado.web.Application: - configuration = load_configuration("config_web/nodenorm.py") - configuration.ES_INDICES = {configuration.ES_DOC_TYPE: "nodenorm_20250929_wop2zrjn"} - configuration.ES_HOST = "http://su10:9200" - app_handlers = EXTRA_HANDLERS - app_settings = {"static_path": "static"} - application = PendingAPI.get_app(configuration, app_settings, app_handlers) + options = tornado.options.OptionParser() + configuration_namespace = NodeNormalizationAPINamespace(options) + + # test configuration + configuration_namespace.elasticsearch["ES_HOST"] = "http://su10:9200" + configuration_namespace.elasticsearch["ES_INDEX"] = "nodenorm_20250507_4ibdxry7" + application = NodeNormalizationAPI.get_app(configuration_namespace) return application @gen_test(timeout=1.50) @@ -66,12 +64,13 @@ def test_get_endpoint(self): class TestSetIdentifierHandlerPost(AsyncHTTPTestCase): def get_app(self) -> tornado.web.Application: - configuration = load_configuration("config_web/nodenorm.py") - configuration.ES_INDICES = {configuration.ES_DOC_TYPE: "nodenorm_20250929_wop2zrjn"} - configuration.ES_HOST = "http://su10:9200" - app_handlers = EXTRA_HANDLERS - app_settings = {"static_path": "static"} - application = PendingAPI.get_app(configuration, app_settings, app_handlers) + options = tornado.options.OptionParser() + configuration_namespace = NodeNormalizationAPINamespace(options) + + # test configuration + configuration_namespace.elasticsearch["ES_HOST"] = "http://su10:9200" + configuration_namespace.elasticsearch["ES_INDEX"] = "nodenorm_20250507_4ibdxry7" + application = NodeNormalizationAPI.get_app(configuration_namespace) return application @gen_test(timeout=1.50)