Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ classifiers = [
]
dependencies = [
"biothings[web_extra]==1.0.2",
"pycurl",
"GitPython>=3.1.43",
"bmt>=1.4.5",
]
Expand Down
29 changes: 9 additions & 20 deletions src/nodenorm/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand All @@ -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():
"""
Expand All @@ -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__":
Expand Down
142 changes: 9 additions & 133 deletions src/nodenorm/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
<http://www.tornadoweb.org/en/stable/web.html#application-configuration>`_.

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]
File renamed without changes.
27 changes: 23 additions & 4 deletions src/nodenorm/config/config.default.json
Original file line number Diff line number Diff line change
@@ -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
}
Expand Down
34 changes: 26 additions & 8 deletions src/nodenorm/handlers/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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."""

Expand All @@ -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
4 changes: 2 additions & 2 deletions src/nodenorm/handlers/conflations.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down
14 changes: 7 additions & 7 deletions src/nodenorm/handlers/curie_prefix.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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(
Expand All @@ -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(
Expand Down
Loading