Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
6 changes: 2 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,7 @@ jobs:
sleep 15

- name: Test server, including OPTIONAL base URLs
# Pin validator action until merge of Materials-Consortia/optimade-validator-action#175
uses: Materials-Consortia/optimade-validator-action@dc4a7b6a83da42e5341a1a401e36d83375550fb7
uses: Materials-Consortia/optimade-validator-action@v2
with:
port: 3213
path: /
Expand All @@ -109,8 +108,7 @@ jobs:
sleep 15

- name: Test index server, including OPTIONAL base URLs
# Pin validator action until merge of Materials-Consortia/optimade-validator-action#175
uses: Materials-Consortia/optimade-validator-action@dc4a7b6a83da42e5341a1a401e36d83375550fb7
uses: Materials-Consortia/optimade-validator-action@v2
with:
port: 3214
path: /
Expand Down
3 changes: 3 additions & 0 deletions docs/api_reference/server/create_app.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# create_app

::: optimade.server.create_app
1 change: 1 addition & 0 deletions docs/deployment/.pages
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
title: "Deployment"
nav:
- integrated.md
- multiple_apps.md
- container.md
27 changes: 9 additions & 18 deletions docs/deployment/integrated.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
The `optimade` package can be used to create a standalone web application that serves the OPTIMADE API based on a pre-configured MongoDB backend.
In this document, we are going to use `optimade` differently and use it to add an OPTIMADE API implementation alongside an existing API that employs an Elasticsearch storage layer.

Let's assume we already have a *FastAPI* application that runs an unrelated web service, and that we use an Elasticsearch backend that contains all structure data, but not necessarily in a form that OPTIMADE expects.
Let's assume we already have a _FastAPI_ application that runs an unrelated web service, and that we use an Elasticsearch backend that contains all structure data, but not necessarily in a form that OPTIMADE expects.

## Providing the `optimade` configuration

Expand All @@ -13,7 +13,7 @@ If you run `optimade` code inside another application, you might want to provide
Let's say you have a file `optimade_config.json` as part of the Python module that you use to create your OPTIMADE API.

!!! tip
You can find more detailed information about configuring the `optimade` server in the [Configuration](../configuration.md) section.
You can find more detailed information about configuring the `optimade` server in the [Configuration](../configuration.md) section.

Before importing any `optimade` modules, you can set the `OPTIMADE_CONFIG_FILE` environment variable to refer to your config file:

Expand All @@ -37,30 +37,21 @@ structures.structures_coll = MyElasticsearchStructureCollection()

You can imagine that `MyElasticsearchStructureCollection` either sub-classes the default `optimade` Elasticsearch implementation ([`ElasticsearchCollection`][optimade.server.entry_collections.elasticsearch.ElasticCollection]) or sub-classes [`EntryCollection`][optimade.server.entry_collections.entry_collections.EntryCollection], depending on how deeply you need to customize the default `optimade` behavior.

## Mounting the OPTIMADE Python tools *FastAPI* app into an existing *FastAPI* app
## Mounting the OPTIMADE Python tools _FastAPI_ app into an existing _FastAPI_ app

Let's assume you have an existing *FastAPI* app `my_app`.
Let's assume you have an existing _FastAPI_ app `my_app`.
It already implements a few routers under certain path prefixes, and now you want to add an OPTIMADE implementation under the path prefix `/optimade`.

First, you have to set the `root_path` in the `optimade` configuration, so that the app expects all requests to be prefixed with `/optimade`.
The primary thing to modify is the `base_url` to match the new subpath. The easiest is to just update your configuration file or env parameters.

Second, you simply mount the `optimade` app into your existing app `my_app`:
Then one can just simply do the following:

```python
from optimade.server.config import CONFIG
from optimade.server.main import main as optimade

CONFIG.root_path = "/optimade"

from optimade.server import main as optimade

optimade.add_major_version_base_url(optimade.app)
my_app.mount("/optimade", optimade.app)
```

!!! tip
In the example above, we imported `CONFIG` before `main` so that our config was loaded before app creation.
To avoid the need for this, the `root_path` can be set in your JSON config file, passed as an environment variable, or declared in a custom Python module (see [Configuration](../configuration.md)).

See also the *FastAPI* documentation on [sub-applications](https://fastapi.tiangolo.com/advanced/sub-applications/).
See also the _FastAPI_ documentation on [sub-applications](https://fastapi.tiangolo.com/advanced/sub-applications/).

Now, if you run `my_app`, it will still serve all its routers as before and in addition it will also serve all OPTIMADE routes under `/optimade/` and the versioned URLs `/optimade/v1/`.
Now, if you run `my_app`, it will still serve all its routers as before and in addition it will also serve all OPTIMADE routes under `/optimade/`.
38 changes: 38 additions & 0 deletions docs/deployment/multiple_apps.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Serve multiple OPTIMADE APIs within a single python process

One can start multiple OPTIMADE API apps within a single FastAPI instance and mount them at different subpaths.

This is enabled by the `create_app` method that allows to override parts of the configuration for each specific app, and set up separate loggers.

Here's a simple example that sets up two OPTIMADE APIs and an Index Meta-DB respectively at subpaths `/app1`, `/app2` and `/idx`.

```python
from fastapi import FastAPI

from optimade.server.config import ServerConfig
from optimade.server.create_app import create_app

parent_app = FastAPI()

base_url = "http://127.0.0.1:8000"

conf1 = ServerConfig()
conf1.base_url = f"{base_url}/app1"
conf1.mongo_database = "optimade_1"
app1 = create_app(conf1, logger_tag="app1")
parent_app.mount("/app1", app1)

conf2 = ServerConfig()
conf2.base_url = f"{base_url}/app2"
conf2.mongo_database = "optimade_2"
app2 = create_app(conf2, logger_tag="app2")
parent_app.mount("/app2", app2)

conf3 = ServerConfig()
conf3.base_url = f"{base_url}/idx"
conf3.mongo_database = "optimade_idx"
app3 = create_app(conf3, index=True, logger_tag="idx")
parent_app.mount("/idx", app3)
```

Note that `ServerConfig()` returns the configuration based on the usual sources - env variables or json file (see [Configuration](../configuration.md) section).
8 changes: 2 additions & 6 deletions docs/getting_started/use_cases.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,12 @@ The [Materials Project](https://materialsproject.org) uses `optimade-python-tool

`optimade-python-tools` handles filter parsing, database query generation and response validation by running the reference server implementation with minimal configuration.

[*odbx*](https://odbx.science), a small database of results from crystal structure prediction calculations, follows a similar approach.
[_odbx_](https://odbx.science), a small database of results from crystal structure prediction calculations, follows a similar approach.
This implementation is open source, available on GitHub at [ml-evs/odbx.science](https://github.com/ml-evs/odbx.science).

## Serving multiple databases

[Materials Cloud](https://materialscloud.org) uses `optimade-python-tools` as a library to provide an OPTIMADE API entry to archived computational materials studies, created with the [AiiDA](https://aiida.net) Python framework and published through their archive.
In this case, each individual study and archive entry has its own database and separate API entry.
The Python classes within the `optimade` package have been extended to make use of AiiDA and its underlying [PostgreSQL](https://postgresql.org) storage engine.

Details of this implementation can be found on GitHub at [aiidateam/aiida-optimade](https://github.com/aiidateam/aiida-optimade).
[Materials Cloud](https://materialscloud.org) uses `optimade-python-tools` as a library to provide an OPTIMADE API entries to 1) their main databases create with the [AiiDA](https://aiida.net) Python framework; and 2) to user-contributed data via the Archive platform. Separate OPTIMADE API apps are started for each database, mounted as separate endpoints to a parent FastAPI instance. For converting the underying data to the OPTIMADE format, the [optimade-maker](https://github.com/materialscloud-org/optimade-maker) toolkit is used.

## Extending an existing API

Expand Down
19 changes: 10 additions & 9 deletions optimade/client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -511,11 +511,12 @@ def _binary_search_count_async(
)
)

# if we got any data, we are below the target value
below = bool(result[base_url].data)

self._progress.disable = self.silent

window, probe = self._update_probe_and_window(
window, probe, bool(result[base_url].data)
)
window, probe = self._update_probe_and_window(window, probe, below)

if window[0] == window[1] and window[0] == probe:
return probe
Expand Down Expand Up @@ -557,16 +558,15 @@ def _update_probe_and_window(
raise RuntimeError(
"Invalid arguments: must provide all or none of window, last_probe and below parameters"
)

probe: int = last_probe

# Exit condition: find a range of (count, count+1) values
# and determine whether the probe was above or below in the last guess
if window[1] is not None and window[1] - window[0] == 1:
if below:
return (window[0], window[0]), window[0]
else:
return (window[1], window[1]), window[1]
else:
return (window[0], window[0]), window[0]

# Enclose the real value in the window, with `None` indicating an open boundary
if below:
Expand All @@ -578,12 +578,13 @@ def _update_probe_and_window(
if window[1] is None:
probe *= 10

# Otherwise, if we're in the window and the ends of the window now have the same power of 10, take the average (102 => 108) => 105
elif round(math.log10(window[0])) == round(math.log10(window[0])):
# Otherwise, if we're in the window and the ends of the window now have the same power of 10 (or within +-1),
# take the average (102 => 108) => 105
elif abs(math.log10(window[1]) - math.log10(window[0])) <= 1:
probe = (window[1] + window[0]) // 2
# otherwise use logarithmic average (10, 1000) => 100
else:
probe = int(10 ** (math.log10(window[1]) + math.log10(window[0]) / 2))
probe = int(10 ** ((math.log10(window[1]) + math.log10(window[0])) / 2))

return window, probe

Expand Down
4 changes: 2 additions & 2 deletions optimade/filtertransformers/base_transformer.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ class BaseTransformer(Transformer, abc.ABC):

"""

mapper: type[BaseResourceMapper] | None = None
mapper: BaseResourceMapper | None = None
operator_map: dict[str, str | None] = {
"<": None,
"<=": None,
Expand All @@ -106,7 +106,7 @@ class BaseTransformer(Transformer, abc.ABC):
_quantity_type: type[Quantity] = Quantity
_quantities = None

def __init__(self, mapper: type[BaseResourceMapper] | None = None):
def __init__(self, mapper: BaseResourceMapper | None = None):
"""Initialise the transformer object, optionally loading in a
resource mapper for use when post-processing.

Expand Down
2 changes: 1 addition & 1 deletion optimade/filtertransformers/elasticsearch.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ class ElasticTransformer(BaseTransformer):

def __init__(
self,
mapper: type[BaseResourceMapper],
mapper: BaseResourceMapper,
quantities: dict[str, Quantity] | None = None,
):
if quantities is not None:
Expand Down
37 changes: 3 additions & 34 deletions optimade/server/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ class ServerConfig(BaseSettings):
extra="allow",
env_file_encoding="utf-8",
case_sensitive=False,
validate_assignment=True,
)

debug: Annotated[
Expand Down Expand Up @@ -370,12 +371,13 @@ class ServerConfig(BaseSettings):
list[str | dict[Literal["name", "type", "unit", "description"], str]],
],
Field(
default_factory=dict,
description=(
"A list of additional fields to be served with the provider's prefix "
"attached, broken down by endpoint."
),
),
] = {}
]
aliases: Annotated[
dict[Literal["links", "references", "structures"], dict[str, str]],
Field(
Expand Down Expand Up @@ -540,32 +542,6 @@ def check_license_info(cls, value: Any) -> AnyHttpUrl | None:

return value

@model_validator(mode="after")
def use_real_mongo_override(self) -> "ServerConfig":
"""Overrides the `database_backend` setting with MongoDB and
raises a deprecation warning.
"""
use_real_mongo = self.use_real_mongo

# Remove from model
del self.use_real_mongo

# Remove from set of user-defined fields
if "use_real_mongo" in self.model_fields_set:
self.model_fields_set.remove("use_real_mongo")

if use_real_mongo is not None:
warnings.warn(
"'use_real_mongo' is deprecated, please set the appropriate 'database_backend' "
"instead.",
DeprecationWarning,
)

if use_real_mongo:
self.database_backend = SupportedBackend.MONGODB

return self

@model_validator(mode="after")
def align_mongo_uri_and_mongo_database(self) -> "ServerConfig":
"""Prefer the value of database name if set from `mongo_uri` rather than
Expand Down Expand Up @@ -621,10 +597,3 @@ def settings_customise_sources(
ConfigFileSettingsSource(settings_cls),
file_secret_settings,
)


CONFIG: ServerConfig = ServerConfig()
"""This singleton loads the config from a hierarchy of sources (see
[`customise_sources`][optimade.server.config.ServerConfig.settings_customise_sources])
and makes it importable in the server code.
"""
Loading
Loading