Skip to content

Commit

Permalink
Moved some useful utilities out to top level utils
Browse files Browse the repository at this point in the history
- Moved get_providers and some associated utilities to new top-level
  optimade.utils module, pre-empting a fledgling client utils module
- Added a get_child_databases_link() function
- Made assigning an ObjectID to a provider optional
- Allow for missing uvicorn when using logger
  • Loading branch information
ml-evs committed Feb 28, 2022
1 parent d7d301c commit c87807e
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 94 deletions.
14 changes: 9 additions & 5 deletions optimade/server/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
from pathlib import Path
import sys

from uvicorn.logging import DefaultFormatter


# Instantiate LOGGER
LOGGER = logging.getLogger("optimade")
Expand All @@ -25,9 +23,15 @@
except ImportError:
CONSOLE_HANDLER.setLevel(os.getenv("OPTIMADE_LOG_LEVEL", "INFO").upper())

# Formatter
CONSOLE_FORMATTER = DefaultFormatter("%(levelprefix)s [%(name)s] %(message)s")
CONSOLE_HANDLER.setFormatter(CONSOLE_FORMATTER)
# Formatter; try to use uvicorn default, otherwise just use built-in default
try:
from uvicorn.logging import DefaultFormatter

CONSOLE_FORMATTER = DefaultFormatter("%(levelprefix)s [%(name)s] %(message)s")
CONSOLE_HANDLER.setFormatter(CONSOLE_FORMATTER)
except ImportError:
pass


# Add handler to LOGGER
LOGGER.addHandler(CONSOLE_HANDLER)
Expand Down
2 changes: 1 addition & 1 deletion optimade/server/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def load_entries(endpoint_name: str, endpoint_collection: EntryCollection):
LOGGER.debug(
"Adding Materials-Consortia providers to links from optimade.org"
)
providers = get_providers()
providers = get_providers(add_mongo_id=True)
for doc in providers:
endpoint_collection.collection.replace_one(
filter={"_id": ObjectId(doc["_id"]["$oid"])},
Expand Down
2 changes: 1 addition & 1 deletion optimade/server/main_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
LOGGER.debug(
"Adding Materials-Consortia providers to links from optimade.org..."
)
providers = get_providers()
providers = get_providers(add_mongo_id=True)
for doc in providers:
links_coll.collection.replace_one(
filter={"_id": ObjectId(doc["_id"]["$oid"])},
Expand Down
101 changes: 14 additions & 87 deletions optimade/server/routers/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,20 @@
from optimade.server.entry_collections import EntryCollection
from optimade.server.exceptions import BadRequest, InternalServerError
from optimade.server.query_params import EntryListingQueryParams, SingleEntryQueryParams
from optimade.utils import mongo_id_for_database, get_providers, PROVIDER_LIST_URLS

__all__ = (
"BASE_URL_PREFIXES",
"meta_values",
"handle_response_fields",
"get_included_relationships",
"get_base_url",
"get_entries",
"get_single_entry",
"mongo_id_for_database",
"get_providers",
"PROVIDER_LIST_URLS",
)

# we need to get rid of any release tags (e.g. -rc.2) and any build metadata (e.g. +py36)
# from the api_version before allowing the URL
Expand All @@ -30,11 +44,6 @@
"patch": f"/v{'.'.join(__api_version__.split('-')[0].split('+')[0].split('.')[:3])}",
}

PROVIDER_LIST_URLS = (
"https://providers.optimade.org/v1/links",
"https://raw.githubusercontent.com/Materials-Consortia/providers/master/src/links/v1/providers.json",
)


class JSONAPIResponse(JSONResponse):
"""This class simply patches `fastapi.responses.JSONResponse` to use the
Expand Down Expand Up @@ -310,85 +319,3 @@ def get_single_entry(
),
included=included,
)


def mongo_id_for_database(database_id: str, database_type: str) -> str:
"""Produce a MondoDB ObjectId for a database"""
from bson.objectid import ObjectId

oid = f"{database_id}{database_type}"
if len(oid) > 12:
oid = oid[:12]
elif len(oid) < 12:
oid = f"{oid}{'0' * (12 - len(oid))}"

return str(ObjectId(oid.encode("UTF-8")))


def get_providers() -> list:
"""Retrieve Materials-Consortia providers (from https://providers.optimade.org/v1/links).
Fallback order if providers.optimade.org is not available:
1. Try Materials-Consortia/providers on GitHub.
2. Try submodule `providers`' list of providers.
3. Log warning that providers list from Materials-Consortia is not included in the
`/links`-endpoint.
Returns:
List of raw JSON-decoded providers including MongoDB object IDs.
"""
import requests

try:
import simplejson as json
except ImportError:
import json

for provider_list_url in PROVIDER_LIST_URLS:
try:
providers = requests.get(provider_list_url).json()
except (
requests.exceptions.ConnectionError,
requests.exceptions.ConnectTimeout,
json.JSONDecodeError,
):
pass
else:
break
else:
try:
from optimade.server.data import providers
except ImportError:
from optimade.server.logger import LOGGER

LOGGER.warning(
"""Could not retrieve a list of providers!
Tried the following resources:
{}
The list of providers will not be included in the `/links`-endpoint.
""".format(
"".join([f" * {_}\n" for _ in PROVIDER_LIST_URLS])
)
)
return []

providers_list = []
for provider in providers.get("data", []):
# Remove/skip "exmpl"
if provider["id"] == "exmpl":
continue

provider.update(provider.pop("attributes", {}))

# Add MongoDB ObjectId
provider["_id"] = {
"$oid": mongo_id_for_database(provider["id"], provider["type"])
}

providers_list.append(provider)

return providers_list
135 changes: 135 additions & 0 deletions optimade/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
"""This submodule implements some useful utilities for dealing
with OPTIMADE providers that can be used in server or client code.
"""

from typing import List

from optimade.models.links import LinksResource

PROVIDER_LIST_URLS = (
"https://providers.optimade.org/v1/links",
"https://raw.githubusercontent.com/Materials-Consortia/providers/master/src/links/v1/providers.json",
)


def mongo_id_for_database(database_id: str, database_type: str) -> str:
"""Produce a MongoDB ObjectId for a database"""
from bson.objectid import ObjectId

oid = f"{database_id}{database_type}"
if len(oid) > 12:
oid = oid[:12]
elif len(oid) < 12:
oid = f"{oid}{'0' * (12 - len(oid))}"

return str(ObjectId(oid.encode("UTF-8")))


def get_providers(add_mongo_id: bool = False) -> list:
"""Retrieve Materials-Consortia providers (from https://providers.optimade.org/v1/links).
Fallback order if providers.optimade.org is not available:
1. Try Materials-Consortia/providers on GitHub.
2. Try submodule `providers`' list of providers.
3. Log warning that providers list from Materials-Consortia is not included in the
`/links`-endpoint.
Arguments:
Whether to populate the `_id` field of the provider with MongoDB ObjectID.
Returns:
List of raw JSON-decoded providers including MongoDB object IDs.
"""
import requests

try:
import simplejson as json
except ImportError:
import json

for provider_list_url in PROVIDER_LIST_URLS:
try:
providers = requests.get(provider_list_url).json()
except (
requests.exceptions.ConnectionError,
requests.exceptions.ConnectTimeout,
json.JSONDecodeError,
):
pass
else:
break
else:
try:
from optimade.server.data import providers
except ImportError:
from optimade.server.logger import LOGGER

LOGGER.warning(
"""Could not retrieve a list of providers!
Tried the following resources:
{}
The list of providers will not be included in the `/links`-endpoint.
""".format(
"".join([f" * {_}\n" for _ in PROVIDER_LIST_URLS])
)
)
return []

providers_list = []
for provider in providers.get("data", []):
# Remove/skip "exmpl"
if provider["id"] == "exmpl":
continue

provider.update(provider.pop("attributes", {}))

# Add MongoDB ObjectId
if add_mongo_id:
provider["_id"] = {
"$oid": mongo_id_for_database(provider["id"], provider["type"])
}

providers_list.append(provider)

return providers_list


def get_child_database_links(provider: LinksResource) -> List[LinksResource]:
"""For a provider, return a list of available child databases.
Arguments:
provider: The links entry for the provider.
Returns:
A list of the valid links entries from this provider that
have `link_type` `"child"`.
"""
import requests
from optimade.models.responses import LinksResponse
from optimade.models.links import LinkType

base_url = provider.pop("base_url")
if base_url is None:
raise RuntimeError(f"Provider {provider['id']} provides no base URL.")

links_endp = base_url + "/v1/links"
links = requests.get(links_endp)

if links.status_code != 200:
raise RuntimeError(
f"Invalid response from {links_endp} for provider {provider['id']}: {links.content}."
)

links = LinksResponse(**links.json())
return [
link
for link in links.data
if link.attributes.link_type == LinkType.child
and link.attributes.base_url is not None
]

0 comments on commit c87807e

Please sign in to comment.