diff --git a/.github/fly.toml b/.github/fly.toml
index 9730bf4fd..9a8264550 100644
--- a/.github/fly.toml
+++ b/.github/fly.toml
@@ -1,11 +1,17 @@
-# fly.toml file generated for broken-field-1976 on 2022-11-21T21:37:55Z
-
app = "optimade"
kill_signal = "SIGINT"
kill_timeout = 5
+swap_size_mb = 1024
processes = []
+[build]
+ builder = "paketobuildpacks/builder:base"
+
[env]
+PORT = 5000
+BPE_OPTIMADE_CONFIG_FILE = "./tests/test_config.json"
+BPL_OPTIMADE_CONFIG_FILE = "./tests/test_config.json"
+OPTIMADE_CONFIG_FILE = "./tests/test_config.json"
[experimental]
allowed_public_ports = []
@@ -36,9 +42,3 @@ processes = []
interval = "15s"
restart_limit = 0
timeout = "2s"
-
-[build]
- builtin = "python"
-
-[build.settings]
- pythonbase = "3.10-slim-buster"
diff --git a/.github/workflows/cd_release.yml b/.github/workflows/cd_release.yml
index 75be344da..e07aec2ae 100644
--- a/.github/workflows/cd_release.yml
+++ b/.github/workflows/cd_release.yml
@@ -6,9 +6,9 @@ on:
- published
env:
- PUBLISH_UPDATE_BRANCH: master
GIT_USER_NAME: OPTIMADE Developers
GIT_USER_EMAIL: "dev@optimade.org"
+ DEFAULT_RELEASE_BRANCH: "master"
jobs:
@@ -16,16 +16,34 @@ jobs:
name: Publish OPTIMADE Python tools
runs-on: ubuntu-latest
if: github.repository == 'Materials-Consortia/optimade-python-tools' && startsWith(github.ref, 'refs/tags/v')
+ outputs:
+ publish_branch: ${{ steps.save_branch.outputs.publish_branch }}
steps:
- - name: Checkout repository
+ - name: Get triggering branch
+ uses: octokit/request-action@v2.x
+ id: get_release_branch
+ with:
+ route: GET /repos/{owner}/{repo}/releases/tags/${{ github.ref_name }}
+ owner: Materials-Consortia
+ repo: optimade-python-tools
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Save branch name
+ id: save_branch
+ run: |
+ echo 'publish_branch=${{ fromJson(steps.get_release_branch.outputs.data).target_commitish }}' >> $GITHUB_OUTPUT
+
+ - name: Checkout publish branch
uses: actions/checkout@v4
with:
submodules: true
fetch-depth: 0
+ branch: ${{ steps.save_branch.outputs.publish_branch }}
- name: Set up Python 3.10
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: '3.10'
@@ -36,21 +54,26 @@ jobs:
pip install -r requirements.txt -r requirements-dev.txt -r requirements-client.txt -r requirements-http-client.txt -r requirements-docs.txt
pip install -e .[all]
+ - name: Get base branch name for tag
+ id: branch_finder
+ run:
+ echo "branch=$(git branch --show-current)" >> "$GITHUB_OUTPUT"
+
- name: Update changelog
uses: CharMixer/auto-changelog-action@v1
with:
token: ${{ secrets.RELEASE_PAT_BOT }}
- release_branch: ${{ env.PUBLISH_UPDATE_BRANCH }}
+ release_branch: ${{ steps.save_branch.outputs.publish_branch }}
exclude_labels: "duplicate,question,invalid,wontfix,dependency_updates,skip_changelog"
- name: Update API Reference docs and version - Commit changes and update tag
run: .github/utils/update_docs.sh
- - name: Update '${{ env.PUBLISH_UPDATE_BRANCH }}'
+ - name: Update triggering branch
uses: CasperWA/push-protected@v2
with:
token: ${{ secrets.RELEASE_PAT_BOT }}
- branch: ${{ env.PUBLISH_UPDATE_BRANCH }}
+ branch: ${{ steps.save_branch.outputs.publish_branch }}
unprotect_reviews: true
sleep: 15
force: true
@@ -63,7 +86,7 @@ jobs:
uses: CharMixer/auto-changelog-action@v1
with:
token: ${{ secrets.RELEASE_PAT_BOT }}
- release_branch: ${{ env.PUBLISH_UPDATE_BRANCH }}
+ release_branch: ${{ steps.save_branch.outputs.publish_branch }}
since_tag: "${{ env.PREVIOUS_VERSION }}"
output: "release_changelog.md"
exclude_labels: "duplicate,question,invalid,wontfix,dependency_updates,skip_changelog"
@@ -96,10 +119,10 @@ jobs:
with:
submodules: true
fetch-depth: 0
- ref: ${{ env.PUBLISH_UPDATE_BRANCH }}
+ ref: ${{ needs.publish.outputs.publish_branch }}
- name: Set up Python 3.10
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: '3.10'
@@ -115,10 +138,15 @@ jobs:
git config --global user.name "${{ env.GIT_USER_NAME }}"
git config --global user.email "${{ env.GIT_USER_EMAIL }}"
- - name: Deploy documentation
+ - name: Deploy versioned documentation
+ run: |
+ mike deploy --push --remote origin --branch gh-pages --config-file mkdocs.yml ${GITHUB_REF#refs/tags/v}
+
+ - name: Deploy stable/latest documentation
+ if: ${{ needs.publish.outputs.publish_branch }} == ${{ env.DEFAULT_RELEASE_BRANCH }}
run: |
mike deploy --push --remote origin --branch gh-pages --update-aliases --config-file mkdocs.yml ${GITHUB_REF#refs/tags/v} stable
- mike deploy --push --remote origin --branch gh-pages --update-aliases --config-file mkdocs.yml latest master
+ mike deploy --push --remote origin --branch gh-pages --update-aliases --config-file mkdocs.yml latest ${{ env.DEFAULT_RELEASE_BRANCH }}
publish_container_image:
name: Publish container image
@@ -126,7 +154,7 @@ jobs:
uses: ./.github/workflows/cd_container_image.yml
with:
release: true
- checkout_ref: master
+ checkout_ref: ${{ needs.publish.outputs.publish_branch }}
secrets: inherit
permissions:
packages: write
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7c62a743b..2d63b11bf 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -9,6 +9,7 @@ on:
env:
PYTEST_ADDOPTS: "--color=yes"
+ LINTING_PY_VERSION: "3.9" # The version of Python to use for linting (typically the minimum supported)
# Cancel running workflows when additional changes are pushed
# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#example-using-a-fallback-value
@@ -26,10 +27,10 @@ jobs:
with:
submodules: true
- - name: Set up Python 3.10
- uses: actions/setup-python@v4
+ - name: Set up Python ${{ env.LINTING_PY_VERSION }}
+ uses: actions/setup-python@v5
with:
- python-version: '3.10'
+ python-version: ${{ env.LINTING_PY_VERSION }}
cache: 'pip'
cache-dependency-path: |
requirements*.txt
@@ -56,10 +57,10 @@ jobs:
with:
submodules: true
- - name: Set up Python 3.10
- uses: actions/setup-python@v4
+ - name: Set up Python ${{ env.LINTING_PY_VERSION }}
+ uses: actions/setup-python@v5
with:
- python-version: '3.10'
+ python-version: ${{ env.LINTING_PY_VERSION }}
cache: 'pip'
cache-dependency-path: |
requirements*.txt
@@ -131,7 +132,7 @@ jobs:
fail-fast: false
max-parallel: 4
matrix:
- python-version: ['3.9', '3.10', '3.11']
+ python-version: ['3.9', '3.10', '3.11', '3.12']
services:
mongo:
@@ -167,7 +168,7 @@ jobs:
fetch-depth: 2
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
@@ -184,25 +185,25 @@ jobs:
pip install -r requirements-http-client.txt
- name: Run non-server tests
- run: pytest -rs -vvv --cov=./optimade/ --cov-report=xml tests/ --ignore tests/server
+ run: pytest -rs -vvv --durations=0 --cov=./optimade/ --cov-report=xml tests/ --ignore tests/server
- name: Install latest server dependencies
run: pip install -r requirements-server.txt
- name: Run server tests (using `mongomock`)
- run: pytest -rs -vvv --cov=./optimade/ --cov-report=xml --cov-append tests/server tests/filtertransformers
+ run: pytest -rs -vvv --durations=0 --cov=./optimade/ --cov-report=xml --cov-append tests/server tests/filtertransformers
env:
OPTIMADE_DATABASE_BACKEND: 'mongomock'
- name: Run server tests with no API validation (using `mongomock`)
run:
- pytest -rs -vvv --cov=./optimade/ --cov-report=xml --cov-append tests/server tests/filtertransformers
+ pytest -rs -vvv --durations=0 --cov=./optimade/ --cov-report=xml --cov-append tests/server tests/filtertransformers
env:
OPTIMADE_DATABASE_BACKEND: 'mongomock'
OPTIMADE_VALIDATE_API_RESPONSE: false
- name: Run server tests (using a real MongoDB)
- run: pytest -rs -vvv --cov=./optimade/ --cov-report=xml --cov-append tests/server tests/filtertransformers
+ run: pytest -rs -vvv --durations=0 --cov=./optimade/ --cov-report=xml --cov-append tests/server tests/filtertransformers
env:
OPTIMADE_DATABASE_BACKEND: 'mongodb'
@@ -227,36 +228,38 @@ jobs:
run: pytest -rs -vvv --cov=./optimade/ --cov-report=xml --cov-append tests/adapters/
- name: Run tests for validator only to assess coverage (mongomock)
- if: matrix.python-version == 3.10
+ if: matrix.python-version == '3.11'
run: pytest -rs --cov=./optimade/ --cov-report=xml:validator_cov.xml --cov-append tests/server/test_server_validation.py
env:
OPTIMADE_DATABASE_BACKEND: 'mongomock'
- name: Run tests for validator only to assess coverage (Elasticsearch)
- if: matrix.python-version == 3.10
+ if: matrix.python-version == '3.11'
run: pytest -rs --cov=./optimade/ --cov-report=xml:validator_cov.xml --cov-append tests/server/test_server_validation.py
env:
OPTIMADE_DATABASE_BACKEND: 'elastic'
OPTIMADE_INSERT_TEST_DATA: false # Must be specified as previous steps will have already inserted the test data
- name: Run tests for validator only to assess coverage (MongoDB)
- if: matrix.python-version == 3.10
+ if: matrix.python-version == '3.11'
run: pytest -rs --cov=./optimade/ --cov-report=xml:validator_cov.xml --cov-append tests/server/test_server_validation.py
env:
OPTIMADE_DATABASE_BACKEND: 'mongodb'
OPTIMADE_INSERT_TEST_DATA: false # Must be specified as previous steps will have already inserted the test data
- name: Run the OPTIMADE Client CLI
- if: matrix.python-version == 3.10
+ if: matrix.python-version == '3.11'
run: |
coverage run --append --source optimade optimade/client/cli.py \
--filter 'nsites = 1' \
+ --http-timeout 1.0 \
--output-file test_get_async.json \
https://optimade.fly.dev
test test_get_async.json
coverage run --append --source optimade optimade/client/cli.py \
--filter 'nsites = 1' \
+ --http-timeout 1.0 \
--count \
--output-file test_count.json \
https://optimade.fly.dev
@@ -265,6 +268,7 @@ jobs:
coverage run --append --source optimade optimade/client/cli.py \
--no-async \
--filter 'nsites = 1' \
+ --http-timeout 1.0 \
--count \
--output-file test_count_no_async.json \
https://optimade.fly.dev
@@ -275,17 +279,19 @@ jobs:
coverage xml
- name: Upload coverage to Codecov
- if: matrix.python-version == '3.10' && github.repository == 'Materials-Consortia/optimade-python-tools'
- uses: codecov/codecov-action@v3
+ if: matrix.python-version == '3.11' && github.repository == 'Materials-Consortia/optimade-python-tools'
+ uses: codecov/codecov-action@v4
with:
+ token: ${{ secrets.CODECOV_TOKEN }}
name: project
file: ./coverage.xml
flags: project
- name: Upload validator coverage to Codecov
- if: matrix.python-version == '3.10' && github.repository == 'Materials-Consortia/optimade-python-tools'
- uses: codecov/codecov-action@v3
+ if: matrix.python-version == '3.11' && github.repository == 'Materials-Consortia/optimade-python-tools'
+ uses: codecov/codecov-action@v4
with:
+ token: ${{ secrets.CODECOV_TOKEN }}
name: validator
file: ./validator_cov.xml
flags: validator
@@ -298,9 +304,9 @@ jobs:
with:
submodules: true
- - uses: actions/setup-python@v4
+ - uses: actions/setup-python@v5
with:
- python-version: '3.10'
+ python-version: ${{ env.LINTING_PY_VERSION }}
cache: 'pip'
cache-dependency-path: |
requirements*.txt
@@ -330,10 +336,10 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- - name: Set up Python 3.10
- uses: actions/setup-python@v4
+ - name: Set up Python ${{ env.LINTING_PY_VERSION }}
+ uses: actions/setup-python@v5
with:
- python-version: '3.10'
+ python-version: ${{ env.LINTING_PY_VERSION }}
cache: 'pip'
cache-dependency-path: |
requirements*.txt
diff --git a/.github/workflows/ci_cd_updated_master.yml b/.github/workflows/ci_cd_updated_master.yml
index 5b8c6371e..13e0a254b 100644
--- a/.github/workflows/ci_cd_updated_master.yml
+++ b/.github/workflows/ci_cd_updated_master.yml
@@ -43,11 +43,11 @@ jobs:
fetch-depth: 0
submodules: true
- - name: Set up Python 3.10
+ - name: Set up Python 3.9
if: env.RELEASE_RUN == 'false'
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
- python-version: '3.10'
+ python-version: '3.9'
- name: Install dependencies
if: env.RELEASE_RUN == 'false'
@@ -138,8 +138,6 @@ jobs:
deploy-fly:
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
- OPTIMADE_CONFIG_FILE: /app/tests/test_config.json
- OPTIMADE_BASE_URL: https://optimade.fly.dev
runs-on: ubuntu-latest
if: github.repository_owner == 'Materials-Consortia'
steps:
@@ -152,6 +150,10 @@ jobs:
- name: Set up Fly
uses: superfly/flyctl-actions/setup-flyctl@master
+ - name: Combine requirements for builder
+ run: |
+ cat requirements-server.txt >> requirements.txt
+
- name: Deploy to Fly
run: flyctl deploy --remote-only -c ./.github/fly.toml
diff --git a/.github/workflows/ci_dependabot.yml b/.github/workflows/ci_dependabot.yml
index 17dd43dee..8683528b9 100644
--- a/.github/workflows/ci_dependabot.yml
+++ b/.github/workflows/ci_dependabot.yml
@@ -25,7 +25,7 @@ jobs:
ref: ${{ env.DEFAULT_REPO_BRANCH }}
- name: Set up Python 3.10
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: '3.10'
@@ -73,13 +73,13 @@ jobs:
- name: Fetch PR body
id: pr_body
- uses: chuhlomin/render-template@v1.7
+ uses: chuhlomin/render-template@v1.10
with:
template: .github/utils/single_dependency_pr_body.txt
- name: Create PR
id: cpr
- uses: peter-evans/create-pull-request@v5
+ uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.RELEASE_PAT_BOT }}
commit-message: New @dependabot-fueled updates
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 7e09132cd..3086bb33a 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,15 +1,10 @@
default_language_version:
- python: python3.10
+ python: python3.9
repos:
- - repo: https://github.com/ambv/black
- rev: 23.9.1
- hooks:
- - id: black
- name: Blacken
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.4.0
+ rev: v4.6.0
hooks:
- id: check-symlinks
- id: check-yaml
@@ -23,11 +18,19 @@ repos:
- id: trailing-whitespace
args: [--markdown-linebreak-ext=md]
+ - repo: https://github.com/asottile/pyupgrade
+ rev: v3.15.2
+ hooks:
+ - id: pyupgrade
+ args: ["--py39-plus"]
+
- repo: https://github.com/charliermarsh/ruff-pre-commit
- rev: 'v0.0.291'
+ rev: 'v0.4.7'
+
hooks:
- id: ruff
- args: [--fix]
+ args: [--fix, --exit-non-zero-on-fix]
+ - id: ruff-format
- repo: local
hooks:
@@ -46,8 +49,10 @@ repos:
description: Update the API Reference documentation whenever a Python file is touched in the code base.
- repo: https://github.com/pre-commit/mirrors-mypy
- rev: v1.5.1
+ rev: v1.10.0
hooks:
- id: mypy
name: "MyPy"
- additional_dependencies: ["types-all", "pydantic~=1.10"]
+ additional_dependencies: ["types-all", "pydantic~=2.0"]
+ exclude: ^tests/.*$
+ args: [--check-untyped-defs]
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b8a54481f..85c88798c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,131 @@
# Changelog
+## [v1.0.5](https://github.com/Materials-Consortia/optimade-python-tools/tree/v1.0.5) (2024-06-11)
+
+[Full Changelog](https://github.com/Materials-Consortia/optimade-python-tools/compare/v1.0.4...v1.0.5)
+
+**Closed issues:**
+
+- Fly has marked us as "high risk" and needs a credit card to unlock [\#2027](https://github.com/Materials-Consortia/optimade-python-tools/issues/2027)
+
+**Merged pull requests:**
+
+- Add `skip_ssl` flag/option to client [\#2086](https://github.com/Materials-Consortia/optimade-python-tools/pull/2086) ([ml-evs](https://github.com/ml-evs))
+
+## [v1.0.4](https://github.com/Materials-Consortia/optimade-python-tools/tree/v1.0.4) (2024-03-29)
+
+[Full Changelog](https://github.com/Materials-Consortia/optimade-python-tools/compare/v1.0.3...v1.0.4)
+
+**Closed issues:**
+
+- Asking a syntax of paging [\#2020](https://github.com/Materials-Consortia/optimade-python-tools/issues/2020)
+
+**Merged pull requests:**
+
+- Use simple bools in pymatgen `Structure` converter [\#2021](https://github.com/Materials-Consortia/optimade-python-tools/pull/2021) ([ml-evs](https://github.com/ml-evs))
+
+## [v1.0.3](https://github.com/Materials-Consortia/optimade-python-tools/tree/v1.0.3) (2024-01-30)
+
+[Full Changelog](https://github.com/Materials-Consortia/optimade-python-tools/compare/v0.25.5...v1.0.3)
+
+**Fixed bugs:**
+
+- Add `requests` timeout when getting providers list [\#1955](https://github.com/Materials-Consortia/optimade-python-tools/pull/1955) ([ml-evs](https://github.com/ml-evs))
+
+**Merged pull requests:**
+
+- Fix release workflow [\#1959](https://github.com/Materials-Consortia/optimade-python-tools/pull/1959) ([ml-evs](https://github.com/ml-evs))
+- Bump providers from `46c8d85` to `ad0e214` [\#1948](https://github.com/Materials-Consortia/optimade-python-tools/pull/1948) ([dependabot[bot]](https://github.com/apps/dependabot))
+- Bump providers from `97c69be` to `46c8d85` [\#1946](https://github.com/Materials-Consortia/optimade-python-tools/pull/1946) ([dependabot[bot]](https://github.com/apps/dependabot))
+- Add upper pin for pandas and ignore pandas3/pyarrow deprecation warning [\#1945](https://github.com/Materials-Consortia/optimade-python-tools/pull/1945) ([ml-evs](https://github.com/ml-evs))
+- Bump providers from `93d2b52` to `97c69be` [\#1941](https://github.com/Materials-Consortia/optimade-python-tools/pull/1941) ([dependabot[bot]](https://github.com/apps/dependabot))
+- Client: attempt to switch to synchronous mode when existing event loop is detected [\#1940](https://github.com/Materials-Consortia/optimade-python-tools/pull/1940) ([ml-evs](https://github.com/ml-evs))
+- mkdocs fix global setting of `members: true` [\#1936](https://github.com/Materials-Consortia/optimade-python-tools/pull/1936) ([ml-evs](https://github.com/ml-evs))
+- Allow backported releases in CI [\#1933](https://github.com/Materials-Consortia/optimade-python-tools/pull/1933) ([ml-evs](https://github.com/ml-evs))
+
+## [v0.25.5](https://github.com/Materials-Consortia/optimade-python-tools/tree/v0.25.5) (2024-01-30)
+
+[Full Changelog](https://github.com/Materials-Consortia/optimade-python-tools/compare/v0.25.4...v0.25.5)
+
+**Fixed bugs:**
+
+- Missing timeout for `requests.get` in `get_providers` [\#1954](https://github.com/Materials-Consortia/optimade-python-tools/issues/1954)
+- Transformers not recognizing `LinksResource` [\#939](https://github.com/Materials-Consortia/optimade-python-tools/issues/939)
+
+**Closed issues:**
+
+- ImportError and AttributeError in the Python notebooks on Binder and Google Colab [\#1939](https://github.com/Materials-Consortia/optimade-python-tools/issues/1939)
+- Docs builds with new mkdocstrings handler ignore filters [\#1934](https://github.com/Materials-Consortia/optimade-python-tools/issues/1934)
+- Cannot release backport branches [\#1930](https://github.com/Materials-Consortia/optimade-python-tools/issues/1930)
+
+## [v0.25.4](https://github.com/Materials-Consortia/optimade-python-tools/tree/v0.25.4) (2024-01-11)
+
+[Full Changelog](https://github.com/Materials-Consortia/optimade-python-tools/compare/v1.0.2...v0.25.4)
+
+## [v1.0.2](https://github.com/Materials-Consortia/optimade-python-tools/tree/v1.0.2) (2024-01-11)
+
+[Full Changelog](https://github.com/Materials-Consortia/optimade-python-tools/compare/v1.0.1...v1.0.2)
+
+**Implemented enhancements:**
+
+- Client: counting number of matching entries when `data_returned` is not available [\#1924](https://github.com/Materials-Consortia/optimade-python-tools/issues/1924)
+- Client: add ability to count the number of matches for a filter using binary search over pagination [\#1925](https://github.com/Materials-Consortia/optimade-python-tools/pull/1925) ([ml-evs](https://github.com/ml-evs))
+
+**Merged pull requests:**
+
+- Add upper pin to numpy in preparation for v2 [\#1928](https://github.com/Materials-Consortia/optimade-python-tools/pull/1928) ([ml-evs](https://github.com/ml-evs))
+- Bump providers from `d8322a4` to `93d2b52` [\#1926](https://github.com/Materials-Consortia/optimade-python-tools/pull/1926) ([dependabot[bot]](https://github.com/apps/dependabot))
+- Bump providers from `564a499` to `d8322a4` [\#1909](https://github.com/Materials-Consortia/optimade-python-tools/pull/1909) ([dependabot[bot]](https://github.com/apps/dependabot))
+
+## [v1.0.1](https://github.com/Materials-Consortia/optimade-python-tools/tree/v1.0.1) (2023-12-04)
+
+[Full Changelog](https://github.com/Materials-Consortia/optimade-python-tools/compare/v1.0.0...v1.0.1)
+
+**Closed issues:**
+
+- Validator does not error on empty endpoints [\#1890](https://github.com/Materials-Consortia/optimade-python-tools/issues/1890)
+- Provider fields not served when already prefixed in database [\#1883](https://github.com/Materials-Consortia/optimade-python-tools/issues/1883)
+- Other potential bandaids to pull off before v1.0.0 [\#1847](https://github.com/Materials-Consortia/optimade-python-tools/issues/1847)
+
+**Merged pull requests:**
+
+- Add optional test for whether entry endpoints have any actual entries [\#1891](https://github.com/Materials-Consortia/optimade-python-tools/pull/1891) ([ml-evs](https://github.com/ml-evs))
+
+## [v1.0.0](https://github.com/Materials-Consortia/optimade-python-tools/tree/v1.0.0) (2023-11-09)
+
+[Full Changelog](https://github.com/Materials-Consortia/optimade-python-tools/compare/v0.25.3...v1.0.0)
+
+optimade-python-tools has reached v1.0.0!
+
+We have decided to make this first major release at this point due to the arduous migration to pydantic v2 between v1.0.0 and v0.25.3. This will allow for improved compatibility with the rest of the ecosystem, plus all of the performance and ergonomics benefits of the new pydantic.
+
+If you are using optimade-python-tools primarily as a library, then you may need to make some code changes to support the new version. For example, the underlying API for dealing with the pydantic models has changed (e.g., `model.dict()` is now `model.model_dump()`) -- a full migration guide can be found in the [pydantic docs](https://docs.pydantic.dev/latest/migration/). Additionally, any cases where the underlying JSON schemas were being modified may need to be updated; internally we are pretty much exclusively operating on the pydantic model objects without needing to modify the raw schemas anymore.
+
+Going forward, v1.0.x will be the last series to support v1.1 of the OPTIMADE specification, with future versions of the package v1.x.y adding features from the pre-release of OPTIMADE v1.2.
+
+A branch will be maintained for the v0.25.x series will be maintained for those who are stuck on pydantic v1 and run into bugs. Please make it clear in any bug reports if you are using this version.
+
+**Fixed bugs:**
+
+- Deserialization regressions: cannot resolve child databases in client [\#1843](https://github.com/Materials-Consortia/optimade-python-tools/issues/1843)
+- Performance regressions following pydantic update [\#1835](https://github.com/Materials-Consortia/optimade-python-tools/issues/1835)
+
+**Closed issues:**
+
+- Fly is using the wrong config file [\#1869](https://github.com/Materials-Consortia/optimade-python-tools/issues/1869)
+- Python 3.12 support [\#1859](https://github.com/Materials-Consortia/optimade-python-tools/issues/1859)
+- Remove `nullable` from schemas \(for OpenAPI 3.1\) [\#1814](https://github.com/Materials-Consortia/optimade-python-tools/issues/1814)
+
+**Merged pull requests:**
+
+- Add Python 3.12 to CI [\#1858](https://github.com/Materials-Consortia/optimade-python-tools/pull/1858) ([ml-evs](https://github.com/ml-evs))
+- Add test that checks whether client can find at least one base URL [\#1849](https://github.com/Materials-Consortia/optimade-python-tools/pull/1849) ([ml-evs](https://github.com/ml-evs))
+- Make client provider scraper more robust and add `--version` [\#1848](https://github.com/Materials-Consortia/optimade-python-tools/pull/1848) ([ml-evs](https://github.com/ml-evs))
+- Fix incorrect 'smart' union discrimination [\#1844](https://github.com/Materials-Consortia/optimade-python-tools/pull/1844) ([ml-evs](https://github.com/ml-evs))
+- CI performance refresh [\#1836](https://github.com/Materials-Consortia/optimade-python-tools/pull/1836) ([ml-evs](https://github.com/ml-evs))
+- Modernize all Python 3.8 annotations [\#1815](https://github.com/Materials-Consortia/optimade-python-tools/pull/1815) ([ml-evs](https://github.com/ml-evs))
+- Migration to pydantic v2 [\#1745](https://github.com/Materials-Consortia/optimade-python-tools/pull/1745) ([ml-evs](https://github.com/ml-evs))
+
## [v0.25.3](https://github.com/Materials-Consortia/optimade-python-tools/tree/v0.25.3) (2023-09-29)
[Full Changelog](https://github.com/Materials-Consortia/optimade-python-tools/compare/v0.25.2...v0.25.3)
diff --git a/Procfile b/Procfile
index f891fdb62..6098cfb4b 100644
--- a/Procfile
+++ b/Procfile
@@ -1 +1 @@
-web: uvicorn optimade.server.main:app --host 0.0.0.0 --port $PORT
+web: OPTIMADE_CONFIG_FILE=./tests/test_config.json uvicorn optimade.server.main:app --host 0.0.0.0 --port $PORT
diff --git a/README.md b/README.md
index 9e4d75f86..f9713e879 100644
--- a/README.md
+++ b/README.md
@@ -87,18 +87,18 @@ Each release of the `optimade` package from this repository only targets one ver
OPTIMADE API version |
- optimade version |
+ optimade requirements |
v1.0.0 |
- v0.12.9 |
+ optimade<=0.12.9 |
v1.1.0 |
- v0.16.0+ |
+ optimade~=0.16 |
diff --git a/docs/all_models.md b/docs/all_models.md
index e9126b03b..1c9d1e7c8 100644
--- a/docs/all_models.md
+++ b/docs/all_models.md
@@ -11,3 +11,6 @@ For example, the three OPTIMADE entry types, `structures`, `references` and `lin
As well as validating data types when creating instances of these models, this package defines several OPTIMADE-specific validators that ensure consistency between fields (e.g., the value of `nsites` matches the number of positions provided in `cartesian_site_positions`).
::: optimade.models
+ options:
+ show_submodules: true
+ show_if_no_docstring: true
diff --git a/docs/api_reference/models/types.md b/docs/api_reference/models/types.md
new file mode 100644
index 000000000..caba0e97d
--- /dev/null
+++ b/docs/api_reference/models/types.md
@@ -0,0 +1,5 @@
+# types
+
+::: optimade.models.types
+ options:
+ show_if_no_docstring: true
diff --git a/docs/static/default_config.json b/docs/static/default_config.json
index ba510dc4f..0c7826c5c 100644
--- a/docs/static/default_config.json
+++ b/docs/static/default_config.json
@@ -12,7 +12,7 @@
"base_url": null,
"implementation": {
"name": "OPTIMADE Python Tools",
- "version": "0.25.3",
+ "version": "1.0.5",
"source_url": "https://github.com/Materials-Consortia/optimade-python-tools",
"maintainer": {"email": "dev@optimade.org"}
},
diff --git a/mkdocs.yml b/mkdocs.yml
index b97931e37..197649435 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -70,29 +70,37 @@ plugins:
lang: en
- mkdocstrings:
default_handler: python
+ enable_inventory: true
handlers:
python:
options:
+ # General options
+ show_source: true
+ show_bases: true
+
+ # Heading options
+ heading_level: 2
show_root_heading: false
show_root_toc_entry: true
show_root_full_path: true
show_object_full_path: false
show_category_heading: false
- show_if_no_docstring: false
- show_source: true
- show_bases: true
- group_by_category: true
- heading_level: 2
+
+ # Members options
+ inherited_members: true
filters:
- "!^_[^_]"
- "!__json_encoder__$"
- "!__all__$"
- "!__config__$"
- "!ValidatorResults$"
- members: true
- inherited_members: true
+ group_by_category: true
+
+ # Docstring options
docstring_style: google
- enable_inventory: true
+ docstring_options:
+ replace_admonitions: true
+ show_if_no_docstring: false
- awesome-pages
- autorefs
diff --git a/openapi/index_openapi.json b/openapi/index_openapi.json
index 62c29ef4a..a61dc3c16 100644
--- a/openapi/index_openapi.json
+++ b/openapi/index_openapi.json
@@ -2,7 +2,7 @@
"openapi": "3.1.0",
"info": {
"title": "OPTIMADE API - Index meta-database",
- "description": "The [Open Databases Integration for Materials Design (OPTIMADE) consortium](https://www.optimade.org/) aims to make materials databases interoperational by developing a common REST API.\nThis is the \"special\" index meta-database.\n\nThis specification is generated using [`optimade-python-tools`](https://github.com/Materials-Consortia/optimade-python-tools/tree/v0.25.3) v0.25.3.",
+ "description": "The [Open Databases Integration for Materials Design (OPTIMADE) consortium](https://www.optimade.org/) aims to make materials databases interoperational by developing a common REST API.\nThis is the \"special\" index meta-database.\n\nThis specification is generated using [`optimade-python-tools`](https://github.com/Materials-Consortia/optimade-python-tools/tree/v1.0.5) v1.0.5.",
"version": "1.1.0"
},
"paths": {
@@ -106,164 +106,164 @@
"operationId": "get_links_links_get",
"parameters": [
{
- "description": "A filter string, in the format described in section API Filtering Format Specification of the specification.",
+ "name": "filter",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
- "title": "Filter",
"description": "A filter string, in the format described in section API Filtering Format Specification of the specification.",
- "default": ""
+ "default": "",
+ "title": "Filter"
},
- "name": "filter",
- "in": "query"
+ "description": "A filter string, in the format described in section API Filtering Format Specification of the specification."
},
{
- "description": "The output format requested (see section Response Format).\nDefaults to the format string 'json', which specifies the standard output format described in this specification.\nExample: `http://example.com/v1/structures?response_format=xml`",
+ "name": "response_format",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
- "title": "Response Format",
"description": "The output format requested (see section Response Format).\nDefaults to the format string 'json', which specifies the standard output format described in this specification.\nExample: `http://example.com/v1/structures?response_format=xml`",
- "default": "json"
+ "default": "json",
+ "title": "Response Format"
},
- "name": "response_format",
- "in": "query"
+ "description": "The output format requested (see section Response Format).\nDefaults to the format string 'json', which specifies the standard output format described in this specification.\nExample: `http://example.com/v1/structures?response_format=xml`"
},
{
- "description": "An email address of the user making the request.\nThe email SHOULD be that of a person and not an automatic system.\nExample: `http://example.com/v1/structures?email_address=user@example.com`",
+ "name": "email_address",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
"format": "email",
- "title": "Email Address",
"description": "An email address of the user making the request.\nThe email SHOULD be that of a person and not an automatic system.\nExample: `http://example.com/v1/structures?email_address=user@example.com`",
- "default": ""
+ "default": "",
+ "title": "Email Address"
},
- "name": "email_address",
- "in": "query"
+ "description": "An email address of the user making the request.\nThe email SHOULD be that of a person and not an automatic system.\nExample: `http://example.com/v1/structures?email_address=user@example.com`"
},
{
- "description": "A comma-delimited set of fields to be provided in the output.\nIf provided, these fields MUST be returned along with the REQUIRED fields.\nOther OPTIONAL fields MUST NOT be returned when this parameter is present.\nExample: `http://example.com/v1/structures?response_fields=last_modified,nsites`",
+ "name": "response_fields",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
"pattern": "([a-z_][a-z_0-9]*(,[a-z_][a-z_0-9]*)*)?",
- "title": "Response Fields",
"description": "A comma-delimited set of fields to be provided in the output.\nIf provided, these fields MUST be returned along with the REQUIRED fields.\nOther OPTIONAL fields MUST NOT be returned when this parameter is present.\nExample: `http://example.com/v1/structures?response_fields=last_modified,nsites`",
- "default": ""
+ "default": "",
+ "title": "Response Fields"
},
- "name": "response_fields",
- "in": "query"
+ "description": "A comma-delimited set of fields to be provided in the output.\nIf provided, these fields MUST be returned along with the REQUIRED fields.\nOther OPTIONAL fields MUST NOT be returned when this parameter is present.\nExample: `http://example.com/v1/structures?response_fields=last_modified,nsites`"
},
{
- "description": "If supporting sortable queries, an implementation MUST use the `sort` query parameter with format as specified by [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-sorting).\n\nAn implementation MAY support multiple sort fields for a single query.\nIf it does, it again MUST conform to the JSON API 1.0 specification.\n\nIf an implementation supports sorting for an entry listing endpoint, then the `/info/` endpoint MUST include, for each field name `` in its `data.properties.` response value that can be used for sorting, the key `sortable` with value `true`.\nIf a field name under an entry listing endpoint supporting sorting cannot be used for sorting, the server MUST either leave out the `sortable` key or set it equal to `false` for the specific field name.\nThe set of field names, with `sortable` equal to `true` are allowed to be used in the \"sort fields\" list according to its definition in the JSON API 1.0 specification.\nThe field `sortable` is in addition to each property description and other OPTIONAL fields.\nAn example is shown in the section Entry Listing Info Endpoints.",
+ "name": "sort",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
"pattern": "([a-z_][a-z_0-9]*(,[a-z_][a-z_0-9]*)*)?",
- "title": "Sort",
"description": "If supporting sortable queries, an implementation MUST use the `sort` query parameter with format as specified by [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-sorting).\n\nAn implementation MAY support multiple sort fields for a single query.\nIf it does, it again MUST conform to the JSON API 1.0 specification.\n\nIf an implementation supports sorting for an entry listing endpoint, then the `/info/` endpoint MUST include, for each field name `` in its `data.properties.` response value that can be used for sorting, the key `sortable` with value `true`.\nIf a field name under an entry listing endpoint supporting sorting cannot be used for sorting, the server MUST either leave out the `sortable` key or set it equal to `false` for the specific field name.\nThe set of field names, with `sortable` equal to `true` are allowed to be used in the \"sort fields\" list according to its definition in the JSON API 1.0 specification.\nThe field `sortable` is in addition to each property description and other OPTIONAL fields.\nAn example is shown in the section Entry Listing Info Endpoints.",
- "default": ""
+ "default": "",
+ "title": "Sort"
},
- "name": "sort",
- "in": "query"
+ "description": "If supporting sortable queries, an implementation MUST use the `sort` query parameter with format as specified by [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-sorting).\n\nAn implementation MAY support multiple sort fields for a single query.\nIf it does, it again MUST conform to the JSON API 1.0 specification.\n\nIf an implementation supports sorting for an entry listing endpoint, then the `/info/` endpoint MUST include, for each field name `` in its `data.properties.` response value that can be used for sorting, the key `sortable` with value `true`.\nIf a field name under an entry listing endpoint supporting sorting cannot be used for sorting, the server MUST either leave out the `sortable` key or set it equal to `false` for the specific field name.\nThe set of field names, with `sortable` equal to `true` are allowed to be used in the \"sort fields\" list according to its definition in the JSON API 1.0 specification.\nThe field `sortable` is in addition to each property description and other OPTIONAL fields.\nAn example is shown in the section Entry Listing Info Endpoints."
},
{
- "description": "Sets a numerical limit on the number of entries returned.\nSee [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-pagination).\nThe API implementation MUST return no more than the number specified.\nIt MAY return fewer.\nThe database MAY have a maximum limit and not accept larger numbers (in which case an error code -- 403 Forbidden -- MUST be returned).\nThe default limit value is up to the API implementation to decide.\nExample: `http://example.com/optimade/v1/structures?page_limit=100`",
+ "name": "page_limit",
+ "in": "query",
"required": false,
"schema": {
"type": "integer",
- "minimum": 0.0,
- "title": "Page Limit",
+ "minimum": 0,
"description": "Sets a numerical limit on the number of entries returned.\nSee [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-pagination).\nThe API implementation MUST return no more than the number specified.\nIt MAY return fewer.\nThe database MAY have a maximum limit and not accept larger numbers (in which case an error code -- 403 Forbidden -- MUST be returned).\nThe default limit value is up to the API implementation to decide.\nExample: `http://example.com/optimade/v1/structures?page_limit=100`",
- "default": 20
+ "default": 20,
+ "title": "Page Limit"
},
- "name": "page_limit",
- "in": "query"
+ "description": "Sets a numerical limit on the number of entries returned.\nSee [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-pagination).\nThe API implementation MUST return no more than the number specified.\nIt MAY return fewer.\nThe database MAY have a maximum limit and not accept larger numbers (in which case an error code -- 403 Forbidden -- MUST be returned).\nThe default limit value is up to the API implementation to decide.\nExample: `http://example.com/optimade/v1/structures?page_limit=100`"
},
{
- "description": "RECOMMENDED for use with _offset-based_ pagination: using `page_offset` and `page_limit` is RECOMMENDED.\nExample: Skip 50 structures and fetch up to 100: `/structures?page_offset=50&page_limit=100`.",
+ "name": "page_offset",
+ "in": "query",
"required": false,
"schema": {
"type": "integer",
- "minimum": 0.0,
- "title": "Page Offset",
+ "minimum": 0,
"description": "RECOMMENDED for use with _offset-based_ pagination: using `page_offset` and `page_limit` is RECOMMENDED.\nExample: Skip 50 structures and fetch up to 100: `/structures?page_offset=50&page_limit=100`.",
- "default": 0
+ "default": 0,
+ "title": "Page Offset"
},
- "name": "page_offset",
- "in": "query"
+ "description": "RECOMMENDED for use with _offset-based_ pagination: using `page_offset` and `page_limit` is RECOMMENDED.\nExample: Skip 50 structures and fetch up to 100: `/structures?page_offset=50&page_limit=100`."
},
{
- "description": "RECOMMENDED for use with _page-based_ pagination: using `page_number` and `page_limit` is RECOMMENDED.\nIt is RECOMMENDED that the first page has number 1, i.e., that `page_number` is 1-based.\nExample: Fetch page 2 of up to 50 structures per page: `/structures?page_number=2&page_limit=50`.",
+ "name": "page_number",
+ "in": "query",
"required": false,
"schema": {
"type": "integer",
- "title": "Page Number",
- "description": "RECOMMENDED for use with _page-based_ pagination: using `page_number` and `page_limit` is RECOMMENDED.\nIt is RECOMMENDED that the first page has number 1, i.e., that `page_number` is 1-based.\nExample: Fetch page 2 of up to 50 structures per page: `/structures?page_number=2&page_limit=50`."
+ "description": "RECOMMENDED for use with _page-based_ pagination: using `page_number` and `page_limit` is RECOMMENDED.\nIt is RECOMMENDED that the first page has number 1, i.e., that `page_number` is 1-based.\nExample: Fetch page 2 of up to 50 structures per page: `/structures?page_number=2&page_limit=50`.",
+ "title": "Page Number"
},
- "name": "page_number",
- "in": "query"
+ "description": "RECOMMENDED for use with _page-based_ pagination: using `page_number` and `page_limit` is RECOMMENDED.\nIt is RECOMMENDED that the first page has number 1, i.e., that `page_number` is 1-based.\nExample: Fetch page 2 of up to 50 structures per page: `/structures?page_number=2&page_limit=50`."
},
{
- "description": "RECOMMENDED for use with _cursor-based_ pagination: using `page_cursor` and `page_limit` is RECOMMENDED.",
+ "name": "page_cursor",
+ "in": "query",
"required": false,
"schema": {
"type": "integer",
- "minimum": 0.0,
- "title": "Page Cursor",
+ "minimum": 0,
"description": "RECOMMENDED for use with _cursor-based_ pagination: using `page_cursor` and `page_limit` is RECOMMENDED.",
- "default": 0
+ "default": 0,
+ "title": "Page Cursor"
},
- "name": "page_cursor",
- "in": "query"
+ "description": "RECOMMENDED for use with _cursor-based_ pagination: using `page_cursor` and `page_limit` is RECOMMENDED."
},
{
- "description": "RECOMMENDED for use with _value-based_ pagination: using `page_above`/`page_below` and `page_limit` is RECOMMENDED.\nExample: Fetch up to 100 structures above sort-field value 4000 (in this example, server chooses to fetch results sorted by increasing `id`, so `page_above` value refers to an `id` value): `/structures?page_above=4000&page_limit=100`.",
+ "name": "page_above",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
- "title": "Page Above",
- "description": "RECOMMENDED for use with _value-based_ pagination: using `page_above`/`page_below` and `page_limit` is RECOMMENDED.\nExample: Fetch up to 100 structures above sort-field value 4000 (in this example, server chooses to fetch results sorted by increasing `id`, so `page_above` value refers to an `id` value): `/structures?page_above=4000&page_limit=100`."
+ "description": "RECOMMENDED for use with _value-based_ pagination: using `page_above`/`page_below` and `page_limit` is RECOMMENDED.\nExample: Fetch up to 100 structures above sort-field value 4000 (in this example, server chooses to fetch results sorted by increasing `id`, so `page_above` value refers to an `id` value): `/structures?page_above=4000&page_limit=100`.",
+ "title": "Page Above"
},
- "name": "page_above",
- "in": "query"
+ "description": "RECOMMENDED for use with _value-based_ pagination: using `page_above`/`page_below` and `page_limit` is RECOMMENDED.\nExample: Fetch up to 100 structures above sort-field value 4000 (in this example, server chooses to fetch results sorted by increasing `id`, so `page_above` value refers to an `id` value): `/structures?page_above=4000&page_limit=100`."
},
{
- "description": "RECOMMENDED for use with _value-based_ pagination: using `page_above`/`page_below` and `page_limit` is RECOMMENDED.",
+ "name": "page_below",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
- "title": "Page Below",
- "description": "RECOMMENDED for use with _value-based_ pagination: using `page_above`/`page_below` and `page_limit` is RECOMMENDED."
+ "description": "RECOMMENDED for use with _value-based_ pagination: using `page_above`/`page_below` and `page_limit` is RECOMMENDED.",
+ "title": "Page Below"
},
- "name": "page_below",
- "in": "query"
+ "description": "RECOMMENDED for use with _value-based_ pagination: using `page_above`/`page_below` and `page_limit` is RECOMMENDED."
},
{
- "description": "A server MAY implement the JSON API concept of returning [compound documents](https://jsonapi.org/format/1.0/#document-compound-documents) by utilizing the `include` query parameter as specified by [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-includes).\n\nAll related resource objects MUST be returned as part of an array value for the top-level `included` field, see the section JSON Response Schema: Common Fields.\n\nThe value of `include` MUST be a comma-separated list of \"relationship paths\", as defined in the [JSON API](https://jsonapi.org/format/1.0/#fetching-includes).\nIf relationship paths are not supported, or a server is unable to identify a relationship path a `400 Bad Request` response MUST be made.\n\nThe **default value** for `include` is `references`.\nThis means `references` entries MUST always be included under the top-level field `included` as default, since a server assumes if `include` is not specified by a client in the request, it is still specified as `include=references`.\nNote, if a client explicitly specifies `include` and leaves out `references`, `references` resource objects MUST NOT be included under the top-level field `included`, as per the definition of `included`, see section JSON Response Schema: Common Fields.\n\n> **Note**: A query with the parameter `include` set to the empty string means no related resource objects are to be returned under the top-level field `included`.",
+ "name": "include",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
- "title": "Include",
"description": "A server MAY implement the JSON API concept of returning [compound documents](https://jsonapi.org/format/1.0/#document-compound-documents) by utilizing the `include` query parameter as specified by [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-includes).\n\nAll related resource objects MUST be returned as part of an array value for the top-level `included` field, see the section JSON Response Schema: Common Fields.\n\nThe value of `include` MUST be a comma-separated list of \"relationship paths\", as defined in the [JSON API](https://jsonapi.org/format/1.0/#fetching-includes).\nIf relationship paths are not supported, or a server is unable to identify a relationship path a `400 Bad Request` response MUST be made.\n\nThe **default value** for `include` is `references`.\nThis means `references` entries MUST always be included under the top-level field `included` as default, since a server assumes if `include` is not specified by a client in the request, it is still specified as `include=references`.\nNote, if a client explicitly specifies `include` and leaves out `references`, `references` resource objects MUST NOT be included under the top-level field `included`, as per the definition of `included`, see section JSON Response Schema: Common Fields.\n\n> **Note**: A query with the parameter `include` set to the empty string means no related resource objects are to be returned under the top-level field `included`.",
- "default": "references"
+ "default": "references",
+ "title": "Include"
},
- "name": "include",
- "in": "query"
+ "description": "A server MAY implement the JSON API concept of returning [compound documents](https://jsonapi.org/format/1.0/#document-compound-documents) by utilizing the `include` query parameter as specified by [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-includes).\n\nAll related resource objects MUST be returned as part of an array value for the top-level `included` field, see the section JSON Response Schema: Common Fields.\n\nThe value of `include` MUST be a comma-separated list of \"relationship paths\", as defined in the [JSON API](https://jsonapi.org/format/1.0/#fetching-includes).\nIf relationship paths are not supported, or a server is unable to identify a relationship path a `400 Bad Request` response MUST be made.\n\nThe **default value** for `include` is `references`.\nThis means `references` entries MUST always be included under the top-level field `included` as default, since a server assumes if `include` is not specified by a client in the request, it is still specified as `include=references`.\nNote, if a client explicitly specifies `include` and leaves out `references`, `references` resource objects MUST NOT be included under the top-level field `included`, as per the definition of `included`, see section JSON Response Schema: Common Fields.\n\n> **Note**: A query with the parameter `include` set to the empty string means no related resource objects are to be returned under the top-level field `included`."
},
{
- "description": "If the client provides the parameter, the value SHOULD have the format `vMAJOR` or `vMAJOR.MINOR`, where MAJOR is a major version and MINOR is a minor version of the API. For example, if a client appends `api_hint=v1.0` to the query string, the hint provided is for major version 1 and minor version 0.",
+ "name": "api_hint",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
"pattern": "(v[0-9]+(\\.[0-9]+)?)?",
- "title": "Api Hint",
"description": "If the client provides the parameter, the value SHOULD have the format `vMAJOR` or `vMAJOR.MINOR`, where MAJOR is a major version and MINOR is a minor version of the API. For example, if a client appends `api_hint=v1.0` to the query string, the hint provided is for major version 1 and minor version 0.",
- "default": ""
+ "default": "",
+ "title": "Api Hint"
},
- "name": "api_hint",
- "in": "query"
+ "description": "If the client provides the parameter, the value SHOULD have the format `vMAJOR` or `vMAJOR.MINOR`, where MAJOR is a major version and MINOR is a minor version of the API. For example, if a client appends `api_hint=v1.0` to the query string, the hint provided is for major version 1 and minor version 0."
}
],
"responses": {
@@ -376,6 +376,7 @@
"components": {
"schemas": {
"Aggregate": {
+ "type": "string",
"enum": [
"ok",
"test",
@@ -387,6 +388,7 @@
},
"Attributes": {
"properties": {},
+ "additionalProperties": true,
"type": "object",
"title": "Attributes",
"description": "Members of the attributes object (\"attributes\") represent information about the resource object in which it's defined.\nThe keys for Attributes MUST NOT be:\n relationships\n links\n id\n type"
@@ -395,9 +397,8 @@
"properties": {
"url": {
"type": "string",
- "maxLength": 65536,
"minLength": 1,
- "pattern": ".+/v[0-1](\\.[0-9]+)*/?$",
+ "pattern": "^.+/v[0-1](\\.[0-9]+)*/?$",
"format": "uri",
"title": "Url",
"description": "A string specifying a versioned base URL that MUST adhere to the rules in section Base URL"
@@ -407,7 +408,7 @@
"pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$",
"title": "Version",
"description": "A string containing the full version number of the API served at that versioned base URL.\nThe version number string MUST NOT be prefixed by, e.g., 'v'.\nExamples: `1.0.0`, `1.0.0-rc.2`.",
- "example": [
+ "examples": [
"0.10.1",
"1.0.0-rc.2",
"1.2.3-rc.5+develop"
@@ -430,6 +431,7 @@
"description": "OPTIONAL human-readable description of the relationship."
}
},
+ "additionalProperties": true,
"type": "object",
"required": [
"description"
@@ -450,12 +452,14 @@
"description": "Resource type"
},
"meta": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/BaseRelationshipMeta"
+ },
+ {
+ "type": "null"
}
],
- "title": "Meta",
"description": "Relationship meta field. MUST contain 'description' if supplied."
}
},
@@ -470,21 +474,25 @@
"EntryRelationships": {
"properties": {
"references": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/ReferenceRelationship"
+ },
+ {
+ "type": "null"
}
],
- "title": "References",
"description": "Object containing links to relationships with entries of the `references` type."
},
"structures": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/StructureRelationship"
+ },
+ {
+ "type": "null"
}
],
- "title": "Structures",
"description": "Object containing links to relationships with entries of the `structures` type."
}
},
@@ -498,32 +506,36 @@
"type": "string",
"title": "Id",
"description": "An entry's ID as defined in section Definition of Terms.\n\n- **Type**: string.\n\n- **Requirements/Conventions**:\n - **Support**: MUST be supported by all implementations, MUST NOT be `null`.\n - **Query**: MUST be a queryable property with support for all mandatory filter features.\n - **Response**: REQUIRED in the response.\n\n- **Examples**:\n - `\"db/1234567\"`\n - `\"cod/2000000\"`\n - `\"cod/2000000@1234567\"`\n - `\"nomad/L1234567890\"`\n - `\"42\"`",
- "x-optimade-support": "must",
- "x-optimade-queryable": "must"
+ "x-optimade-queryable": "must",
+ "x-optimade-support": "must"
},
"type": {
"type": "string",
"title": "Type",
"description": "The name of the type of an entry.\n\n- **Type**: string.\n\n- **Requirements/Conventions**:\n - **Support**: MUST be supported by all implementations, MUST NOT be `null`.\n - **Query**: MUST be a queryable property with support for all mandatory filter features.\n - **Response**: REQUIRED in the response.\n - MUST be an existing entry type.\n - The entry of type `` and ID `` MUST be returned in response to a request for `//` under the versioned base URL.\n\n- **Example**: `\"structures\"`",
- "x-optimade-support": "must",
- "x-optimade-queryable": "must"
+ "x-optimade-queryable": "must",
+ "x-optimade-support": "must"
},
"links": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/ResourceLinks"
+ },
+ {
+ "type": "null"
}
],
- "title": "Links",
"description": "a links object containing links related to the resource."
},
"meta": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/Meta"
+ },
+ {
+ "type": "null"
}
],
- "title": "Meta",
"description": "a meta object containing non-standard meta-information about a resource that can not be represented as an attribute or relationship."
},
"attributes": {
@@ -532,16 +544,17 @@
"$ref": "#/components/schemas/EntryResourceAttributes"
}
],
- "title": "Attributes",
"description": "A dictionary, containing key-value pairs representing the entry's properties, except for `type` and `id`.\nDatabase-provider-specific properties need to include the database-provider-specific prefix (see section on Database-Provider-Specific Namespace Prefixes)."
},
"relationships": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/EntryRelationships"
+ },
+ {
+ "type": "null"
}
],
- "title": "Relationships",
"description": "A dictionary containing references to other entries according to the description in section Relationships encoded as [JSON API Relationships](https://jsonapi.org/format/1.0/#document-resource-object-relationships).\nThe OPTIONAL human-readable description of the relationship MAY be provided in the `description` field inside the `meta` dictionary of the JSON API resource identifier object."
}
},
@@ -557,21 +570,36 @@
"EntryResourceAttributes": {
"properties": {
"immutable_id": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Immutable Id",
"description": "The entry's immutable ID (e.g., an UUID). This is important for databases having preferred IDs that point to \"the latest version\" of a record, but still offer access to older variants. This ID maps to the version-specific record, in case it changes in the future.\n\n- **Type**: string.\n\n- **Requirements/Conventions**:\n - **Support**: OPTIONAL support in implementations, i.e., MAY be `null`.\n - **Query**: MUST be a queryable property with support for all mandatory filter features.\n\n- **Examples**:\n - `\"8bd3e750-b477-41a0-9b11-3a799f21b44f\"`\n - `\"fjeiwoj,54;@=%<>#32\"` (Strings that are not URL-safe are allowed.)",
- "x-optimade-support": "optional",
- "x-optimade-queryable": "must"
+ "x-optimade-queryable": "must",
+ "x-optimade-support": "optional"
},
"last_modified": {
- "type": "string",
- "format": "date-time",
+ "anyOf": [
+ {
+ "type": "string",
+ "format": "date-time"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Last Modified",
"description": "Date and time representing when the entry was last modified.\n\n- **Type**: timestamp.\n\n- **Requirements/Conventions**:\n - **Support**: SHOULD be supported by all implementations, i.e., SHOULD NOT be `null`.\n - **Query**: MUST be a queryable property with support for all mandatory filter features.\n - **Response**: REQUIRED in the response unless the query parameter `response_fields` is present and does not include this property.\n\n- **Example**:\n - As part of JSON response format: `\"2007-04-05T14:30:20Z\"` (i.e., encoded as an [RFC 3339 Internet Date/Time Format](https://tools.ietf.org/html/rfc3339#section-5.6) string.)",
- "x-optimade-support": "should",
- "x-optimade-queryable": "must"
+ "x-optimade-queryable": "must",
+ "x-optimade-support": "should"
}
},
+ "additionalProperties": true,
"type": "object",
"required": [
"last_modified"
@@ -582,55 +610,96 @@
"Error": {
"properties": {
"id": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Id",
"description": "A unique identifier for this particular occurrence of the problem."
},
"links": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/ErrorLinks"
+ },
+ {
+ "type": "null"
}
],
- "title": "Links",
"description": "A links object storing about"
},
"status": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Status",
"description": "the HTTP status code applicable to this problem, expressed as a string value."
},
"code": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Code",
"description": "an application-specific error code, expressed as a string value."
},
"title": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Title",
"description": "A short, human-readable summary of the problem. It **SHOULD NOT** change from occurrence to occurrence of the problem, except for purposes of localization."
},
"detail": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Detail",
"description": "A human-readable explanation specific to this occurrence of the problem."
},
"source": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/ErrorSource"
+ },
+ {
+ "type": "null"
}
],
- "title": "Source",
"description": "An object containing references to the source of the error"
},
"meta": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/Meta"
+ },
+ {
+ "type": "null"
}
],
- "title": "Meta",
"description": "a meta object containing non-standard meta-information about the error."
}
},
@@ -644,12 +713,14 @@
"anyOf": [
{
"type": "string",
- "maxLength": 65536,
"minLength": 1,
"format": "uri"
},
{
"$ref": "#/components/schemas/Link"
+ },
+ {
+ "type": "null"
}
],
"title": "About",
@@ -672,6 +743,9 @@
"$ref": "#/components/schemas/Resource"
},
"type": "array"
+ },
+ {
+ "type": "null"
}
],
"uniqueItems": true,
@@ -684,7 +758,6 @@
"$ref": "#/components/schemas/ResponseMeta"
}
],
- "title": "Meta",
"description": "A meta object containing non-standard information."
},
"errors": {
@@ -697,30 +770,41 @@
"description": "A list of OPTIMADE-specific JSON API error objects, where the field detail MUST be present."
},
"included": {
- "items": {
- "$ref": "#/components/schemas/Resource"
- },
- "type": "array",
+ "anyOf": [
+ {
+ "items": {
+ "$ref": "#/components/schemas/Resource"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
"uniqueItems": true,
"title": "Included",
"description": "A list of unique included resources"
},
"links": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/ToplevelLinks"
+ },
+ {
+ "type": "null"
}
],
- "title": "Links",
"description": "Links associated with the primary data or errors"
},
"jsonapi": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/JsonApi"
+ },
+ {
+ "type": "null"
}
],
- "title": "Jsonapi",
"description": "Information about the JSON API used"
}
},
@@ -735,12 +819,26 @@
"ErrorSource": {
"properties": {
"pointer": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Pointer",
"description": "a JSON Pointer [RFC6901] to the associated entity in the request document [e.g. \"/data\" for a primary data object, or \"/data/attributes/title\" for a specific attribute]."
},
"parameter": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Parameter",
"description": "a string indicating which URI query parameter caused the error."
}
@@ -752,12 +850,26 @@
"Implementation": {
"properties": {
"name": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Name",
"description": "name of the implementation"
},
"version": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Version",
"description": "version string of the current implementation"
},
@@ -765,12 +877,14 @@
"anyOf": [
{
"type": "string",
- "maxLength": 65536,
"minLength": 1,
"format": "uri"
},
{
"$ref": "#/components/schemas/Link"
+ },
+ {
+ "type": "null"
}
],
"title": "Homepage",
@@ -780,36 +894,42 @@
"anyOf": [
{
"type": "string",
- "maxLength": 65536,
"minLength": 1,
"format": "uri"
},
{
"$ref": "#/components/schemas/Link"
+ },
+ {
+ "type": "null"
}
],
"title": "Source Url",
"description": "A [JSON API links object](http://jsonapi.org/format/1.0/#document-links) pointing to the implementation source, either downloadable archive or version control system."
},
"maintainer": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/ImplementationMaintainer"
+ },
+ {
+ "type": "null"
}
],
- "title": "Maintainer",
"description": "A dictionary providing details about the maintainer of the implementation."
},
"issue_tracker": {
"anyOf": [
{
"type": "string",
- "maxLength": 65536,
"minLength": 1,
"format": "uri"
},
{
"$ref": "#/components/schemas/Link"
+ },
+ {
+ "type": "null"
}
],
"title": "Issue Tracker",
@@ -843,7 +963,7 @@
"pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$",
"title": "Api Version",
"description": "Presently used full version of the OPTIMADE API.\nThe version number string MUST NOT be prefixed by, e.g., \"v\".\nExamples: `1.0.0`, `1.0.0-rc.2`.",
- "example": [
+ "examples": [
"0.10.1",
"1.0.0-rc.2",
"1.2.3-rc.5+develop"
@@ -908,42 +1028,59 @@
"properties": {
"id": {
"type": "string",
- "pattern": "^/$",
+ "enum": [
+ "/"
+ ],
+ "const": "/",
"title": "Id",
"default": "/"
},
"type": {
"type": "string",
- "pattern": "^info$",
+ "enum": [
+ "info"
+ ],
+ "const": "info",
"title": "Type",
"default": "info"
},
"links": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/ResourceLinks"
+ },
+ {
+ "type": "null"
}
],
- "title": "Links",
"description": "a links object containing links related to the resource."
},
"meta": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/Meta"
+ },
+ {
+ "type": "null"
}
],
- "title": "Meta",
"description": "a meta object containing non-standard meta-information about a resource that can not be represented as an attribute or relationship."
},
"attributes": {
"$ref": "#/components/schemas/IndexInfoAttributes"
},
"relationships": {
- "additionalProperties": {
- "$ref": "#/components/schemas/IndexRelationship"
- },
- "type": "object",
+ "anyOf": [
+ {
+ "additionalProperties": {
+ "$ref": "#/components/schemas/IndexRelationship"
+ },
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Relationships",
"description": "Reference to the Links identifier object under the `links` endpoint that the provider has chosen as their 'default' OPTIMADE API database.\nA client SHOULD present this database as the first choice when an end-user chooses this provider."
}
@@ -966,7 +1103,6 @@
"$ref": "#/components/schemas/IndexInfoResource"
}
],
- "title": "Data",
"description": "Index meta-database /info data."
},
"meta": {
@@ -975,43 +1111,60 @@
"$ref": "#/components/schemas/ResponseMeta"
}
],
- "title": "Meta",
"description": "A meta object containing non-standard information"
},
"errors": {
- "items": {
- "$ref": "#/components/schemas/Error"
- },
- "type": "array",
+ "anyOf": [
+ {
+ "items": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
"uniqueItems": true,
"title": "Errors",
"description": "A list of unique errors"
},
"included": {
- "items": {
- "$ref": "#/components/schemas/Resource"
- },
- "type": "array",
+ "anyOf": [
+ {
+ "items": {
+ "$ref": "#/components/schemas/Resource"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
"uniqueItems": true,
"title": "Included",
"description": "A list of unique included resources"
},
"links": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/ToplevelLinks"
+ },
+ {
+ "type": "null"
}
],
- "title": "Links",
"description": "Links associated with the primary data or errors"
},
"jsonapi": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/JsonApi"
+ },
+ {
+ "type": "null"
}
],
- "title": "Jsonapi",
"description": "Information about the JSON API used"
}
},
@@ -1020,18 +1173,19 @@
"data",
"meta"
],
- "title": "IndexInfoResponse",
- "description": "errors are not allowed"
+ "title": "IndexInfoResponse"
},
"IndexRelationship": {
"properties": {
"data": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/RelatedLinksResource"
+ },
+ {
+ "type": "null"
}
],
- "title": "Data",
"description": "[JSON API resource linkage](http://jsonapi.org/format/1.0/#document-links).\nIt MUST be either `null` or contain a single Links identifier object with the fields `id` and `type`"
}
},
@@ -1051,12 +1205,14 @@
"default": "1.0"
},
"meta": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/Meta"
+ },
+ {
+ "type": "null"
}
],
- "title": "Meta",
"description": "Non-standard meta information"
}
},
@@ -1068,19 +1224,20 @@
"properties": {
"href": {
"type": "string",
- "maxLength": 65536,
"minLength": 1,
"format": "uri",
"title": "Href",
- "description": "a string containing the link\u2019s URL."
+ "description": "a string containing the link's URL."
},
"meta": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/Meta"
+ },
+ {
+ "type": "null"
}
],
- "title": "Meta",
"description": "a meta object containing non-standard meta-information about the link."
}
},
@@ -1092,6 +1249,7 @@
"description": "A link **MUST** be represented as either: a string containing the link's URL or a link object."
},
"LinkType": {
+ "type": "string",
"enum": [
"child",
"root",
@@ -1107,32 +1265,40 @@
"type": "string",
"title": "Id",
"description": "An entry's ID as defined in section Definition of Terms.\n\n- **Type**: string.\n\n- **Requirements/Conventions**:\n - **Support**: MUST be supported by all implementations, MUST NOT be `null`.\n - **Query**: MUST be a queryable property with support for all mandatory filter features.\n - **Response**: REQUIRED in the response.\n\n- **Examples**:\n - `\"db/1234567\"`\n - `\"cod/2000000\"`\n - `\"cod/2000000@1234567\"`\n - `\"nomad/L1234567890\"`\n - `\"42\"`",
- "x-optimade-support": "must",
- "x-optimade-queryable": "must"
+ "x-optimade-queryable": "must",
+ "x-optimade-support": "must"
},
"type": {
"type": "string",
+ "enum": [
+ "links"
+ ],
+ "const": "links",
"pattern": "^links$",
"title": "Type",
"description": "These objects are described in detail in the section Links Endpoint",
"default": "links"
},
"links": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/ResourceLinks"
+ },
+ {
+ "type": "null"
}
],
- "title": "Links",
"description": "a links object containing links related to the resource."
},
"meta": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/Meta"
+ },
+ {
+ "type": "null"
}
],
- "title": "Meta",
"description": "a meta object containing non-standard meta-information about a resource that can not be represented as an attribute or relationship."
},
"attributes": {
@@ -1141,16 +1307,17 @@
"$ref": "#/components/schemas/LinksResourceAttributes"
}
],
- "title": "Attributes",
"description": "A dictionary containing key-value pairs representing the Links resource's properties."
},
"relationships": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/EntryRelationships"
+ },
+ {
+ "type": "null"
}
],
- "title": "Relationships",
"description": "A dictionary containing references to other entries according to the description in section Relationships encoded as [JSON API Relationships](https://jsonapi.org/format/1.0/#document-resource-object-relationships).\nThe OPTIONAL human-readable description of the relationship MAY be provided in the `description` field inside the `meta` dictionary of the JSON API resource identifier object."
}
},
@@ -1179,12 +1346,14 @@
"anyOf": [
{
"type": "string",
- "maxLength": 65536,
"minLength": 1,
"format": "uri"
},
{
"$ref": "#/components/schemas/Link"
+ },
+ {
+ "type": "null"
}
],
"title": "Base Url",
@@ -1194,12 +1363,14 @@
"anyOf": [
{
"type": "string",
- "maxLength": 65536,
"minLength": 1,
"format": "uri"
},
{
"$ref": "#/components/schemas/Link"
+ },
+ {
+ "type": "null"
}
],
"title": "Homepage",
@@ -1215,9 +1386,12 @@
"description": "The type of the linked relation.\nMUST be one of these values: 'child', 'root', 'external', 'providers'."
},
"aggregate": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/Aggregate"
+ },
+ {
+ "type": "null"
}
],
"title": "Aggregate",
@@ -1225,11 +1399,19 @@
"default": "ok"
},
"no_aggregate_reason": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "No Aggregate Reason",
"description": "An OPTIONAL human-readable string indicating the reason for suggesting not to aggregate results following the link.\nIt SHOULD NOT be present if `aggregate`=`ok`."
}
},
+ "additionalProperties": true,
"type": "object",
"required": [
"name",
@@ -1268,14 +1450,20 @@
"$ref": "#/components/schemas/ResponseMeta"
}
],
- "title": "Meta",
"description": "A meta object containing non-standard information"
},
"errors": {
- "items": {
- "$ref": "#/components/schemas/Error"
- },
- "type": "array",
+ "anyOf": [
+ {
+ "items": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
"uniqueItems": true,
"title": "Errors",
"description": "A list of unique errors"
@@ -1293,27 +1481,35 @@
"type": "object"
},
"type": "array"
+ },
+ {
+ "type": "null"
}
],
"uniqueItems": true,
- "title": "Included"
+ "title": "Included",
+ "description": "A list of unique included OPTIMADE entry resources."
},
"links": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/ToplevelLinks"
+ },
+ {
+ "type": "null"
}
],
- "title": "Links",
"description": "Links associated with the primary data or errors"
},
"jsonapi": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/JsonApi"
+ },
+ {
+ "type": "null"
}
],
- "title": "Jsonapi",
"description": "Information about the JSON API used"
}
},
@@ -1322,11 +1518,11 @@
"data",
"meta"
],
- "title": "LinksResponse",
- "description": "errors are not allowed"
+ "title": "LinksResponse"
},
"Meta": {
"properties": {},
+ "additionalProperties": true,
"type": "object",
"title": "Meta",
"description": "Non-standard meta-information that can not be represented as an attribute or relationship."
@@ -1334,31 +1530,61 @@
"OptimadeError": {
"properties": {
"id": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Id",
"description": "A unique identifier for this particular occurrence of the problem."
},
"links": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/ErrorLinks"
+ },
+ {
+ "type": "null"
}
],
- "title": "Links",
"description": "A links object storing about"
},
"status": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Status",
"description": "the HTTP status code applicable to this problem, expressed as a string value."
},
"code": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Code",
"description": "an application-specific error code, expressed as a string value."
},
"title": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Title",
"description": "A short, human-readable summary of the problem. It **SHOULD NOT** change from occurrence to occurrence of the problem, except for purposes of localization."
},
@@ -1368,21 +1594,25 @@
"description": "A human-readable explanation specific to this occurrence of the problem."
},
"source": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/ErrorSource"
+ },
+ {
+ "type": "null"
}
],
- "title": "Source",
"description": "An object containing references to the source of the error"
},
"meta": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/Meta"
+ },
+ {
+ "type": "null"
}
],
- "title": "Meta",
"description": "a meta object containing non-standard meta-information about the error."
}
},
@@ -1415,12 +1645,14 @@
"anyOf": [
{
"type": "string",
- "maxLength": 65536,
"minLength": 1,
"format": "uri"
},
{
"$ref": "#/components/schemas/Link"
+ },
+ {
+ "type": "null"
}
],
"title": "Homepage",
@@ -1439,12 +1671,14 @@
"ReferenceRelationship": {
"properties": {
"links": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/RelationshipLinks"
+ },
+ {
+ "type": "null"
}
],
- "title": "Links",
"description": "a links object containing at least one of the following: self, related"
},
"data": {
@@ -1457,6 +1691,9 @@
"$ref": "#/components/schemas/BaseRelationshipResource"
},
"type": "array"
+ },
+ {
+ "type": "null"
}
],
"uniqueItems": true,
@@ -1464,18 +1701,19 @@
"description": "Resource linkage"
},
"meta": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/Meta"
+ },
+ {
+ "type": "null"
}
],
- "title": "Meta",
"description": "a meta object that contains non-standard meta-information about the relationship."
}
},
"type": "object",
- "title": "ReferenceRelationship",
- "description": "Similar to normal JSON API relationship, but with addition of OPTIONAL meta field for a resource."
+ "title": "ReferenceRelationship"
},
"RelatedLinksResource": {
"properties": {
@@ -1486,7 +1724,10 @@
},
"type": {
"type": "string",
- "pattern": "^links$",
+ "enum": [
+ "links"
+ ],
+ "const": "links",
"title": "Type",
"default": "links"
}
@@ -1505,12 +1746,14 @@
"anyOf": [
{
"type": "string",
- "maxLength": 65536,
"minLength": 1,
"format": "uri"
},
{
"$ref": "#/components/schemas/Link"
+ },
+ {
+ "type": "null"
}
],
"title": "Self",
@@ -1520,12 +1763,14 @@
"anyOf": [
{
"type": "string",
- "maxLength": 65536,
"minLength": 1,
"format": "uri"
},
{
"$ref": "#/components/schemas/Link"
+ },
+ {
+ "type": "null"
}
],
"title": "Related",
@@ -1555,39 +1800,47 @@
"description": "Resource type"
},
"links": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/ResourceLinks"
+ },
+ {
+ "type": "null"
}
],
- "title": "Links",
"description": "a links object containing links related to the resource."
},
"meta": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/Meta"
+ },
+ {
+ "type": "null"
}
],
- "title": "Meta",
"description": "a meta object containing non-standard meta-information about a resource that can not be represented as an attribute or relationship."
},
"attributes": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/Attributes"
+ },
+ {
+ "type": "null"
}
],
- "title": "Attributes",
"description": "an attributes object representing some of the resource\u2019s data."
},
"relationships": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/Relationships"
+ },
+ {
+ "type": "null"
}
],
- "title": "Relationships",
"description": "[Relationships object](https://jsonapi.org/format/1.0/#document-resource-object-relationships)\ndescribing relationships between the resource and other JSON API resources."
}
},
@@ -1605,12 +1858,14 @@
"anyOf": [
{
"type": "string",
- "maxLength": 65536,
"minLength": 1,
"format": "uri"
},
{
"$ref": "#/components/schemas/Link"
+ },
+ {
+ "type": "null"
}
],
"title": "Self",
@@ -1629,7 +1884,6 @@
"$ref": "#/components/schemas/ResponseMetaQuery"
}
],
- "title": "Query",
"description": "Information on the Query that was requested"
},
"api_version": {
@@ -1637,7 +1891,7 @@
"pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$",
"title": "Api Version",
"description": "Presently used full version of the OPTIMADE API.\nThe version number string MUST NOT be prefixed by, e.g., \"v\".\nExamples: `1.0.0`, `1.0.0-rc.2`.",
- "example": [
+ "examples": [
"0.10.1",
"1.0.0-rc.2",
"1.2.3-rc.5+develop"
@@ -1652,72 +1906,121 @@
"anyOf": [
{
"type": "string",
- "maxLength": 65536,
"minLength": 1,
"format": "uri"
},
{
"$ref": "#/components/schemas/Link"
+ },
+ {
+ "type": "null"
}
],
"title": "Schema",
"description": "A [JSON API links object](http://jsonapi.org/format/1.0/#document-links) that points to a schema for the response.\nIf it is a string, or a dictionary containing no `meta` field, the provided URL MUST point at an [OpenAPI](https://swagger.io/specification/) schema.\nIt is possible that future versions of this specification allows for alternative schema types.\nHence, if the `meta` field of the JSON API links object is provided and contains a field `schema_type` that is not equal to the string `OpenAPI` the client MUST not handle failures to parse the schema or to validate the response against the schema as errors."
},
"time_stamp": {
- "type": "string",
- "format": "date-time",
+ "anyOf": [
+ {
+ "type": "string",
+ "format": "date-time"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Time Stamp",
"description": "A timestamp containing the date and time at which the query was executed."
},
"data_returned": {
- "type": "integer",
- "minimum": 0.0,
+ "anyOf": [
+ {
+ "type": "integer",
+ "minimum": 0.0
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Data Returned",
"description": "An integer containing the total number of data resource objects returned for the current `filter` query, independent of pagination."
},
"provider": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/Provider"
+ },
+ {
+ "type": "null"
}
],
- "title": "Provider",
"description": "information on the database provider of the implementation."
},
"data_available": {
- "type": "integer",
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Data Available",
"description": "An integer containing the total number of data resource objects available in the database for the endpoint."
},
"last_id": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Last Id",
"description": "a string containing the last ID returned"
},
"response_message": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Response Message",
"description": "response string from the server"
},
"implementation": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/Implementation"
+ },
+ {
+ "type": "null"
}
],
- "title": "Implementation",
"description": "a dictionary describing the server implementation"
},
"warnings": {
- "items": {
- "$ref": "#/components/schemas/Warnings"
- },
- "type": "array",
+ "anyOf": [
+ {
+ "items": {
+ "$ref": "#/components/schemas/Warnings"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
"uniqueItems": true,
"title": "Warnings",
"description": "A list of warning resource objects representing non-critical errors or warnings.\nA warning resource object is defined similarly to a [JSON API error object](http://jsonapi.org/format/1.0/#error-objects), but MUST also include the field `type`, which MUST have the value `\"warning\"`.\nThe field `detail` MUST be present and SHOULD contain a non-critical message, e.g., reporting unrecognized search attributes or deprecated features.\nThe field `status`, representing a HTTP response status code, MUST NOT be present for a warning resource object.\nThis is an exclusive field for error resource objects."
}
},
+ "additionalProperties": true,
"type": "object",
"required": [
"query",
@@ -1745,12 +2048,14 @@
"StructureRelationship": {
"properties": {
"links": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/RelationshipLinks"
+ },
+ {
+ "type": "null"
}
],
- "title": "Links",
"description": "a links object containing at least one of the following: self, related"
},
"data": {
@@ -1763,6 +2068,9 @@
"$ref": "#/components/schemas/BaseRelationshipResource"
},
"type": "array"
+ },
+ {
+ "type": "null"
}
],
"uniqueItems": true,
@@ -1770,18 +2078,19 @@
"description": "Resource linkage"
},
"meta": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/Meta"
+ },
+ {
+ "type": "null"
}
],
- "title": "Meta",
"description": "a meta object that contains non-standard meta-information about the relationship."
}
},
"type": "object",
- "title": "StructureRelationship",
- "description": "Similar to normal JSON API relationship, but with addition of OPTIONAL meta field for a resource."
+ "title": "StructureRelationship"
},
"ToplevelLinks": {
"properties": {
@@ -1789,12 +2098,14 @@
"anyOf": [
{
"type": "string",
- "maxLength": 65536,
"minLength": 1,
"format": "uri"
},
{
"$ref": "#/components/schemas/Link"
+ },
+ {
+ "type": "null"
}
],
"title": "Self",
@@ -1804,12 +2115,14 @@
"anyOf": [
{
"type": "string",
- "maxLength": 65536,
"minLength": 1,
"format": "uri"
},
{
"$ref": "#/components/schemas/Link"
+ },
+ {
+ "type": "null"
}
],
"title": "Related",
@@ -1819,12 +2132,14 @@
"anyOf": [
{
"type": "string",
- "maxLength": 65536,
"minLength": 1,
"format": "uri"
},
{
"$ref": "#/components/schemas/Link"
+ },
+ {
+ "type": "null"
}
],
"title": "First",
@@ -1834,12 +2149,14 @@
"anyOf": [
{
"type": "string",
- "maxLength": 65536,
"minLength": 1,
"format": "uri"
},
{
"$ref": "#/components/schemas/Link"
+ },
+ {
+ "type": "null"
}
],
"title": "Last",
@@ -1849,12 +2166,14 @@
"anyOf": [
{
"type": "string",
- "maxLength": 65536,
"minLength": 1,
"format": "uri"
},
{
"$ref": "#/components/schemas/Link"
+ },
+ {
+ "type": "null"
}
],
"title": "Prev",
@@ -1864,18 +2183,21 @@
"anyOf": [
{
"type": "string",
- "maxLength": 65536,
"minLength": 1,
"format": "uri"
},
{
"$ref": "#/components/schemas/Link"
+ },
+ {
+ "type": "null"
}
],
"title": "Next",
"description": "The next page of data"
}
},
+ "additionalProperties": true,
"type": "object",
"title": "ToplevelLinks",
"description": "A set of Links objects, possibly including pagination"
@@ -1883,26 +2205,49 @@
"Warnings": {
"properties": {
"id": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Id",
"description": "A unique identifier for this particular occurrence of the problem."
},
"links": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/ErrorLinks"
+ },
+ {
+ "type": "null"
}
],
- "title": "Links",
"description": "A links object storing about"
},
"code": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Code",
"description": "an application-specific error code, expressed as a string value."
},
"title": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Title",
"description": "A short, human-readable summary of the problem. It **SHOULD NOT** change from occurrence to occurrence of the problem, except for purposes of localization."
},
@@ -1912,25 +2257,33 @@
"description": "A human-readable explanation specific to this occurrence of the problem."
},
"source": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/ErrorSource"
+ },
+ {
+ "type": "null"
}
],
- "title": "Source",
"description": "An object containing references to the source of the error"
},
"meta": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/Meta"
+ },
+ {
+ "type": "null"
}
],
- "title": "Meta",
"description": "a meta object containing non-standard meta-information about the error."
},
"type": {
"type": "string",
+ "enum": [
+ "warning"
+ ],
+ "const": "warning",
"pattern": "^warning$",
"title": "Type",
"description": "Warnings must be of type \"warning\"",
diff --git a/openapi/openapi.json b/openapi/openapi.json
index d66e0fc52..13e9fada3 100644
--- a/openapi/openapi.json
+++ b/openapi/openapi.json
@@ -2,7 +2,7 @@
"openapi": "3.1.0",
"info": {
"title": "OPTIMADE API",
- "description": "The [Open Databases Integration for Materials Design (OPTIMADE) consortium](https://www.optimade.org/) aims to make materials databases interoperational by developing a common REST API.\n\nThis specification is generated using [`optimade-python-tools`](https://github.com/Materials-Consortia/optimade-python-tools/tree/v0.25.3) v0.25.3.",
+ "description": "The [Open Databases Integration for Materials Design (OPTIMADE) consortium](https://www.optimade.org/) aims to make materials databases interoperational by developing a common REST API.\n\nThis specification is generated using [`optimade-python-tools`](https://github.com/Materials-Consortia/optimade-python-tools/tree/v1.0.5) v1.0.5.",
"version": "1.1.0"
},
"paths": {
@@ -106,13 +106,13 @@
"operationId": "get_entry_info_info__entry__get",
"parameters": [
{
+ "name": "entry",
+ "in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Entry"
- },
- "name": "entry",
- "in": "path"
+ }
}
],
"responses": {
@@ -208,164 +208,164 @@
"operationId": "get_links_links_get",
"parameters": [
{
- "description": "A filter string, in the format described in section API Filtering Format Specification of the specification.",
+ "name": "filter",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
- "title": "Filter",
"description": "A filter string, in the format described in section API Filtering Format Specification of the specification.",
- "default": ""
+ "default": "",
+ "title": "Filter"
},
- "name": "filter",
- "in": "query"
+ "description": "A filter string, in the format described in section API Filtering Format Specification of the specification."
},
{
- "description": "The output format requested (see section Response Format).\nDefaults to the format string 'json', which specifies the standard output format described in this specification.\nExample: `http://example.com/v1/structures?response_format=xml`",
+ "name": "response_format",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
- "title": "Response Format",
"description": "The output format requested (see section Response Format).\nDefaults to the format string 'json', which specifies the standard output format described in this specification.\nExample: `http://example.com/v1/structures?response_format=xml`",
- "default": "json"
+ "default": "json",
+ "title": "Response Format"
},
- "name": "response_format",
- "in": "query"
+ "description": "The output format requested (see section Response Format).\nDefaults to the format string 'json', which specifies the standard output format described in this specification.\nExample: `http://example.com/v1/structures?response_format=xml`"
},
{
- "description": "An email address of the user making the request.\nThe email SHOULD be that of a person and not an automatic system.\nExample: `http://example.com/v1/structures?email_address=user@example.com`",
+ "name": "email_address",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
"format": "email",
- "title": "Email Address",
"description": "An email address of the user making the request.\nThe email SHOULD be that of a person and not an automatic system.\nExample: `http://example.com/v1/structures?email_address=user@example.com`",
- "default": ""
+ "default": "",
+ "title": "Email Address"
},
- "name": "email_address",
- "in": "query"
+ "description": "An email address of the user making the request.\nThe email SHOULD be that of a person and not an automatic system.\nExample: `http://example.com/v1/structures?email_address=user@example.com`"
},
{
- "description": "A comma-delimited set of fields to be provided in the output.\nIf provided, these fields MUST be returned along with the REQUIRED fields.\nOther OPTIONAL fields MUST NOT be returned when this parameter is present.\nExample: `http://example.com/v1/structures?response_fields=last_modified,nsites`",
+ "name": "response_fields",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
"pattern": "([a-z_][a-z_0-9]*(,[a-z_][a-z_0-9]*)*)?",
- "title": "Response Fields",
"description": "A comma-delimited set of fields to be provided in the output.\nIf provided, these fields MUST be returned along with the REQUIRED fields.\nOther OPTIONAL fields MUST NOT be returned when this parameter is present.\nExample: `http://example.com/v1/structures?response_fields=last_modified,nsites`",
- "default": ""
+ "default": "",
+ "title": "Response Fields"
},
- "name": "response_fields",
- "in": "query"
+ "description": "A comma-delimited set of fields to be provided in the output.\nIf provided, these fields MUST be returned along with the REQUIRED fields.\nOther OPTIONAL fields MUST NOT be returned when this parameter is present.\nExample: `http://example.com/v1/structures?response_fields=last_modified,nsites`"
},
{
- "description": "If supporting sortable queries, an implementation MUST use the `sort` query parameter with format as specified by [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-sorting).\n\nAn implementation MAY support multiple sort fields for a single query.\nIf it does, it again MUST conform to the JSON API 1.0 specification.\n\nIf an implementation supports sorting for an entry listing endpoint, then the `/info/` endpoint MUST include, for each field name `` in its `data.properties.` response value that can be used for sorting, the key `sortable` with value `true`.\nIf a field name under an entry listing endpoint supporting sorting cannot be used for sorting, the server MUST either leave out the `sortable` key or set it equal to `false` for the specific field name.\nThe set of field names, with `sortable` equal to `true` are allowed to be used in the \"sort fields\" list according to its definition in the JSON API 1.0 specification.\nThe field `sortable` is in addition to each property description and other OPTIONAL fields.\nAn example is shown in the section Entry Listing Info Endpoints.",
+ "name": "sort",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
"pattern": "([a-z_][a-z_0-9]*(,[a-z_][a-z_0-9]*)*)?",
- "title": "Sort",
"description": "If supporting sortable queries, an implementation MUST use the `sort` query parameter with format as specified by [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-sorting).\n\nAn implementation MAY support multiple sort fields for a single query.\nIf it does, it again MUST conform to the JSON API 1.0 specification.\n\nIf an implementation supports sorting for an entry listing endpoint, then the `/info/` endpoint MUST include, for each field name `` in its `data.properties.` response value that can be used for sorting, the key `sortable` with value `true`.\nIf a field name under an entry listing endpoint supporting sorting cannot be used for sorting, the server MUST either leave out the `sortable` key or set it equal to `false` for the specific field name.\nThe set of field names, with `sortable` equal to `true` are allowed to be used in the \"sort fields\" list according to its definition in the JSON API 1.0 specification.\nThe field `sortable` is in addition to each property description and other OPTIONAL fields.\nAn example is shown in the section Entry Listing Info Endpoints.",
- "default": ""
+ "default": "",
+ "title": "Sort"
},
- "name": "sort",
- "in": "query"
+ "description": "If supporting sortable queries, an implementation MUST use the `sort` query parameter with format as specified by [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-sorting).\n\nAn implementation MAY support multiple sort fields for a single query.\nIf it does, it again MUST conform to the JSON API 1.0 specification.\n\nIf an implementation supports sorting for an entry listing endpoint, then the `/info/` endpoint MUST include, for each field name `` in its `data.properties.` response value that can be used for sorting, the key `sortable` with value `true`.\nIf a field name under an entry listing endpoint supporting sorting cannot be used for sorting, the server MUST either leave out the `sortable` key or set it equal to `false` for the specific field name.\nThe set of field names, with `sortable` equal to `true` are allowed to be used in the \"sort fields\" list according to its definition in the JSON API 1.0 specification.\nThe field `sortable` is in addition to each property description and other OPTIONAL fields.\nAn example is shown in the section Entry Listing Info Endpoints."
},
{
- "description": "Sets a numerical limit on the number of entries returned.\nSee [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-pagination).\nThe API implementation MUST return no more than the number specified.\nIt MAY return fewer.\nThe database MAY have a maximum limit and not accept larger numbers (in which case an error code -- 403 Forbidden -- MUST be returned).\nThe default limit value is up to the API implementation to decide.\nExample: `http://example.com/optimade/v1/structures?page_limit=100`",
+ "name": "page_limit",
+ "in": "query",
"required": false,
"schema": {
"type": "integer",
- "minimum": 0.0,
- "title": "Page Limit",
+ "minimum": 0,
"description": "Sets a numerical limit on the number of entries returned.\nSee [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-pagination).\nThe API implementation MUST return no more than the number specified.\nIt MAY return fewer.\nThe database MAY have a maximum limit and not accept larger numbers (in which case an error code -- 403 Forbidden -- MUST be returned).\nThe default limit value is up to the API implementation to decide.\nExample: `http://example.com/optimade/v1/structures?page_limit=100`",
- "default": 20
+ "default": 20,
+ "title": "Page Limit"
},
- "name": "page_limit",
- "in": "query"
+ "description": "Sets a numerical limit on the number of entries returned.\nSee [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-pagination).\nThe API implementation MUST return no more than the number specified.\nIt MAY return fewer.\nThe database MAY have a maximum limit and not accept larger numbers (in which case an error code -- 403 Forbidden -- MUST be returned).\nThe default limit value is up to the API implementation to decide.\nExample: `http://example.com/optimade/v1/structures?page_limit=100`"
},
{
- "description": "RECOMMENDED for use with _offset-based_ pagination: using `page_offset` and `page_limit` is RECOMMENDED.\nExample: Skip 50 structures and fetch up to 100: `/structures?page_offset=50&page_limit=100`.",
+ "name": "page_offset",
+ "in": "query",
"required": false,
"schema": {
"type": "integer",
- "minimum": 0.0,
- "title": "Page Offset",
+ "minimum": 0,
"description": "RECOMMENDED for use with _offset-based_ pagination: using `page_offset` and `page_limit` is RECOMMENDED.\nExample: Skip 50 structures and fetch up to 100: `/structures?page_offset=50&page_limit=100`.",
- "default": 0
+ "default": 0,
+ "title": "Page Offset"
},
- "name": "page_offset",
- "in": "query"
+ "description": "RECOMMENDED for use with _offset-based_ pagination: using `page_offset` and `page_limit` is RECOMMENDED.\nExample: Skip 50 structures and fetch up to 100: `/structures?page_offset=50&page_limit=100`."
},
{
- "description": "RECOMMENDED for use with _page-based_ pagination: using `page_number` and `page_limit` is RECOMMENDED.\nIt is RECOMMENDED that the first page has number 1, i.e., that `page_number` is 1-based.\nExample: Fetch page 2 of up to 50 structures per page: `/structures?page_number=2&page_limit=50`.",
+ "name": "page_number",
+ "in": "query",
"required": false,
"schema": {
"type": "integer",
- "title": "Page Number",
- "description": "RECOMMENDED for use with _page-based_ pagination: using `page_number` and `page_limit` is RECOMMENDED.\nIt is RECOMMENDED that the first page has number 1, i.e., that `page_number` is 1-based.\nExample: Fetch page 2 of up to 50 structures per page: `/structures?page_number=2&page_limit=50`."
+ "description": "RECOMMENDED for use with _page-based_ pagination: using `page_number` and `page_limit` is RECOMMENDED.\nIt is RECOMMENDED that the first page has number 1, i.e., that `page_number` is 1-based.\nExample: Fetch page 2 of up to 50 structures per page: `/structures?page_number=2&page_limit=50`.",
+ "title": "Page Number"
},
- "name": "page_number",
- "in": "query"
+ "description": "RECOMMENDED for use with _page-based_ pagination: using `page_number` and `page_limit` is RECOMMENDED.\nIt is RECOMMENDED that the first page has number 1, i.e., that `page_number` is 1-based.\nExample: Fetch page 2 of up to 50 structures per page: `/structures?page_number=2&page_limit=50`."
},
{
- "description": "RECOMMENDED for use with _cursor-based_ pagination: using `page_cursor` and `page_limit` is RECOMMENDED.",
+ "name": "page_cursor",
+ "in": "query",
"required": false,
"schema": {
"type": "integer",
- "minimum": 0.0,
- "title": "Page Cursor",
+ "minimum": 0,
"description": "RECOMMENDED for use with _cursor-based_ pagination: using `page_cursor` and `page_limit` is RECOMMENDED.",
- "default": 0
+ "default": 0,
+ "title": "Page Cursor"
},
- "name": "page_cursor",
- "in": "query"
+ "description": "RECOMMENDED for use with _cursor-based_ pagination: using `page_cursor` and `page_limit` is RECOMMENDED."
},
{
- "description": "RECOMMENDED for use with _value-based_ pagination: using `page_above`/`page_below` and `page_limit` is RECOMMENDED.\nExample: Fetch up to 100 structures above sort-field value 4000 (in this example, server chooses to fetch results sorted by increasing `id`, so `page_above` value refers to an `id` value): `/structures?page_above=4000&page_limit=100`.",
+ "name": "page_above",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
- "title": "Page Above",
- "description": "RECOMMENDED for use with _value-based_ pagination: using `page_above`/`page_below` and `page_limit` is RECOMMENDED.\nExample: Fetch up to 100 structures above sort-field value 4000 (in this example, server chooses to fetch results sorted by increasing `id`, so `page_above` value refers to an `id` value): `/structures?page_above=4000&page_limit=100`."
+ "description": "RECOMMENDED for use with _value-based_ pagination: using `page_above`/`page_below` and `page_limit` is RECOMMENDED.\nExample: Fetch up to 100 structures above sort-field value 4000 (in this example, server chooses to fetch results sorted by increasing `id`, so `page_above` value refers to an `id` value): `/structures?page_above=4000&page_limit=100`.",
+ "title": "Page Above"
},
- "name": "page_above",
- "in": "query"
+ "description": "RECOMMENDED for use with _value-based_ pagination: using `page_above`/`page_below` and `page_limit` is RECOMMENDED.\nExample: Fetch up to 100 structures above sort-field value 4000 (in this example, server chooses to fetch results sorted by increasing `id`, so `page_above` value refers to an `id` value): `/structures?page_above=4000&page_limit=100`."
},
{
- "description": "RECOMMENDED for use with _value-based_ pagination: using `page_above`/`page_below` and `page_limit` is RECOMMENDED.",
+ "name": "page_below",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
- "title": "Page Below",
- "description": "RECOMMENDED for use with _value-based_ pagination: using `page_above`/`page_below` and `page_limit` is RECOMMENDED."
+ "description": "RECOMMENDED for use with _value-based_ pagination: using `page_above`/`page_below` and `page_limit` is RECOMMENDED.",
+ "title": "Page Below"
},
- "name": "page_below",
- "in": "query"
+ "description": "RECOMMENDED for use with _value-based_ pagination: using `page_above`/`page_below` and `page_limit` is RECOMMENDED."
},
{
- "description": "A server MAY implement the JSON API concept of returning [compound documents](https://jsonapi.org/format/1.0/#document-compound-documents) by utilizing the `include` query parameter as specified by [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-includes).\n\nAll related resource objects MUST be returned as part of an array value for the top-level `included` field, see the section JSON Response Schema: Common Fields.\n\nThe value of `include` MUST be a comma-separated list of \"relationship paths\", as defined in the [JSON API](https://jsonapi.org/format/1.0/#fetching-includes).\nIf relationship paths are not supported, or a server is unable to identify a relationship path a `400 Bad Request` response MUST be made.\n\nThe **default value** for `include` is `references`.\nThis means `references` entries MUST always be included under the top-level field `included` as default, since a server assumes if `include` is not specified by a client in the request, it is still specified as `include=references`.\nNote, if a client explicitly specifies `include` and leaves out `references`, `references` resource objects MUST NOT be included under the top-level field `included`, as per the definition of `included`, see section JSON Response Schema: Common Fields.\n\n> **Note**: A query with the parameter `include` set to the empty string means no related resource objects are to be returned under the top-level field `included`.",
+ "name": "include",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
- "title": "Include",
"description": "A server MAY implement the JSON API concept of returning [compound documents](https://jsonapi.org/format/1.0/#document-compound-documents) by utilizing the `include` query parameter as specified by [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-includes).\n\nAll related resource objects MUST be returned as part of an array value for the top-level `included` field, see the section JSON Response Schema: Common Fields.\n\nThe value of `include` MUST be a comma-separated list of \"relationship paths\", as defined in the [JSON API](https://jsonapi.org/format/1.0/#fetching-includes).\nIf relationship paths are not supported, or a server is unable to identify a relationship path a `400 Bad Request` response MUST be made.\n\nThe **default value** for `include` is `references`.\nThis means `references` entries MUST always be included under the top-level field `included` as default, since a server assumes if `include` is not specified by a client in the request, it is still specified as `include=references`.\nNote, if a client explicitly specifies `include` and leaves out `references`, `references` resource objects MUST NOT be included under the top-level field `included`, as per the definition of `included`, see section JSON Response Schema: Common Fields.\n\n> **Note**: A query with the parameter `include` set to the empty string means no related resource objects are to be returned under the top-level field `included`.",
- "default": "references"
+ "default": "references",
+ "title": "Include"
},
- "name": "include",
- "in": "query"
+ "description": "A server MAY implement the JSON API concept of returning [compound documents](https://jsonapi.org/format/1.0/#document-compound-documents) by utilizing the `include` query parameter as specified by [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-includes).\n\nAll related resource objects MUST be returned as part of an array value for the top-level `included` field, see the section JSON Response Schema: Common Fields.\n\nThe value of `include` MUST be a comma-separated list of \"relationship paths\", as defined in the [JSON API](https://jsonapi.org/format/1.0/#fetching-includes).\nIf relationship paths are not supported, or a server is unable to identify a relationship path a `400 Bad Request` response MUST be made.\n\nThe **default value** for `include` is `references`.\nThis means `references` entries MUST always be included under the top-level field `included` as default, since a server assumes if `include` is not specified by a client in the request, it is still specified as `include=references`.\nNote, if a client explicitly specifies `include` and leaves out `references`, `references` resource objects MUST NOT be included under the top-level field `included`, as per the definition of `included`, see section JSON Response Schema: Common Fields.\n\n> **Note**: A query with the parameter `include` set to the empty string means no related resource objects are to be returned under the top-level field `included`."
},
{
- "description": "If the client provides the parameter, the value SHOULD have the format `vMAJOR` or `vMAJOR.MINOR`, where MAJOR is a major version and MINOR is a minor version of the API. For example, if a client appends `api_hint=v1.0` to the query string, the hint provided is for major version 1 and minor version 0.",
+ "name": "api_hint",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
"pattern": "(v[0-9]+(\\.[0-9]+)?)?",
- "title": "Api Hint",
"description": "If the client provides the parameter, the value SHOULD have the format `vMAJOR` or `vMAJOR.MINOR`, where MAJOR is a major version and MINOR is a minor version of the API. For example, if a client appends `api_hint=v1.0` to the query string, the hint provided is for major version 1 and minor version 0.",
- "default": ""
+ "default": "",
+ "title": "Api Hint"
},
- "name": "api_hint",
- "in": "query"
+ "description": "If the client provides the parameter, the value SHOULD have the format `vMAJOR` or `vMAJOR.MINOR`, where MAJOR is a major version and MINOR is a minor version of the API. For example, if a client appends `api_hint=v1.0` to the query string, the hint provided is for major version 1 and minor version 0."
}
],
"responses": {
@@ -461,164 +461,164 @@
"operationId": "get_references_references_get",
"parameters": [
{
- "description": "A filter string, in the format described in section API Filtering Format Specification of the specification.",
+ "name": "filter",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
- "title": "Filter",
"description": "A filter string, in the format described in section API Filtering Format Specification of the specification.",
- "default": ""
+ "default": "",
+ "title": "Filter"
},
- "name": "filter",
- "in": "query"
+ "description": "A filter string, in the format described in section API Filtering Format Specification of the specification."
},
{
- "description": "The output format requested (see section Response Format).\nDefaults to the format string 'json', which specifies the standard output format described in this specification.\nExample: `http://example.com/v1/structures?response_format=xml`",
+ "name": "response_format",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
- "title": "Response Format",
"description": "The output format requested (see section Response Format).\nDefaults to the format string 'json', which specifies the standard output format described in this specification.\nExample: `http://example.com/v1/structures?response_format=xml`",
- "default": "json"
+ "default": "json",
+ "title": "Response Format"
},
- "name": "response_format",
- "in": "query"
+ "description": "The output format requested (see section Response Format).\nDefaults to the format string 'json', which specifies the standard output format described in this specification.\nExample: `http://example.com/v1/structures?response_format=xml`"
},
{
- "description": "An email address of the user making the request.\nThe email SHOULD be that of a person and not an automatic system.\nExample: `http://example.com/v1/structures?email_address=user@example.com`",
+ "name": "email_address",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
"format": "email",
- "title": "Email Address",
"description": "An email address of the user making the request.\nThe email SHOULD be that of a person and not an automatic system.\nExample: `http://example.com/v1/structures?email_address=user@example.com`",
- "default": ""
+ "default": "",
+ "title": "Email Address"
},
- "name": "email_address",
- "in": "query"
+ "description": "An email address of the user making the request.\nThe email SHOULD be that of a person and not an automatic system.\nExample: `http://example.com/v1/structures?email_address=user@example.com`"
},
{
- "description": "A comma-delimited set of fields to be provided in the output.\nIf provided, these fields MUST be returned along with the REQUIRED fields.\nOther OPTIONAL fields MUST NOT be returned when this parameter is present.\nExample: `http://example.com/v1/structures?response_fields=last_modified,nsites`",
+ "name": "response_fields",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
"pattern": "([a-z_][a-z_0-9]*(,[a-z_][a-z_0-9]*)*)?",
- "title": "Response Fields",
"description": "A comma-delimited set of fields to be provided in the output.\nIf provided, these fields MUST be returned along with the REQUIRED fields.\nOther OPTIONAL fields MUST NOT be returned when this parameter is present.\nExample: `http://example.com/v1/structures?response_fields=last_modified,nsites`",
- "default": ""
+ "default": "",
+ "title": "Response Fields"
},
- "name": "response_fields",
- "in": "query"
+ "description": "A comma-delimited set of fields to be provided in the output.\nIf provided, these fields MUST be returned along with the REQUIRED fields.\nOther OPTIONAL fields MUST NOT be returned when this parameter is present.\nExample: `http://example.com/v1/structures?response_fields=last_modified,nsites`"
},
{
- "description": "If supporting sortable queries, an implementation MUST use the `sort` query parameter with format as specified by [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-sorting).\n\nAn implementation MAY support multiple sort fields for a single query.\nIf it does, it again MUST conform to the JSON API 1.0 specification.\n\nIf an implementation supports sorting for an entry listing endpoint, then the `/info/` endpoint MUST include, for each field name `` in its `data.properties.` response value that can be used for sorting, the key `sortable` with value `true`.\nIf a field name under an entry listing endpoint supporting sorting cannot be used for sorting, the server MUST either leave out the `sortable` key or set it equal to `false` for the specific field name.\nThe set of field names, with `sortable` equal to `true` are allowed to be used in the \"sort fields\" list according to its definition in the JSON API 1.0 specification.\nThe field `sortable` is in addition to each property description and other OPTIONAL fields.\nAn example is shown in the section Entry Listing Info Endpoints.",
+ "name": "sort",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
"pattern": "([a-z_][a-z_0-9]*(,[a-z_][a-z_0-9]*)*)?",
- "title": "Sort",
"description": "If supporting sortable queries, an implementation MUST use the `sort` query parameter with format as specified by [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-sorting).\n\nAn implementation MAY support multiple sort fields for a single query.\nIf it does, it again MUST conform to the JSON API 1.0 specification.\n\nIf an implementation supports sorting for an entry listing endpoint, then the `/info/` endpoint MUST include, for each field name `` in its `data.properties.` response value that can be used for sorting, the key `sortable` with value `true`.\nIf a field name under an entry listing endpoint supporting sorting cannot be used for sorting, the server MUST either leave out the `sortable` key or set it equal to `false` for the specific field name.\nThe set of field names, with `sortable` equal to `true` are allowed to be used in the \"sort fields\" list according to its definition in the JSON API 1.0 specification.\nThe field `sortable` is in addition to each property description and other OPTIONAL fields.\nAn example is shown in the section Entry Listing Info Endpoints.",
- "default": ""
+ "default": "",
+ "title": "Sort"
},
- "name": "sort",
- "in": "query"
+ "description": "If supporting sortable queries, an implementation MUST use the `sort` query parameter with format as specified by [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-sorting).\n\nAn implementation MAY support multiple sort fields for a single query.\nIf it does, it again MUST conform to the JSON API 1.0 specification.\n\nIf an implementation supports sorting for an entry listing endpoint, then the `/info/` endpoint MUST include, for each field name `` in its `data.properties.` response value that can be used for sorting, the key `sortable` with value `true`.\nIf a field name under an entry listing endpoint supporting sorting cannot be used for sorting, the server MUST either leave out the `sortable` key or set it equal to `false` for the specific field name.\nThe set of field names, with `sortable` equal to `true` are allowed to be used in the \"sort fields\" list according to its definition in the JSON API 1.0 specification.\nThe field `sortable` is in addition to each property description and other OPTIONAL fields.\nAn example is shown in the section Entry Listing Info Endpoints."
},
{
- "description": "Sets a numerical limit on the number of entries returned.\nSee [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-pagination).\nThe API implementation MUST return no more than the number specified.\nIt MAY return fewer.\nThe database MAY have a maximum limit and not accept larger numbers (in which case an error code -- 403 Forbidden -- MUST be returned).\nThe default limit value is up to the API implementation to decide.\nExample: `http://example.com/optimade/v1/structures?page_limit=100`",
+ "name": "page_limit",
+ "in": "query",
"required": false,
"schema": {
"type": "integer",
- "minimum": 0.0,
- "title": "Page Limit",
+ "minimum": 0,
"description": "Sets a numerical limit on the number of entries returned.\nSee [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-pagination).\nThe API implementation MUST return no more than the number specified.\nIt MAY return fewer.\nThe database MAY have a maximum limit and not accept larger numbers (in which case an error code -- 403 Forbidden -- MUST be returned).\nThe default limit value is up to the API implementation to decide.\nExample: `http://example.com/optimade/v1/structures?page_limit=100`",
- "default": 20
+ "default": 20,
+ "title": "Page Limit"
},
- "name": "page_limit",
- "in": "query"
+ "description": "Sets a numerical limit on the number of entries returned.\nSee [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-pagination).\nThe API implementation MUST return no more than the number specified.\nIt MAY return fewer.\nThe database MAY have a maximum limit and not accept larger numbers (in which case an error code -- 403 Forbidden -- MUST be returned).\nThe default limit value is up to the API implementation to decide.\nExample: `http://example.com/optimade/v1/structures?page_limit=100`"
},
{
- "description": "RECOMMENDED for use with _offset-based_ pagination: using `page_offset` and `page_limit` is RECOMMENDED.\nExample: Skip 50 structures and fetch up to 100: `/structures?page_offset=50&page_limit=100`.",
+ "name": "page_offset",
+ "in": "query",
"required": false,
"schema": {
"type": "integer",
- "minimum": 0.0,
- "title": "Page Offset",
+ "minimum": 0,
"description": "RECOMMENDED for use with _offset-based_ pagination: using `page_offset` and `page_limit` is RECOMMENDED.\nExample: Skip 50 structures and fetch up to 100: `/structures?page_offset=50&page_limit=100`.",
- "default": 0
+ "default": 0,
+ "title": "Page Offset"
},
- "name": "page_offset",
- "in": "query"
+ "description": "RECOMMENDED for use with _offset-based_ pagination: using `page_offset` and `page_limit` is RECOMMENDED.\nExample: Skip 50 structures and fetch up to 100: `/structures?page_offset=50&page_limit=100`."
},
{
- "description": "RECOMMENDED for use with _page-based_ pagination: using `page_number` and `page_limit` is RECOMMENDED.\nIt is RECOMMENDED that the first page has number 1, i.e., that `page_number` is 1-based.\nExample: Fetch page 2 of up to 50 structures per page: `/structures?page_number=2&page_limit=50`.",
+ "name": "page_number",
+ "in": "query",
"required": false,
"schema": {
"type": "integer",
- "title": "Page Number",
- "description": "RECOMMENDED for use with _page-based_ pagination: using `page_number` and `page_limit` is RECOMMENDED.\nIt is RECOMMENDED that the first page has number 1, i.e., that `page_number` is 1-based.\nExample: Fetch page 2 of up to 50 structures per page: `/structures?page_number=2&page_limit=50`."
+ "description": "RECOMMENDED for use with _page-based_ pagination: using `page_number` and `page_limit` is RECOMMENDED.\nIt is RECOMMENDED that the first page has number 1, i.e., that `page_number` is 1-based.\nExample: Fetch page 2 of up to 50 structures per page: `/structures?page_number=2&page_limit=50`.",
+ "title": "Page Number"
},
- "name": "page_number",
- "in": "query"
+ "description": "RECOMMENDED for use with _page-based_ pagination: using `page_number` and `page_limit` is RECOMMENDED.\nIt is RECOMMENDED that the first page has number 1, i.e., that `page_number` is 1-based.\nExample: Fetch page 2 of up to 50 structures per page: `/structures?page_number=2&page_limit=50`."
},
{
- "description": "RECOMMENDED for use with _cursor-based_ pagination: using `page_cursor` and `page_limit` is RECOMMENDED.",
+ "name": "page_cursor",
+ "in": "query",
"required": false,
"schema": {
"type": "integer",
- "minimum": 0.0,
- "title": "Page Cursor",
+ "minimum": 0,
"description": "RECOMMENDED for use with _cursor-based_ pagination: using `page_cursor` and `page_limit` is RECOMMENDED.",
- "default": 0
+ "default": 0,
+ "title": "Page Cursor"
},
- "name": "page_cursor",
- "in": "query"
+ "description": "RECOMMENDED for use with _cursor-based_ pagination: using `page_cursor` and `page_limit` is RECOMMENDED."
},
{
- "description": "RECOMMENDED for use with _value-based_ pagination: using `page_above`/`page_below` and `page_limit` is RECOMMENDED.\nExample: Fetch up to 100 structures above sort-field value 4000 (in this example, server chooses to fetch results sorted by increasing `id`, so `page_above` value refers to an `id` value): `/structures?page_above=4000&page_limit=100`.",
+ "name": "page_above",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
- "title": "Page Above",
- "description": "RECOMMENDED for use with _value-based_ pagination: using `page_above`/`page_below` and `page_limit` is RECOMMENDED.\nExample: Fetch up to 100 structures above sort-field value 4000 (in this example, server chooses to fetch results sorted by increasing `id`, so `page_above` value refers to an `id` value): `/structures?page_above=4000&page_limit=100`."
+ "description": "RECOMMENDED for use with _value-based_ pagination: using `page_above`/`page_below` and `page_limit` is RECOMMENDED.\nExample: Fetch up to 100 structures above sort-field value 4000 (in this example, server chooses to fetch results sorted by increasing `id`, so `page_above` value refers to an `id` value): `/structures?page_above=4000&page_limit=100`.",
+ "title": "Page Above"
},
- "name": "page_above",
- "in": "query"
+ "description": "RECOMMENDED for use with _value-based_ pagination: using `page_above`/`page_below` and `page_limit` is RECOMMENDED.\nExample: Fetch up to 100 structures above sort-field value 4000 (in this example, server chooses to fetch results sorted by increasing `id`, so `page_above` value refers to an `id` value): `/structures?page_above=4000&page_limit=100`."
},
{
- "description": "RECOMMENDED for use with _value-based_ pagination: using `page_above`/`page_below` and `page_limit` is RECOMMENDED.",
+ "name": "page_below",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
- "title": "Page Below",
- "description": "RECOMMENDED for use with _value-based_ pagination: using `page_above`/`page_below` and `page_limit` is RECOMMENDED."
+ "description": "RECOMMENDED for use with _value-based_ pagination: using `page_above`/`page_below` and `page_limit` is RECOMMENDED.",
+ "title": "Page Below"
},
- "name": "page_below",
- "in": "query"
+ "description": "RECOMMENDED for use with _value-based_ pagination: using `page_above`/`page_below` and `page_limit` is RECOMMENDED."
},
{
- "description": "A server MAY implement the JSON API concept of returning [compound documents](https://jsonapi.org/format/1.0/#document-compound-documents) by utilizing the `include` query parameter as specified by [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-includes).\n\nAll related resource objects MUST be returned as part of an array value for the top-level `included` field, see the section JSON Response Schema: Common Fields.\n\nThe value of `include` MUST be a comma-separated list of \"relationship paths\", as defined in the [JSON API](https://jsonapi.org/format/1.0/#fetching-includes).\nIf relationship paths are not supported, or a server is unable to identify a relationship path a `400 Bad Request` response MUST be made.\n\nThe **default value** for `include` is `references`.\nThis means `references` entries MUST always be included under the top-level field `included` as default, since a server assumes if `include` is not specified by a client in the request, it is still specified as `include=references`.\nNote, if a client explicitly specifies `include` and leaves out `references`, `references` resource objects MUST NOT be included under the top-level field `included`, as per the definition of `included`, see section JSON Response Schema: Common Fields.\n\n> **Note**: A query with the parameter `include` set to the empty string means no related resource objects are to be returned under the top-level field `included`.",
+ "name": "include",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
- "title": "Include",
"description": "A server MAY implement the JSON API concept of returning [compound documents](https://jsonapi.org/format/1.0/#document-compound-documents) by utilizing the `include` query parameter as specified by [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-includes).\n\nAll related resource objects MUST be returned as part of an array value for the top-level `included` field, see the section JSON Response Schema: Common Fields.\n\nThe value of `include` MUST be a comma-separated list of \"relationship paths\", as defined in the [JSON API](https://jsonapi.org/format/1.0/#fetching-includes).\nIf relationship paths are not supported, or a server is unable to identify a relationship path a `400 Bad Request` response MUST be made.\n\nThe **default value** for `include` is `references`.\nThis means `references` entries MUST always be included under the top-level field `included` as default, since a server assumes if `include` is not specified by a client in the request, it is still specified as `include=references`.\nNote, if a client explicitly specifies `include` and leaves out `references`, `references` resource objects MUST NOT be included under the top-level field `included`, as per the definition of `included`, see section JSON Response Schema: Common Fields.\n\n> **Note**: A query with the parameter `include` set to the empty string means no related resource objects are to be returned under the top-level field `included`.",
- "default": "references"
+ "default": "references",
+ "title": "Include"
},
- "name": "include",
- "in": "query"
+ "description": "A server MAY implement the JSON API concept of returning [compound documents](https://jsonapi.org/format/1.0/#document-compound-documents) by utilizing the `include` query parameter as specified by [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-includes).\n\nAll related resource objects MUST be returned as part of an array value for the top-level `included` field, see the section JSON Response Schema: Common Fields.\n\nThe value of `include` MUST be a comma-separated list of \"relationship paths\", as defined in the [JSON API](https://jsonapi.org/format/1.0/#fetching-includes).\nIf relationship paths are not supported, or a server is unable to identify a relationship path a `400 Bad Request` response MUST be made.\n\nThe **default value** for `include` is `references`.\nThis means `references` entries MUST always be included under the top-level field `included` as default, since a server assumes if `include` is not specified by a client in the request, it is still specified as `include=references`.\nNote, if a client explicitly specifies `include` and leaves out `references`, `references` resource objects MUST NOT be included under the top-level field `included`, as per the definition of `included`, see section JSON Response Schema: Common Fields.\n\n> **Note**: A query with the parameter `include` set to the empty string means no related resource objects are to be returned under the top-level field `included`."
},
{
- "description": "If the client provides the parameter, the value SHOULD have the format `vMAJOR` or `vMAJOR.MINOR`, where MAJOR is a major version and MINOR is a minor version of the API. For example, if a client appends `api_hint=v1.0` to the query string, the hint provided is for major version 1 and minor version 0.",
+ "name": "api_hint",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
"pattern": "(v[0-9]+(\\.[0-9]+)?)?",
- "title": "Api Hint",
"description": "If the client provides the parameter, the value SHOULD have the format `vMAJOR` or `vMAJOR.MINOR`, where MAJOR is a major version and MINOR is a minor version of the API. For example, if a client appends `api_hint=v1.0` to the query string, the hint provided is for major version 1 and minor version 0.",
- "default": ""
+ "default": "",
+ "title": "Api Hint"
},
- "name": "api_hint",
- "in": "query"
+ "description": "If the client provides the parameter, the value SHOULD have the format `vMAJOR` or `vMAJOR.MINOR`, where MAJOR is a major version and MINOR is a minor version of the API. For example, if a client appends `api_hint=v1.0` to the query string, the hint provided is for major version 1 and minor version 0."
}
],
"responses": {
@@ -714,76 +714,76 @@
"operationId": "get_single_reference_references__entry_id__get",
"parameters": [
{
+ "name": "entry_id",
+ "in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Entry Id"
- },
- "name": "entry_id",
- "in": "path"
+ }
},
{
- "description": "The output format requested (see section Response Format).\nDefaults to the format string 'json', which specifies the standard output format described in this specification.\nExample: `http://example.com/v1/structures?response_format=xml`",
+ "name": "response_format",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
- "title": "Response Format",
"description": "The output format requested (see section Response Format).\nDefaults to the format string 'json', which specifies the standard output format described in this specification.\nExample: `http://example.com/v1/structures?response_format=xml`",
- "default": "json"
+ "default": "json",
+ "title": "Response Format"
},
- "name": "response_format",
- "in": "query"
+ "description": "The output format requested (see section Response Format).\nDefaults to the format string 'json', which specifies the standard output format described in this specification.\nExample: `http://example.com/v1/structures?response_format=xml`"
},
{
- "description": "An email address of the user making the request.\nThe email SHOULD be that of a person and not an automatic system.\nExample: `http://example.com/v1/structures?email_address=user@example.com`",
+ "name": "email_address",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
"format": "email",
- "title": "Email Address",
"description": "An email address of the user making the request.\nThe email SHOULD be that of a person and not an automatic system.\nExample: `http://example.com/v1/structures?email_address=user@example.com`",
- "default": ""
+ "default": "",
+ "title": "Email Address"
},
- "name": "email_address",
- "in": "query"
+ "description": "An email address of the user making the request.\nThe email SHOULD be that of a person and not an automatic system.\nExample: `http://example.com/v1/structures?email_address=user@example.com`"
},
{
- "description": "A comma-delimited set of fields to be provided in the output.\nIf provided, these fields MUST be returned along with the REQUIRED fields.\nOther OPTIONAL fields MUST NOT be returned when this parameter is present.\nExample: `http://example.com/v1/structures?response_fields=last_modified,nsites`",
+ "name": "response_fields",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
"pattern": "([a-z_][a-z_0-9]*(,[a-z_][a-z_0-9]*)*)?",
- "title": "Response Fields",
"description": "A comma-delimited set of fields to be provided in the output.\nIf provided, these fields MUST be returned along with the REQUIRED fields.\nOther OPTIONAL fields MUST NOT be returned when this parameter is present.\nExample: `http://example.com/v1/structures?response_fields=last_modified,nsites`",
- "default": ""
+ "default": "",
+ "title": "Response Fields"
},
- "name": "response_fields",
- "in": "query"
+ "description": "A comma-delimited set of fields to be provided in the output.\nIf provided, these fields MUST be returned along with the REQUIRED fields.\nOther OPTIONAL fields MUST NOT be returned when this parameter is present.\nExample: `http://example.com/v1/structures?response_fields=last_modified,nsites`"
},
{
- "description": "A server MAY implement the JSON API concept of returning [compound documents](https://jsonapi.org/format/1.0/#document-compound-documents) by utilizing the `include` query parameter as specified by [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-includes).\n\nAll related resource objects MUST be returned as part of an array value for the top-level `included` field, see the section JSON Response Schema: Common Fields.\n\nThe value of `include` MUST be a comma-separated list of \"relationship paths\", as defined in the [JSON API](https://jsonapi.org/format/1.0/#fetching-includes).\nIf relationship paths are not supported, or a server is unable to identify a relationship path a `400 Bad Request` response MUST be made.\n\nThe **default value** for `include` is `references`.\nThis means `references` entries MUST always be included under the top-level field `included` as default, since a server assumes if `include` is not specified by a client in the request, it is still specified as `include=references`.\nNote, if a client explicitly specifies `include` and leaves out `references`, `references` resource objects MUST NOT be included under the top-level field `included`, as per the definition of `included`, see section JSON Response Schema: Common Fields.\n\n> **Note**: A query with the parameter `include` set to the empty string means no related resource objects are to be returned under the top-level field `included`.",
+ "name": "include",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
- "title": "Include",
"description": "A server MAY implement the JSON API concept of returning [compound documents](https://jsonapi.org/format/1.0/#document-compound-documents) by utilizing the `include` query parameter as specified by [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-includes).\n\nAll related resource objects MUST be returned as part of an array value for the top-level `included` field, see the section JSON Response Schema: Common Fields.\n\nThe value of `include` MUST be a comma-separated list of \"relationship paths\", as defined in the [JSON API](https://jsonapi.org/format/1.0/#fetching-includes).\nIf relationship paths are not supported, or a server is unable to identify a relationship path a `400 Bad Request` response MUST be made.\n\nThe **default value** for `include` is `references`.\nThis means `references` entries MUST always be included under the top-level field `included` as default, since a server assumes if `include` is not specified by a client in the request, it is still specified as `include=references`.\nNote, if a client explicitly specifies `include` and leaves out `references`, `references` resource objects MUST NOT be included under the top-level field `included`, as per the definition of `included`, see section JSON Response Schema: Common Fields.\n\n> **Note**: A query with the parameter `include` set to the empty string means no related resource objects are to be returned under the top-level field `included`.",
- "default": "references"
+ "default": "references",
+ "title": "Include"
},
- "name": "include",
- "in": "query"
+ "description": "A server MAY implement the JSON API concept of returning [compound documents](https://jsonapi.org/format/1.0/#document-compound-documents) by utilizing the `include` query parameter as specified by [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-includes).\n\nAll related resource objects MUST be returned as part of an array value for the top-level `included` field, see the section JSON Response Schema: Common Fields.\n\nThe value of `include` MUST be a comma-separated list of \"relationship paths\", as defined in the [JSON API](https://jsonapi.org/format/1.0/#fetching-includes).\nIf relationship paths are not supported, or a server is unable to identify a relationship path a `400 Bad Request` response MUST be made.\n\nThe **default value** for `include` is `references`.\nThis means `references` entries MUST always be included under the top-level field `included` as default, since a server assumes if `include` is not specified by a client in the request, it is still specified as `include=references`.\nNote, if a client explicitly specifies `include` and leaves out `references`, `references` resource objects MUST NOT be included under the top-level field `included`, as per the definition of `included`, see section JSON Response Schema: Common Fields.\n\n> **Note**: A query with the parameter `include` set to the empty string means no related resource objects are to be returned under the top-level field `included`."
},
{
- "description": "If the client provides the parameter, the value SHOULD have the format `vMAJOR` or `vMAJOR.MINOR`, where MAJOR is a major version and MINOR is a minor version of the API. For example, if a client appends `api_hint=v1.0` to the query string, the hint provided is for major version 1 and minor version 0.",
+ "name": "api_hint",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
"pattern": "(v[0-9]+(\\.[0-9]+)?)?",
- "title": "Api Hint",
"description": "If the client provides the parameter, the value SHOULD have the format `vMAJOR` or `vMAJOR.MINOR`, where MAJOR is a major version and MINOR is a minor version of the API. For example, if a client appends `api_hint=v1.0` to the query string, the hint provided is for major version 1 and minor version 0.",
- "default": ""
+ "default": "",
+ "title": "Api Hint"
},
- "name": "api_hint",
- "in": "query"
+ "description": "If the client provides the parameter, the value SHOULD have the format `vMAJOR` or `vMAJOR.MINOR`, where MAJOR is a major version and MINOR is a minor version of the API. For example, if a client appends `api_hint=v1.0` to the query string, the hint provided is for major version 1 and minor version 0."
}
],
"responses": {
@@ -879,164 +879,164 @@
"operationId": "get_structures_structures_get",
"parameters": [
{
- "description": "A filter string, in the format described in section API Filtering Format Specification of the specification.",
+ "name": "filter",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
- "title": "Filter",
"description": "A filter string, in the format described in section API Filtering Format Specification of the specification.",
- "default": ""
+ "default": "",
+ "title": "Filter"
},
- "name": "filter",
- "in": "query"
+ "description": "A filter string, in the format described in section API Filtering Format Specification of the specification."
},
{
- "description": "The output format requested (see section Response Format).\nDefaults to the format string 'json', which specifies the standard output format described in this specification.\nExample: `http://example.com/v1/structures?response_format=xml`",
+ "name": "response_format",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
- "title": "Response Format",
"description": "The output format requested (see section Response Format).\nDefaults to the format string 'json', which specifies the standard output format described in this specification.\nExample: `http://example.com/v1/structures?response_format=xml`",
- "default": "json"
+ "default": "json",
+ "title": "Response Format"
},
- "name": "response_format",
- "in": "query"
+ "description": "The output format requested (see section Response Format).\nDefaults to the format string 'json', which specifies the standard output format described in this specification.\nExample: `http://example.com/v1/structures?response_format=xml`"
},
{
- "description": "An email address of the user making the request.\nThe email SHOULD be that of a person and not an automatic system.\nExample: `http://example.com/v1/structures?email_address=user@example.com`",
+ "name": "email_address",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
"format": "email",
- "title": "Email Address",
"description": "An email address of the user making the request.\nThe email SHOULD be that of a person and not an automatic system.\nExample: `http://example.com/v1/structures?email_address=user@example.com`",
- "default": ""
+ "default": "",
+ "title": "Email Address"
},
- "name": "email_address",
- "in": "query"
+ "description": "An email address of the user making the request.\nThe email SHOULD be that of a person and not an automatic system.\nExample: `http://example.com/v1/structures?email_address=user@example.com`"
},
{
- "description": "A comma-delimited set of fields to be provided in the output.\nIf provided, these fields MUST be returned along with the REQUIRED fields.\nOther OPTIONAL fields MUST NOT be returned when this parameter is present.\nExample: `http://example.com/v1/structures?response_fields=last_modified,nsites`",
+ "name": "response_fields",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
"pattern": "([a-z_][a-z_0-9]*(,[a-z_][a-z_0-9]*)*)?",
- "title": "Response Fields",
"description": "A comma-delimited set of fields to be provided in the output.\nIf provided, these fields MUST be returned along with the REQUIRED fields.\nOther OPTIONAL fields MUST NOT be returned when this parameter is present.\nExample: `http://example.com/v1/structures?response_fields=last_modified,nsites`",
- "default": ""
+ "default": "",
+ "title": "Response Fields"
},
- "name": "response_fields",
- "in": "query"
+ "description": "A comma-delimited set of fields to be provided in the output.\nIf provided, these fields MUST be returned along with the REQUIRED fields.\nOther OPTIONAL fields MUST NOT be returned when this parameter is present.\nExample: `http://example.com/v1/structures?response_fields=last_modified,nsites`"
},
{
- "description": "If supporting sortable queries, an implementation MUST use the `sort` query parameter with format as specified by [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-sorting).\n\nAn implementation MAY support multiple sort fields for a single query.\nIf it does, it again MUST conform to the JSON API 1.0 specification.\n\nIf an implementation supports sorting for an entry listing endpoint, then the `/info/` endpoint MUST include, for each field name `` in its `data.properties.` response value that can be used for sorting, the key `sortable` with value `true`.\nIf a field name under an entry listing endpoint supporting sorting cannot be used for sorting, the server MUST either leave out the `sortable` key or set it equal to `false` for the specific field name.\nThe set of field names, with `sortable` equal to `true` are allowed to be used in the \"sort fields\" list according to its definition in the JSON API 1.0 specification.\nThe field `sortable` is in addition to each property description and other OPTIONAL fields.\nAn example is shown in the section Entry Listing Info Endpoints.",
+ "name": "sort",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
"pattern": "([a-z_][a-z_0-9]*(,[a-z_][a-z_0-9]*)*)?",
- "title": "Sort",
"description": "If supporting sortable queries, an implementation MUST use the `sort` query parameter with format as specified by [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-sorting).\n\nAn implementation MAY support multiple sort fields for a single query.\nIf it does, it again MUST conform to the JSON API 1.0 specification.\n\nIf an implementation supports sorting for an entry listing endpoint, then the `/info/` endpoint MUST include, for each field name `` in its `data.properties.` response value that can be used for sorting, the key `sortable` with value `true`.\nIf a field name under an entry listing endpoint supporting sorting cannot be used for sorting, the server MUST either leave out the `sortable` key or set it equal to `false` for the specific field name.\nThe set of field names, with `sortable` equal to `true` are allowed to be used in the \"sort fields\" list according to its definition in the JSON API 1.0 specification.\nThe field `sortable` is in addition to each property description and other OPTIONAL fields.\nAn example is shown in the section Entry Listing Info Endpoints.",
- "default": ""
+ "default": "",
+ "title": "Sort"
},
- "name": "sort",
- "in": "query"
+ "description": "If supporting sortable queries, an implementation MUST use the `sort` query parameter with format as specified by [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-sorting).\n\nAn implementation MAY support multiple sort fields for a single query.\nIf it does, it again MUST conform to the JSON API 1.0 specification.\n\nIf an implementation supports sorting for an entry listing endpoint, then the `/info/` endpoint MUST include, for each field name `` in its `data.properties.` response value that can be used for sorting, the key `sortable` with value `true`.\nIf a field name under an entry listing endpoint supporting sorting cannot be used for sorting, the server MUST either leave out the `sortable` key or set it equal to `false` for the specific field name.\nThe set of field names, with `sortable` equal to `true` are allowed to be used in the \"sort fields\" list according to its definition in the JSON API 1.0 specification.\nThe field `sortable` is in addition to each property description and other OPTIONAL fields.\nAn example is shown in the section Entry Listing Info Endpoints."
},
{
- "description": "Sets a numerical limit on the number of entries returned.\nSee [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-pagination).\nThe API implementation MUST return no more than the number specified.\nIt MAY return fewer.\nThe database MAY have a maximum limit and not accept larger numbers (in which case an error code -- 403 Forbidden -- MUST be returned).\nThe default limit value is up to the API implementation to decide.\nExample: `http://example.com/optimade/v1/structures?page_limit=100`",
+ "name": "page_limit",
+ "in": "query",
"required": false,
"schema": {
"type": "integer",
- "minimum": 0.0,
- "title": "Page Limit",
+ "minimum": 0,
"description": "Sets a numerical limit on the number of entries returned.\nSee [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-pagination).\nThe API implementation MUST return no more than the number specified.\nIt MAY return fewer.\nThe database MAY have a maximum limit and not accept larger numbers (in which case an error code -- 403 Forbidden -- MUST be returned).\nThe default limit value is up to the API implementation to decide.\nExample: `http://example.com/optimade/v1/structures?page_limit=100`",
- "default": 20
+ "default": 20,
+ "title": "Page Limit"
},
- "name": "page_limit",
- "in": "query"
+ "description": "Sets a numerical limit on the number of entries returned.\nSee [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-pagination).\nThe API implementation MUST return no more than the number specified.\nIt MAY return fewer.\nThe database MAY have a maximum limit and not accept larger numbers (in which case an error code -- 403 Forbidden -- MUST be returned).\nThe default limit value is up to the API implementation to decide.\nExample: `http://example.com/optimade/v1/structures?page_limit=100`"
},
{
- "description": "RECOMMENDED for use with _offset-based_ pagination: using `page_offset` and `page_limit` is RECOMMENDED.\nExample: Skip 50 structures and fetch up to 100: `/structures?page_offset=50&page_limit=100`.",
+ "name": "page_offset",
+ "in": "query",
"required": false,
"schema": {
"type": "integer",
- "minimum": 0.0,
- "title": "Page Offset",
+ "minimum": 0,
"description": "RECOMMENDED for use with _offset-based_ pagination: using `page_offset` and `page_limit` is RECOMMENDED.\nExample: Skip 50 structures and fetch up to 100: `/structures?page_offset=50&page_limit=100`.",
- "default": 0
+ "default": 0,
+ "title": "Page Offset"
},
- "name": "page_offset",
- "in": "query"
+ "description": "RECOMMENDED for use with _offset-based_ pagination: using `page_offset` and `page_limit` is RECOMMENDED.\nExample: Skip 50 structures and fetch up to 100: `/structures?page_offset=50&page_limit=100`."
},
{
- "description": "RECOMMENDED for use with _page-based_ pagination: using `page_number` and `page_limit` is RECOMMENDED.\nIt is RECOMMENDED that the first page has number 1, i.e., that `page_number` is 1-based.\nExample: Fetch page 2 of up to 50 structures per page: `/structures?page_number=2&page_limit=50`.",
+ "name": "page_number",
+ "in": "query",
"required": false,
"schema": {
"type": "integer",
- "title": "Page Number",
- "description": "RECOMMENDED for use with _page-based_ pagination: using `page_number` and `page_limit` is RECOMMENDED.\nIt is RECOMMENDED that the first page has number 1, i.e., that `page_number` is 1-based.\nExample: Fetch page 2 of up to 50 structures per page: `/structures?page_number=2&page_limit=50`."
+ "description": "RECOMMENDED for use with _page-based_ pagination: using `page_number` and `page_limit` is RECOMMENDED.\nIt is RECOMMENDED that the first page has number 1, i.e., that `page_number` is 1-based.\nExample: Fetch page 2 of up to 50 structures per page: `/structures?page_number=2&page_limit=50`.",
+ "title": "Page Number"
},
- "name": "page_number",
- "in": "query"
+ "description": "RECOMMENDED for use with _page-based_ pagination: using `page_number` and `page_limit` is RECOMMENDED.\nIt is RECOMMENDED that the first page has number 1, i.e., that `page_number` is 1-based.\nExample: Fetch page 2 of up to 50 structures per page: `/structures?page_number=2&page_limit=50`."
},
{
- "description": "RECOMMENDED for use with _cursor-based_ pagination: using `page_cursor` and `page_limit` is RECOMMENDED.",
+ "name": "page_cursor",
+ "in": "query",
"required": false,
"schema": {
"type": "integer",
- "minimum": 0.0,
- "title": "Page Cursor",
+ "minimum": 0,
"description": "RECOMMENDED for use with _cursor-based_ pagination: using `page_cursor` and `page_limit` is RECOMMENDED.",
- "default": 0
+ "default": 0,
+ "title": "Page Cursor"
},
- "name": "page_cursor",
- "in": "query"
+ "description": "RECOMMENDED for use with _cursor-based_ pagination: using `page_cursor` and `page_limit` is RECOMMENDED."
},
{
- "description": "RECOMMENDED for use with _value-based_ pagination: using `page_above`/`page_below` and `page_limit` is RECOMMENDED.\nExample: Fetch up to 100 structures above sort-field value 4000 (in this example, server chooses to fetch results sorted by increasing `id`, so `page_above` value refers to an `id` value): `/structures?page_above=4000&page_limit=100`.",
+ "name": "page_above",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
- "title": "Page Above",
- "description": "RECOMMENDED for use with _value-based_ pagination: using `page_above`/`page_below` and `page_limit` is RECOMMENDED.\nExample: Fetch up to 100 structures above sort-field value 4000 (in this example, server chooses to fetch results sorted by increasing `id`, so `page_above` value refers to an `id` value): `/structures?page_above=4000&page_limit=100`."
+ "description": "RECOMMENDED for use with _value-based_ pagination: using `page_above`/`page_below` and `page_limit` is RECOMMENDED.\nExample: Fetch up to 100 structures above sort-field value 4000 (in this example, server chooses to fetch results sorted by increasing `id`, so `page_above` value refers to an `id` value): `/structures?page_above=4000&page_limit=100`.",
+ "title": "Page Above"
},
- "name": "page_above",
- "in": "query"
+ "description": "RECOMMENDED for use with _value-based_ pagination: using `page_above`/`page_below` and `page_limit` is RECOMMENDED.\nExample: Fetch up to 100 structures above sort-field value 4000 (in this example, server chooses to fetch results sorted by increasing `id`, so `page_above` value refers to an `id` value): `/structures?page_above=4000&page_limit=100`."
},
{
- "description": "RECOMMENDED for use with _value-based_ pagination: using `page_above`/`page_below` and `page_limit` is RECOMMENDED.",
+ "name": "page_below",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
- "title": "Page Below",
- "description": "RECOMMENDED for use with _value-based_ pagination: using `page_above`/`page_below` and `page_limit` is RECOMMENDED."
+ "description": "RECOMMENDED for use with _value-based_ pagination: using `page_above`/`page_below` and `page_limit` is RECOMMENDED.",
+ "title": "Page Below"
},
- "name": "page_below",
- "in": "query"
+ "description": "RECOMMENDED for use with _value-based_ pagination: using `page_above`/`page_below` and `page_limit` is RECOMMENDED."
},
{
- "description": "A server MAY implement the JSON API concept of returning [compound documents](https://jsonapi.org/format/1.0/#document-compound-documents) by utilizing the `include` query parameter as specified by [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-includes).\n\nAll related resource objects MUST be returned as part of an array value for the top-level `included` field, see the section JSON Response Schema: Common Fields.\n\nThe value of `include` MUST be a comma-separated list of \"relationship paths\", as defined in the [JSON API](https://jsonapi.org/format/1.0/#fetching-includes).\nIf relationship paths are not supported, or a server is unable to identify a relationship path a `400 Bad Request` response MUST be made.\n\nThe **default value** for `include` is `references`.\nThis means `references` entries MUST always be included under the top-level field `included` as default, since a server assumes if `include` is not specified by a client in the request, it is still specified as `include=references`.\nNote, if a client explicitly specifies `include` and leaves out `references`, `references` resource objects MUST NOT be included under the top-level field `included`, as per the definition of `included`, see section JSON Response Schema: Common Fields.\n\n> **Note**: A query with the parameter `include` set to the empty string means no related resource objects are to be returned under the top-level field `included`.",
+ "name": "include",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
- "title": "Include",
"description": "A server MAY implement the JSON API concept of returning [compound documents](https://jsonapi.org/format/1.0/#document-compound-documents) by utilizing the `include` query parameter as specified by [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-includes).\n\nAll related resource objects MUST be returned as part of an array value for the top-level `included` field, see the section JSON Response Schema: Common Fields.\n\nThe value of `include` MUST be a comma-separated list of \"relationship paths\", as defined in the [JSON API](https://jsonapi.org/format/1.0/#fetching-includes).\nIf relationship paths are not supported, or a server is unable to identify a relationship path a `400 Bad Request` response MUST be made.\n\nThe **default value** for `include` is `references`.\nThis means `references` entries MUST always be included under the top-level field `included` as default, since a server assumes if `include` is not specified by a client in the request, it is still specified as `include=references`.\nNote, if a client explicitly specifies `include` and leaves out `references`, `references` resource objects MUST NOT be included under the top-level field `included`, as per the definition of `included`, see section JSON Response Schema: Common Fields.\n\n> **Note**: A query with the parameter `include` set to the empty string means no related resource objects are to be returned under the top-level field `included`.",
- "default": "references"
+ "default": "references",
+ "title": "Include"
},
- "name": "include",
- "in": "query"
+ "description": "A server MAY implement the JSON API concept of returning [compound documents](https://jsonapi.org/format/1.0/#document-compound-documents) by utilizing the `include` query parameter as specified by [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-includes).\n\nAll related resource objects MUST be returned as part of an array value for the top-level `included` field, see the section JSON Response Schema: Common Fields.\n\nThe value of `include` MUST be a comma-separated list of \"relationship paths\", as defined in the [JSON API](https://jsonapi.org/format/1.0/#fetching-includes).\nIf relationship paths are not supported, or a server is unable to identify a relationship path a `400 Bad Request` response MUST be made.\n\nThe **default value** for `include` is `references`.\nThis means `references` entries MUST always be included under the top-level field `included` as default, since a server assumes if `include` is not specified by a client in the request, it is still specified as `include=references`.\nNote, if a client explicitly specifies `include` and leaves out `references`, `references` resource objects MUST NOT be included under the top-level field `included`, as per the definition of `included`, see section JSON Response Schema: Common Fields.\n\n> **Note**: A query with the parameter `include` set to the empty string means no related resource objects are to be returned under the top-level field `included`."
},
{
- "description": "If the client provides the parameter, the value SHOULD have the format `vMAJOR` or `vMAJOR.MINOR`, where MAJOR is a major version and MINOR is a minor version of the API. For example, if a client appends `api_hint=v1.0` to the query string, the hint provided is for major version 1 and minor version 0.",
+ "name": "api_hint",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
"pattern": "(v[0-9]+(\\.[0-9]+)?)?",
- "title": "Api Hint",
"description": "If the client provides the parameter, the value SHOULD have the format `vMAJOR` or `vMAJOR.MINOR`, where MAJOR is a major version and MINOR is a minor version of the API. For example, if a client appends `api_hint=v1.0` to the query string, the hint provided is for major version 1 and minor version 0.",
- "default": ""
+ "default": "",
+ "title": "Api Hint"
},
- "name": "api_hint",
- "in": "query"
+ "description": "If the client provides the parameter, the value SHOULD have the format `vMAJOR` or `vMAJOR.MINOR`, where MAJOR is a major version and MINOR is a minor version of the API. For example, if a client appends `api_hint=v1.0` to the query string, the hint provided is for major version 1 and minor version 0."
}
],
"responses": {
@@ -1132,76 +1132,76 @@
"operationId": "get_single_structure_structures__entry_id__get",
"parameters": [
{
+ "name": "entry_id",
+ "in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Entry Id"
- },
- "name": "entry_id",
- "in": "path"
+ }
},
{
- "description": "The output format requested (see section Response Format).\nDefaults to the format string 'json', which specifies the standard output format described in this specification.\nExample: `http://example.com/v1/structures?response_format=xml`",
+ "name": "response_format",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
- "title": "Response Format",
"description": "The output format requested (see section Response Format).\nDefaults to the format string 'json', which specifies the standard output format described in this specification.\nExample: `http://example.com/v1/structures?response_format=xml`",
- "default": "json"
+ "default": "json",
+ "title": "Response Format"
},
- "name": "response_format",
- "in": "query"
+ "description": "The output format requested (see section Response Format).\nDefaults to the format string 'json', which specifies the standard output format described in this specification.\nExample: `http://example.com/v1/structures?response_format=xml`"
},
{
- "description": "An email address of the user making the request.\nThe email SHOULD be that of a person and not an automatic system.\nExample: `http://example.com/v1/structures?email_address=user@example.com`",
+ "name": "email_address",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
"format": "email",
- "title": "Email Address",
"description": "An email address of the user making the request.\nThe email SHOULD be that of a person and not an automatic system.\nExample: `http://example.com/v1/structures?email_address=user@example.com`",
- "default": ""
+ "default": "",
+ "title": "Email Address"
},
- "name": "email_address",
- "in": "query"
+ "description": "An email address of the user making the request.\nThe email SHOULD be that of a person and not an automatic system.\nExample: `http://example.com/v1/structures?email_address=user@example.com`"
},
{
- "description": "A comma-delimited set of fields to be provided in the output.\nIf provided, these fields MUST be returned along with the REQUIRED fields.\nOther OPTIONAL fields MUST NOT be returned when this parameter is present.\nExample: `http://example.com/v1/structures?response_fields=last_modified,nsites`",
+ "name": "response_fields",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
"pattern": "([a-z_][a-z_0-9]*(,[a-z_][a-z_0-9]*)*)?",
- "title": "Response Fields",
"description": "A comma-delimited set of fields to be provided in the output.\nIf provided, these fields MUST be returned along with the REQUIRED fields.\nOther OPTIONAL fields MUST NOT be returned when this parameter is present.\nExample: `http://example.com/v1/structures?response_fields=last_modified,nsites`",
- "default": ""
+ "default": "",
+ "title": "Response Fields"
},
- "name": "response_fields",
- "in": "query"
+ "description": "A comma-delimited set of fields to be provided in the output.\nIf provided, these fields MUST be returned along with the REQUIRED fields.\nOther OPTIONAL fields MUST NOT be returned when this parameter is present.\nExample: `http://example.com/v1/structures?response_fields=last_modified,nsites`"
},
{
- "description": "A server MAY implement the JSON API concept of returning [compound documents](https://jsonapi.org/format/1.0/#document-compound-documents) by utilizing the `include` query parameter as specified by [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-includes).\n\nAll related resource objects MUST be returned as part of an array value for the top-level `included` field, see the section JSON Response Schema: Common Fields.\n\nThe value of `include` MUST be a comma-separated list of \"relationship paths\", as defined in the [JSON API](https://jsonapi.org/format/1.0/#fetching-includes).\nIf relationship paths are not supported, or a server is unable to identify a relationship path a `400 Bad Request` response MUST be made.\n\nThe **default value** for `include` is `references`.\nThis means `references` entries MUST always be included under the top-level field `included` as default, since a server assumes if `include` is not specified by a client in the request, it is still specified as `include=references`.\nNote, if a client explicitly specifies `include` and leaves out `references`, `references` resource objects MUST NOT be included under the top-level field `included`, as per the definition of `included`, see section JSON Response Schema: Common Fields.\n\n> **Note**: A query with the parameter `include` set to the empty string means no related resource objects are to be returned under the top-level field `included`.",
+ "name": "include",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
- "title": "Include",
"description": "A server MAY implement the JSON API concept of returning [compound documents](https://jsonapi.org/format/1.0/#document-compound-documents) by utilizing the `include` query parameter as specified by [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-includes).\n\nAll related resource objects MUST be returned as part of an array value for the top-level `included` field, see the section JSON Response Schema: Common Fields.\n\nThe value of `include` MUST be a comma-separated list of \"relationship paths\", as defined in the [JSON API](https://jsonapi.org/format/1.0/#fetching-includes).\nIf relationship paths are not supported, or a server is unable to identify a relationship path a `400 Bad Request` response MUST be made.\n\nThe **default value** for `include` is `references`.\nThis means `references` entries MUST always be included under the top-level field `included` as default, since a server assumes if `include` is not specified by a client in the request, it is still specified as `include=references`.\nNote, if a client explicitly specifies `include` and leaves out `references`, `references` resource objects MUST NOT be included under the top-level field `included`, as per the definition of `included`, see section JSON Response Schema: Common Fields.\n\n> **Note**: A query with the parameter `include` set to the empty string means no related resource objects are to be returned under the top-level field `included`.",
- "default": "references"
+ "default": "references",
+ "title": "Include"
},
- "name": "include",
- "in": "query"
+ "description": "A server MAY implement the JSON API concept of returning [compound documents](https://jsonapi.org/format/1.0/#document-compound-documents) by utilizing the `include` query parameter as specified by [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-includes).\n\nAll related resource objects MUST be returned as part of an array value for the top-level `included` field, see the section JSON Response Schema: Common Fields.\n\nThe value of `include` MUST be a comma-separated list of \"relationship paths\", as defined in the [JSON API](https://jsonapi.org/format/1.0/#fetching-includes).\nIf relationship paths are not supported, or a server is unable to identify a relationship path a `400 Bad Request` response MUST be made.\n\nThe **default value** for `include` is `references`.\nThis means `references` entries MUST always be included under the top-level field `included` as default, since a server assumes if `include` is not specified by a client in the request, it is still specified as `include=references`.\nNote, if a client explicitly specifies `include` and leaves out `references`, `references` resource objects MUST NOT be included under the top-level field `included`, as per the definition of `included`, see section JSON Response Schema: Common Fields.\n\n> **Note**: A query with the parameter `include` set to the empty string means no related resource objects are to be returned under the top-level field `included`."
},
{
- "description": "If the client provides the parameter, the value SHOULD have the format `vMAJOR` or `vMAJOR.MINOR`, where MAJOR is a major version and MINOR is a minor version of the API. For example, if a client appends `api_hint=v1.0` to the query string, the hint provided is for major version 1 and minor version 0.",
+ "name": "api_hint",
+ "in": "query",
"required": false,
"schema": {
"type": "string",
"pattern": "(v[0-9]+(\\.[0-9]+)?)?",
- "title": "Api Hint",
"description": "If the client provides the parameter, the value SHOULD have the format `vMAJOR` or `vMAJOR.MINOR`, where MAJOR is a major version and MINOR is a minor version of the API. For example, if a client appends `api_hint=v1.0` to the query string, the hint provided is for major version 1 and minor version 0.",
- "default": ""
+ "default": "",
+ "title": "Api Hint"
},
- "name": "api_hint",
- "in": "query"
+ "description": "If the client provides the parameter, the value SHOULD have the format `vMAJOR` or `vMAJOR.MINOR`, where MAJOR is a major version and MINOR is a minor version of the API. For example, if a client appends `api_hint=v1.0` to the query string, the hint provided is for major version 1 and minor version 0."
}
],
"responses": {
@@ -1314,6 +1314,7 @@
"components": {
"schemas": {
"Aggregate": {
+ "type": "string",
"enum": [
"ok",
"test",
@@ -1335,8 +1336,8 @@
"type": "array",
"title": "Sites In Groups",
"description": "Index of the sites (0-based) that belong to each group for each assembly.\n\n- **Examples**:\n - `[[1], [2]]`: two groups, one with the second site, one with the third.\n - `[[1,2], [3]]`: one group with the second and third site, one with the fourth.",
- "x-optimade-support": "must",
- "x-optimade-queryable": "optional"
+ "x-optimade-queryable": "optional",
+ "x-optimade-support": "must"
},
"group_probabilities": {
"items": {
@@ -1345,8 +1346,8 @@
"type": "array",
"title": "Group Probabilities",
"description": "Statistical probability of each group. It MUST have the same length as `sites_in_groups`.\nIt SHOULD sum to one.\nSee below for examples of how to specify the probability of the occurrence of a vacancy.\nThe possible reasons for the values not to sum to one are the same as already specified above for the `concentration` of each `species`.",
- "x-optimade-support": "must",
- "x-optimade-queryable": "optional"
+ "x-optimade-queryable": "optional",
+ "x-optimade-support": "must"
}
},
"type": "object",
@@ -1359,6 +1360,7 @@
},
"Attributes": {
"properties": {},
+ "additionalProperties": true,
"type": "object",
"title": "Attributes",
"description": "Members of the attributes object (\"attributes\") represent information about the resource object in which it's defined.\nThe keys for Attributes MUST NOT be:\n relationships\n links\n id\n type"
@@ -1367,9 +1369,8 @@
"properties": {
"url": {
"type": "string",
- "maxLength": 65536,
"minLength": 1,
- "pattern": ".+/v[0-1](\\.[0-9]+)*/?$",
+ "pattern": "^.+/v[0-1](\\.[0-9]+)*/?$",
"format": "uri",
"title": "Url",
"description": "A string specifying a versioned base URL that MUST adhere to the rules in section Base URL"
@@ -1379,7 +1380,7 @@
"pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$",
"title": "Version",
"description": "A string containing the full version number of the API served at that versioned base URL.\nThe version number string MUST NOT be prefixed by, e.g., 'v'.\nExamples: `1.0.0`, `1.0.0-rc.2`.",
- "example": [
+ "examples": [
"0.10.1",
"1.0.0-rc.2",
"1.2.3-rc.5+develop"
@@ -1401,7 +1402,7 @@
"pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$",
"title": "Api Version",
"description": "Presently used full version of the OPTIMADE API.\nThe version number string MUST NOT be prefixed by, e.g., \"v\".\nExamples: `1.0.0`, `1.0.0-rc.2`.",
- "example": [
+ "examples": [
"0.10.1",
"1.0.0-rc.2",
"1.2.3-rc.5+develop"
@@ -1446,7 +1447,14 @@
"description": "Available entry endpoints as a function of output formats."
},
"is_index": {
- "type": "boolean",
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Is Index",
"description": "If true, this is an index meta-database base URL (see section Index Meta-Database). If this member is not provided, the client MUST assume this is not an index meta-database base URL (i.e., the default is for `is_index` to be `false`).",
"default": false
@@ -1466,44 +1474,56 @@
"properties": {
"id": {
"type": "string",
- "pattern": "^/$",
+ "enum": [
+ "/"
+ ],
+ "const": "/",
"title": "Id",
"default": "/"
},
"type": {
"type": "string",
- "pattern": "^info$",
+ "enum": [
+ "info"
+ ],
+ "const": "info",
"title": "Type",
"default": "info"
},
"links": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/ResourceLinks"
+ },
+ {
+ "type": "null"
}
],
- "title": "Links",
"description": "a links object containing links related to the resource."
},
"meta": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/Meta"
+ },
+ {
+ "type": "null"
}
],
- "title": "Meta",
"description": "a meta object containing non-standard meta-information about a resource that can not be represented as an attribute or relationship."
},
"attributes": {
"$ref": "#/components/schemas/BaseInfoAttributes"
},
"relationships": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/Relationships"
+ },
+ {
+ "type": "null"
}
],
- "title": "Relationships",
"description": "[Relationships object](https://jsonapi.org/format/1.0/#document-resource-object-relationships)\ndescribing relationships between the resource and other JSON API resources."
}
},
@@ -1513,8 +1533,7 @@
"type",
"attributes"
],
- "title": "BaseInfoResource",
- "description": "Resource objects appear in a JSON API document to represent resources."
+ "title": "BaseInfoResource"
},
"BaseRelationshipMeta": {
"properties": {
@@ -1524,6 +1543,7 @@
"description": "OPTIONAL human-readable description of the relationship."
}
},
+ "additionalProperties": true,
"type": "object",
"required": [
"description"
@@ -1544,12 +1564,14 @@
"description": "Resource type"
},
"meta": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/BaseRelationshipMeta"
+ },
+ {
+ "type": "null"
}
],
- "title": "Meta",
"description": "Relationship meta field. MUST contain 'description' if supplied."
}
},
@@ -1562,6 +1584,7 @@
"description": "Minimum requirements to represent a relationship resource"
},
"DataType": {
+ "type": "string",
"enum": [
"string",
"integer",
@@ -1573,7 +1596,7 @@
"unknown"
],
"title": "DataType",
- "description": "Optimade Data Types\n\nSee the section \"Data types\" in the OPTIMADE API specification for more information."
+ "description": "Optimade Data types\n\nSee the section \"Data types\" in the OPTIMADE API specification for more information."
},
"EntryInfoProperty": {
"properties": {
@@ -1583,19 +1606,36 @@
"description": "A human-readable description of the entry property"
},
"unit": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Unit",
"description": "The physical unit of the entry property.\nThis MUST be a valid representation of units according to version 2.1 of [The Unified Code for Units of Measure](https://unitsofmeasure.org/ucum.html).\nIt is RECOMMENDED that non-standard (non-SI) units are described in the description for the property."
},
"sortable": {
- "type": "boolean",
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Sortable",
"description": "Defines whether the entry property can be used for sorting with the \"sort\" parameter.\nIf the entry listing endpoint supports sorting, this key MUST be present for sortable properties with value `true`."
},
"type": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/DataType"
+ },
+ {
+ "type": "null"
}
],
"title": "Type",
@@ -1660,7 +1700,6 @@
"$ref": "#/components/schemas/EntryInfoResource"
}
],
- "title": "Data",
"description": "OPTIMADE information for an entry endpoint."
},
"meta": {
@@ -1669,43 +1708,60 @@
"$ref": "#/components/schemas/ResponseMeta"
}
],
- "title": "Meta",
"description": "A meta object containing non-standard information"
},
"errors": {
- "items": {
- "$ref": "#/components/schemas/Error"
- },
- "type": "array",
+ "anyOf": [
+ {
+ "items": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
"uniqueItems": true,
"title": "Errors",
"description": "A list of unique errors"
},
"included": {
- "items": {
- "$ref": "#/components/schemas/Resource"
- },
- "type": "array",
+ "anyOf": [
+ {
+ "items": {
+ "$ref": "#/components/schemas/Resource"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
"uniqueItems": true,
"title": "Included",
"description": "A list of unique included resources"
},
"links": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/ToplevelLinks"
+ },
+ {
+ "type": "null"
}
],
- "title": "Links",
"description": "Links associated with the primary data or errors"
},
"jsonapi": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/JsonApi"
+ },
+ {
+ "type": "null"
}
],
- "title": "Jsonapi",
"description": "Information about the JSON API used"
}
},
@@ -1714,27 +1770,30 @@
"data",
"meta"
],
- "title": "EntryInfoResponse",
- "description": "errors are not allowed"
+ "title": "EntryInfoResponse"
},
"EntryRelationships": {
"properties": {
"references": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/ReferenceRelationship"
+ },
+ {
+ "type": "null"
}
],
- "title": "References",
"description": "Object containing links to relationships with entries of the `references` type."
},
"structures": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/StructureRelationship"
+ },
+ {
+ "type": "null"
}
],
- "title": "Structures",
"description": "Object containing links to relationships with entries of the `structures` type."
}
},
@@ -1748,32 +1807,36 @@
"type": "string",
"title": "Id",
"description": "An entry's ID as defined in section Definition of Terms.\n\n- **Type**: string.\n\n- **Requirements/Conventions**:\n - **Support**: MUST be supported by all implementations, MUST NOT be `null`.\n - **Query**: MUST be a queryable property with support for all mandatory filter features.\n - **Response**: REQUIRED in the response.\n\n- **Examples**:\n - `\"db/1234567\"`\n - `\"cod/2000000\"`\n - `\"cod/2000000@1234567\"`\n - `\"nomad/L1234567890\"`\n - `\"42\"`",
- "x-optimade-support": "must",
- "x-optimade-queryable": "must"
+ "x-optimade-queryable": "must",
+ "x-optimade-support": "must"
},
"type": {
"type": "string",
"title": "Type",
"description": "The name of the type of an entry.\n\n- **Type**: string.\n\n- **Requirements/Conventions**:\n - **Support**: MUST be supported by all implementations, MUST NOT be `null`.\n - **Query**: MUST be a queryable property with support for all mandatory filter features.\n - **Response**: REQUIRED in the response.\n - MUST be an existing entry type.\n - The entry of type `` and ID `` MUST be returned in response to a request for `//` under the versioned base URL.\n\n- **Example**: `\"structures\"`",
- "x-optimade-support": "must",
- "x-optimade-queryable": "must"
+ "x-optimade-queryable": "must",
+ "x-optimade-support": "must"
},
"links": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/ResourceLinks"
+ },
+ {
+ "type": "null"
}
],
- "title": "Links",
"description": "a links object containing links related to the resource."
},
"meta": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/Meta"
+ },
+ {
+ "type": "null"
}
],
- "title": "Meta",
"description": "a meta object containing non-standard meta-information about a resource that can not be represented as an attribute or relationship."
},
"attributes": {
@@ -1782,16 +1845,17 @@
"$ref": "#/components/schemas/EntryResourceAttributes"
}
],
- "title": "Attributes",
"description": "A dictionary, containing key-value pairs representing the entry's properties, except for `type` and `id`.\nDatabase-provider-specific properties need to include the database-provider-specific prefix (see section on Database-Provider-Specific Namespace Prefixes)."
},
"relationships": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/EntryRelationships"
+ },
+ {
+ "type": "null"
}
],
- "title": "Relationships",
"description": "A dictionary containing references to other entries according to the description in section Relationships encoded as [JSON API Relationships](https://jsonapi.org/format/1.0/#document-resource-object-relationships).\nThe OPTIONAL human-readable description of the relationship MAY be provided in the `description` field inside the `meta` dictionary of the JSON API resource identifier object."
}
},
@@ -1807,21 +1871,36 @@
"EntryResourceAttributes": {
"properties": {
"immutable_id": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Immutable Id",
"description": "The entry's immutable ID (e.g., an UUID). This is important for databases having preferred IDs that point to \"the latest version\" of a record, but still offer access to older variants. This ID maps to the version-specific record, in case it changes in the future.\n\n- **Type**: string.\n\n- **Requirements/Conventions**:\n - **Support**: OPTIONAL support in implementations, i.e., MAY be `null`.\n - **Query**: MUST be a queryable property with support for all mandatory filter features.\n\n- **Examples**:\n - `\"8bd3e750-b477-41a0-9b11-3a799f21b44f\"`\n - `\"fjeiwoj,54;@=%<>#32\"` (Strings that are not URL-safe are allowed.)",
- "x-optimade-support": "optional",
- "x-optimade-queryable": "must"
+ "x-optimade-queryable": "must",
+ "x-optimade-support": "optional"
},
"last_modified": {
- "type": "string",
- "format": "date-time",
+ "anyOf": [
+ {
+ "type": "string",
+ "format": "date-time"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Last Modified",
"description": "Date and time representing when the entry was last modified.\n\n- **Type**: timestamp.\n\n- **Requirements/Conventions**:\n - **Support**: SHOULD be supported by all implementations, i.e., SHOULD NOT be `null`.\n - **Query**: MUST be a queryable property with support for all mandatory filter features.\n - **Response**: REQUIRED in the response unless the query parameter `response_fields` is present and does not include this property.\n\n- **Example**:\n - As part of JSON response format: `\"2007-04-05T14:30:20Z\"` (i.e., encoded as an [RFC 3339 Internet Date/Time Format](https://tools.ietf.org/html/rfc3339#section-5.6) string.)",
- "x-optimade-support": "should",
- "x-optimade-queryable": "must"
+ "x-optimade-queryable": "must",
+ "x-optimade-support": "should"
}
},
+ "additionalProperties": true,
"type": "object",
"required": [
"last_modified"
@@ -1832,55 +1911,96 @@
"Error": {
"properties": {
"id": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Id",
"description": "A unique identifier for this particular occurrence of the problem."
},
"links": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/ErrorLinks"
+ },
+ {
+ "type": "null"
}
],
- "title": "Links",
"description": "A links object storing about"
},
"status": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Status",
"description": "the HTTP status code applicable to this problem, expressed as a string value."
},
"code": {
- "type": "string",
- "title": "Code",
- "description": "an application-specific error code, expressed as a string value."
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Code",
+ "description": "an application-specific error code, expressed as a string value."
},
"title": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Title",
"description": "A short, human-readable summary of the problem. It **SHOULD NOT** change from occurrence to occurrence of the problem, except for purposes of localization."
},
"detail": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Detail",
"description": "A human-readable explanation specific to this occurrence of the problem."
},
"source": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/ErrorSource"
+ },
+ {
+ "type": "null"
}
],
- "title": "Source",
"description": "An object containing references to the source of the error"
},
"meta": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/Meta"
+ },
+ {
+ "type": "null"
}
],
- "title": "Meta",
"description": "a meta object containing non-standard meta-information about the error."
}
},
@@ -1894,12 +2014,14 @@
"anyOf": [
{
"type": "string",
- "maxLength": 65536,
"minLength": 1,
"format": "uri"
},
{
"$ref": "#/components/schemas/Link"
+ },
+ {
+ "type": "null"
}
],
"title": "About",
@@ -1922,6 +2044,9 @@
"$ref": "#/components/schemas/Resource"
},
"type": "array"
+ },
+ {
+ "type": "null"
}
],
"uniqueItems": true,
@@ -1934,7 +2059,6 @@
"$ref": "#/components/schemas/ResponseMeta"
}
],
- "title": "Meta",
"description": "A meta object containing non-standard information."
},
"errors": {
@@ -1947,30 +2071,41 @@
"description": "A list of OPTIMADE-specific JSON API error objects, where the field detail MUST be present."
},
"included": {
- "items": {
- "$ref": "#/components/schemas/Resource"
- },
- "type": "array",
+ "anyOf": [
+ {
+ "items": {
+ "$ref": "#/components/schemas/Resource"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
"uniqueItems": true,
"title": "Included",
"description": "A list of unique included resources"
},
"links": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/ToplevelLinks"
+ },
+ {
+ "type": "null"
}
],
- "title": "Links",
"description": "Links associated with the primary data or errors"
},
"jsonapi": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/JsonApi"
+ },
+ {
+ "type": "null"
}
],
- "title": "Jsonapi",
"description": "Information about the JSON API used"
}
},
@@ -1985,12 +2120,26 @@
"ErrorSource": {
"properties": {
"pointer": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Pointer",
"description": "a JSON Pointer [RFC6901] to the associated entity in the request document [e.g. \"/data\" for a primary data object, or \"/data/attributes/title\" for a specific attribute]."
},
"parameter": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Parameter",
"description": "a string indicating which URI query parameter caused the error."
}
@@ -2002,12 +2151,26 @@
"Implementation": {
"properties": {
"name": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Name",
"description": "name of the implementation"
},
"version": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Version",
"description": "version string of the current implementation"
},
@@ -2015,12 +2178,14 @@
"anyOf": [
{
"type": "string",
- "maxLength": 65536,
"minLength": 1,
"format": "uri"
},
{
"$ref": "#/components/schemas/Link"
+ },
+ {
+ "type": "null"
}
],
"title": "Homepage",
@@ -2030,36 +2195,42 @@
"anyOf": [
{
"type": "string",
- "maxLength": 65536,
"minLength": 1,
"format": "uri"
},
{
"$ref": "#/components/schemas/Link"
+ },
+ {
+ "type": "null"
}
],
"title": "Source Url",
"description": "A [JSON API links object](http://jsonapi.org/format/1.0/#document-links) pointing to the implementation source, either downloadable archive or version control system."
},
"maintainer": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/ImplementationMaintainer"
+ },
+ {
+ "type": "null"
}
],
- "title": "Maintainer",
"description": "A dictionary providing details about the maintainer of the implementation."
},
"issue_tracker": {
"anyOf": [
{
"type": "string",
- "maxLength": 65536,
"minLength": 1,
"format": "uri"
},
{
"$ref": "#/components/schemas/Link"
+ },
+ {
+ "type": "null"
}
],
"title": "Issue Tracker",
@@ -2094,7 +2265,6 @@
"$ref": "#/components/schemas/BaseInfoResource"
}
],
- "title": "Data",
"description": "The implementations /info data."
},
"meta": {
@@ -2103,43 +2273,60 @@
"$ref": "#/components/schemas/ResponseMeta"
}
],
- "title": "Meta",
"description": "A meta object containing non-standard information"
},
"errors": {
- "items": {
- "$ref": "#/components/schemas/Error"
- },
- "type": "array",
+ "anyOf": [
+ {
+ "items": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
"uniqueItems": true,
"title": "Errors",
"description": "A list of unique errors"
},
"included": {
- "items": {
- "$ref": "#/components/schemas/Resource"
- },
- "type": "array",
+ "anyOf": [
+ {
+ "items": {
+ "$ref": "#/components/schemas/Resource"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
"uniqueItems": true,
"title": "Included",
"description": "A list of unique included resources"
},
"links": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/ToplevelLinks"
+ },
+ {
+ "type": "null"
}
],
- "title": "Links",
"description": "Links associated with the primary data or errors"
},
"jsonapi": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/JsonApi"
+ },
+ {
+ "type": "null"
}
],
- "title": "Jsonapi",
"description": "Information about the JSON API used"
}
},
@@ -2148,8 +2335,7 @@
"data",
"meta"
],
- "title": "InfoResponse",
- "description": "errors are not allowed"
+ "title": "InfoResponse"
},
"JsonApi": {
"properties": {
@@ -2160,12 +2346,14 @@
"default": "1.0"
},
"meta": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/Meta"
+ },
+ {
+ "type": "null"
}
],
- "title": "Meta",
"description": "Non-standard meta information"
}
},
@@ -2177,19 +2365,20 @@
"properties": {
"href": {
"type": "string",
- "maxLength": 65536,
"minLength": 1,
"format": "uri",
"title": "Href",
- "description": "a string containing the link\u2019s URL."
+ "description": "a string containing the link's URL."
},
"meta": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/Meta"
+ },
+ {
+ "type": "null"
}
],
- "title": "Meta",
"description": "a meta object containing non-standard meta-information about the link."
}
},
@@ -2201,6 +2390,7 @@
"description": "A link **MUST** be represented as either: a string containing the link's URL or a link object."
},
"LinkType": {
+ "type": "string",
"enum": [
"child",
"root",
@@ -2216,32 +2406,40 @@
"type": "string",
"title": "Id",
"description": "An entry's ID as defined in section Definition of Terms.\n\n- **Type**: string.\n\n- **Requirements/Conventions**:\n - **Support**: MUST be supported by all implementations, MUST NOT be `null`.\n - **Query**: MUST be a queryable property with support for all mandatory filter features.\n - **Response**: REQUIRED in the response.\n\n- **Examples**:\n - `\"db/1234567\"`\n - `\"cod/2000000\"`\n - `\"cod/2000000@1234567\"`\n - `\"nomad/L1234567890\"`\n - `\"42\"`",
- "x-optimade-support": "must",
- "x-optimade-queryable": "must"
+ "x-optimade-queryable": "must",
+ "x-optimade-support": "must"
},
"type": {
"type": "string",
+ "enum": [
+ "links"
+ ],
+ "const": "links",
"pattern": "^links$",
"title": "Type",
"description": "These objects are described in detail in the section Links Endpoint",
"default": "links"
},
"links": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/ResourceLinks"
+ },
+ {
+ "type": "null"
}
],
- "title": "Links",
"description": "a links object containing links related to the resource."
},
"meta": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/Meta"
+ },
+ {
+ "type": "null"
}
],
- "title": "Meta",
"description": "a meta object containing non-standard meta-information about a resource that can not be represented as an attribute or relationship."
},
"attributes": {
@@ -2250,16 +2448,17 @@
"$ref": "#/components/schemas/LinksResourceAttributes"
}
],
- "title": "Attributes",
"description": "A dictionary containing key-value pairs representing the Links resource's properties."
},
"relationships": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/EntryRelationships"
+ },
+ {
+ "type": "null"
}
],
- "title": "Relationships",
"description": "A dictionary containing references to other entries according to the description in section Relationships encoded as [JSON API Relationships](https://jsonapi.org/format/1.0/#document-resource-object-relationships).\nThe OPTIONAL human-readable description of the relationship MAY be provided in the `description` field inside the `meta` dictionary of the JSON API resource identifier object."
}
},
@@ -2288,12 +2487,14 @@
"anyOf": [
{
"type": "string",
- "maxLength": 65536,
"minLength": 1,
"format": "uri"
},
{
"$ref": "#/components/schemas/Link"
+ },
+ {
+ "type": "null"
}
],
"title": "Base Url",
@@ -2303,12 +2504,14 @@
"anyOf": [
{
"type": "string",
- "maxLength": 65536,
"minLength": 1,
"format": "uri"
},
{
"$ref": "#/components/schemas/Link"
+ },
+ {
+ "type": "null"
}
],
"title": "Homepage",
@@ -2324,9 +2527,12 @@
"description": "The type of the linked relation.\nMUST be one of these values: 'child', 'root', 'external', 'providers'."
},
"aggregate": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/Aggregate"
+ },
+ {
+ "type": "null"
}
],
"title": "Aggregate",
@@ -2334,11 +2540,19 @@
"default": "ok"
},
"no_aggregate_reason": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "No Aggregate Reason",
"description": "An OPTIONAL human-readable string indicating the reason for suggesting not to aggregate results following the link.\nIt SHOULD NOT be present if `aggregate`=`ok`."
}
},
+ "additionalProperties": true,
"type": "object",
"required": [
"name",
@@ -2377,14 +2591,20 @@
"$ref": "#/components/schemas/ResponseMeta"
}
],
- "title": "Meta",
"description": "A meta object containing non-standard information"
},
"errors": {
- "items": {
- "$ref": "#/components/schemas/Error"
- },
- "type": "array",
+ "anyOf": [
+ {
+ "items": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
"uniqueItems": true,
"title": "Errors",
"description": "A list of unique errors"
@@ -2402,27 +2622,35 @@
"type": "object"
},
"type": "array"
+ },
+ {
+ "type": "null"
}
],
"uniqueItems": true,
- "title": "Included"
+ "title": "Included",
+ "description": "A list of unique included OPTIMADE entry resources."
},
"links": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/ToplevelLinks"
+ },
+ {
+ "type": "null"
}
],
- "title": "Links",
"description": "Links associated with the primary data or errors"
},
"jsonapi": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/JsonApi"
+ },
+ {
+ "type": "null"
}
],
- "title": "Jsonapi",
"description": "Information about the JSON API used"
}
},
@@ -2431,11 +2659,11 @@
"data",
"meta"
],
- "title": "LinksResponse",
- "description": "errors are not allowed"
+ "title": "LinksResponse"
},
"Meta": {
"properties": {},
+ "additionalProperties": true,
"type": "object",
"title": "Meta",
"description": "Non-standard meta-information that can not be represented as an attribute or relationship."
@@ -2443,31 +2671,61 @@
"OptimadeError": {
"properties": {
"id": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Id",
"description": "A unique identifier for this particular occurrence of the problem."
},
"links": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/ErrorLinks"
+ },
+ {
+ "type": "null"
}
],
- "title": "Links",
"description": "A links object storing about"
},
"status": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Status",
"description": "the HTTP status code applicable to this problem, expressed as a string value."
},
"code": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Code",
"description": "an application-specific error code, expressed as a string value."
},
"title": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Title",
"description": "A short, human-readable summary of the problem. It **SHOULD NOT** change from occurrence to occurrence of the problem, except for purposes of localization."
},
@@ -2477,21 +2735,25 @@
"description": "A human-readable explanation specific to this occurrence of the problem."
},
"source": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/ErrorSource"
+ },
+ {
+ "type": "null"
}
],
- "title": "Source",
"description": "An object containing references to the source of the error"
},
"meta": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/Meta"
+ },
+ {
+ "type": "null"
}
],
- "title": "Meta",
"description": "a meta object containing non-standard meta-information about the error."
}
},
@@ -2517,24 +2779,38 @@
"type": "string",
"title": "Name",
"description": "Full name of the person, REQUIRED.",
- "x-optimade-support": "must",
- "x-optimade-queryable": "optional"
+ "x-optimade-queryable": "optional",
+ "x-optimade-support": "must"
},
"firstname": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Firstname",
"description": "First name of the person.",
- "x-optimade-support": "optional",
- "x-optimade-queryable": "optional"
+ "x-optimade-queryable": "optional",
+ "x-optimade-support": "optional"
},
"lastname": {
- "type": "string",
- "title": "Lastname",
- "description": "Last name of the person.",
- "x-optimade-support": "optional",
- "x-optimade-queryable": "optional"
- }
- },
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Lastname",
+ "description": "Last name of the person.",
+ "x-optimade-queryable": "optional",
+ "x-optimade-support": "optional"
+ }
+ },
"type": "object",
"required": [
"name"
@@ -2564,12 +2840,14 @@
"anyOf": [
{
"type": "string",
- "maxLength": 65536,
"minLength": 1,
"format": "uri"
},
{
"$ref": "#/components/schemas/Link"
+ },
+ {
+ "type": "null"
}
],
"title": "Homepage",
@@ -2588,12 +2866,14 @@
"ReferenceRelationship": {
"properties": {
"links": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/RelationshipLinks"
+ },
+ {
+ "type": "null"
}
],
- "title": "Links",
"description": "a links object containing at least one of the following: self, related"
},
"data": {
@@ -2606,6 +2886,9 @@
"$ref": "#/components/schemas/BaseRelationshipResource"
},
"type": "array"
+ },
+ {
+ "type": "null"
}
],
"uniqueItems": true,
@@ -2613,18 +2896,19 @@
"description": "Resource linkage"
},
"meta": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/Meta"
+ },
+ {
+ "type": "null"
}
],
- "title": "Meta",
"description": "a meta object that contains non-standard meta-information about the relationship."
}
},
"type": "object",
- "title": "ReferenceRelationship",
- "description": "Similar to normal JSON API relationship, but with addition of OPTIONAL meta field for a resource."
+ "title": "ReferenceRelationship"
},
"ReferenceResource": {
"properties": {
@@ -2632,46 +2916,56 @@
"type": "string",
"title": "Id",
"description": "An entry's ID as defined in section Definition of Terms.\n\n- **Type**: string.\n\n- **Requirements/Conventions**:\n - **Support**: MUST be supported by all implementations, MUST NOT be `null`.\n - **Query**: MUST be a queryable property with support for all mandatory filter features.\n - **Response**: REQUIRED in the response.\n\n- **Examples**:\n - `\"db/1234567\"`\n - `\"cod/2000000\"`\n - `\"cod/2000000@1234567\"`\n - `\"nomad/L1234567890\"`\n - `\"42\"`",
- "x-optimade-support": "must",
- "x-optimade-queryable": "must"
+ "x-optimade-queryable": "must",
+ "x-optimade-support": "must"
},
"type": {
"type": "string",
+ "enum": [
+ "references"
+ ],
+ "const": "references",
"pattern": "^references$",
"title": "Type",
"description": "The name of the type of an entry.\n- **Type**: string.\n- **Requirements/Conventions**:\n - **Support**: MUST be supported by all implementations, MUST NOT be `null`.\n - **Query**: MUST be a queryable property with support for all mandatory filter features.\n - **Response**: REQUIRED in the response.\n - MUST be an existing entry type.\n - The entry of type and ID MUST be returned in response to a request for `//` under the versioned base URL.\n- **Example**: `\"structures\"`",
"default": "references",
- "x-optimade-support": "must",
- "x-optimade-queryable": "must"
+ "x-optimade-queryable": "must",
+ "x-optimade-support": "must"
},
"links": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/ResourceLinks"
+ },
+ {
+ "type": "null"
}
],
- "title": "Links",
"description": "a links object containing links related to the resource."
},
"meta": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/Meta"
+ },
+ {
+ "type": "null"
}
],
- "title": "Meta",
"description": "a meta object containing non-standard meta-information about a resource that can not be represented as an attribute or relationship."
},
"attributes": {
"$ref": "#/components/schemas/ReferenceResourceAttributes"
},
"relationships": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/EntryRelationships"
+ },
+ {
+ "type": "null"
}
],
- "title": "Relationships",
"description": "A dictionary containing references to other entries according to the description in section Relationships encoded as [JSON API Relationships](https://jsonapi.org/format/1.0/#document-resource-object-relationships).\nThe OPTIONAL human-readable description of the relationship MAY be provided in the `description` field inside the `meta` dictionary of the JSON API resource identifier object."
}
},
@@ -2687,212 +2981,408 @@
"ReferenceResourceAttributes": {
"properties": {
"immutable_id": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Immutable Id",
"description": "The entry's immutable ID (e.g., an UUID). This is important for databases having preferred IDs that point to \"the latest version\" of a record, but still offer access to older variants. This ID maps to the version-specific record, in case it changes in the future.\n\n- **Type**: string.\n\n- **Requirements/Conventions**:\n - **Support**: OPTIONAL support in implementations, i.e., MAY be `null`.\n - **Query**: MUST be a queryable property with support for all mandatory filter features.\n\n- **Examples**:\n - `\"8bd3e750-b477-41a0-9b11-3a799f21b44f\"`\n - `\"fjeiwoj,54;@=%<>#32\"` (Strings that are not URL-safe are allowed.)",
- "x-optimade-support": "optional",
- "x-optimade-queryable": "must"
+ "x-optimade-queryable": "must",
+ "x-optimade-support": "optional"
},
"last_modified": {
- "type": "string",
- "format": "date-time",
+ "anyOf": [
+ {
+ "type": "string",
+ "format": "date-time"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Last Modified",
"description": "Date and time representing when the entry was last modified.\n\n- **Type**: timestamp.\n\n- **Requirements/Conventions**:\n - **Support**: SHOULD be supported by all implementations, i.e., SHOULD NOT be `null`.\n - **Query**: MUST be a queryable property with support for all mandatory filter features.\n - **Response**: REQUIRED in the response unless the query parameter `response_fields` is present and does not include this property.\n\n- **Example**:\n - As part of JSON response format: `\"2007-04-05T14:30:20Z\"` (i.e., encoded as an [RFC 3339 Internet Date/Time Format](https://tools.ietf.org/html/rfc3339#section-5.6) string.)",
- "x-optimade-support": "should",
- "x-optimade-queryable": "must"
+ "x-optimade-queryable": "must",
+ "x-optimade-support": "should"
},
"authors": {
- "items": {
- "$ref": "#/components/schemas/Person"
- },
- "type": "array",
+ "anyOf": [
+ {
+ "items": {
+ "$ref": "#/components/schemas/Person"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Authors",
"description": "List of person objects containing the authors of the reference.",
- "x-optimade-support": "optional",
- "x-optimade-queryable": "optional"
+ "x-optimade-queryable": "optional",
+ "x-optimade-support": "optional"
},
"editors": {
- "items": {
- "$ref": "#/components/schemas/Person"
- },
- "type": "array",
+ "anyOf": [
+ {
+ "items": {
+ "$ref": "#/components/schemas/Person"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Editors",
"description": "List of person objects containing the editors of the reference.",
- "x-optimade-support": "optional",
- "x-optimade-queryable": "optional"
+ "x-optimade-queryable": "optional",
+ "x-optimade-support": "optional"
},
"doi": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Doi",
"description": "The digital object identifier of the reference.",
- "x-optimade-support": "optional",
- "x-optimade-queryable": "optional"
+ "x-optimade-queryable": "optional",
+ "x-optimade-support": "optional"
},
"url": {
- "type": "string",
- "maxLength": 65536,
- "minLength": 1,
- "format": "uri",
+ "anyOf": [
+ {
+ "type": "string",
+ "minLength": 1,
+ "format": "uri"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Url",
"description": "The URL of the reference.",
- "x-optimade-support": "optional",
- "x-optimade-queryable": "optional"
+ "x-optimade-queryable": "optional",
+ "x-optimade-support": "optional"
},
"address": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Address",
"description": "Meaning of property matches the BiBTeX specification.",
- "x-optimade-support": "optional",
- "x-optimade-queryable": "optional"
+ "x-optimade-queryable": "optional",
+ "x-optimade-support": "optional"
},
"annote": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Annote",
"description": "Meaning of property matches the BiBTeX specification.",
- "x-optimade-support": "optional",
- "x-optimade-queryable": "optional"
+ "x-optimade-queryable": "optional",
+ "x-optimade-support": "optional"
},
"booktitle": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Booktitle",
"description": "Meaning of property matches the BiBTeX specification.",
- "x-optimade-support": "optional",
- "x-optimade-queryable": "optional"
+ "x-optimade-queryable": "optional",
+ "x-optimade-support": "optional"
},
"chapter": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Chapter",
"description": "Meaning of property matches the BiBTeX specification.",
- "x-optimade-support": "optional",
- "x-optimade-queryable": "optional"
+ "x-optimade-queryable": "optional",
+ "x-optimade-support": "optional"
},
"crossref": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Crossref",
"description": "Meaning of property matches the BiBTeX specification.",
- "x-optimade-support": "optional",
- "x-optimade-queryable": "optional"
+ "x-optimade-queryable": "optional",
+ "x-optimade-support": "optional"
},
"edition": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Edition",
"description": "Meaning of property matches the BiBTeX specification.",
- "x-optimade-support": "optional",
- "x-optimade-queryable": "optional"
+ "x-optimade-queryable": "optional",
+ "x-optimade-support": "optional"
},
"howpublished": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Howpublished",
"description": "Meaning of property matches the BiBTeX specification.",
- "x-optimade-support": "optional",
- "x-optimade-queryable": "optional"
+ "x-optimade-queryable": "optional",
+ "x-optimade-support": "optional"
},
"institution": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Institution",
"description": "Meaning of property matches the BiBTeX specification.",
- "x-optimade-support": "optional",
- "x-optimade-queryable": "optional"
+ "x-optimade-queryable": "optional",
+ "x-optimade-support": "optional"
},
"journal": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Journal",
"description": "Meaning of property matches the BiBTeX specification.",
- "x-optimade-support": "optional",
- "x-optimade-queryable": "optional"
+ "x-optimade-queryable": "optional",
+ "x-optimade-support": "optional"
},
"key": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Key",
"description": "Meaning of property matches the BiBTeX specification.",
- "x-optimade-support": "optional",
- "x-optimade-queryable": "optional"
+ "x-optimade-queryable": "optional",
+ "x-optimade-support": "optional"
},
"month": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Month",
"description": "Meaning of property matches the BiBTeX specification.",
- "x-optimade-support": "optional",
- "x-optimade-queryable": "optional"
+ "x-optimade-queryable": "optional",
+ "x-optimade-support": "optional"
},
"note": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Note",
"description": "Meaning of property matches the BiBTeX specification.",
- "x-optimade-support": "optional",
- "x-optimade-queryable": "optional"
+ "x-optimade-queryable": "optional",
+ "x-optimade-support": "optional"
},
"number": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Number",
"description": "Meaning of property matches the BiBTeX specification.",
- "x-optimade-support": "optional",
- "x-optimade-queryable": "optional"
+ "x-optimade-queryable": "optional",
+ "x-optimade-support": "optional"
},
"organization": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Organization",
"description": "Meaning of property matches the BiBTeX specification.",
- "x-optimade-support": "optional",
- "x-optimade-queryable": "optional"
+ "x-optimade-queryable": "optional",
+ "x-optimade-support": "optional"
},
"pages": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Pages",
"description": "Meaning of property matches the BiBTeX specification.",
- "x-optimade-support": "optional",
- "x-optimade-queryable": "optional"
+ "x-optimade-queryable": "optional",
+ "x-optimade-support": "optional"
},
"publisher": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Publisher",
"description": "Meaning of property matches the BiBTeX specification.",
- "x-optimade-support": "optional",
- "x-optimade-queryable": "optional"
+ "x-optimade-queryable": "optional",
+ "x-optimade-support": "optional"
},
"school": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "School",
"description": "Meaning of property matches the BiBTeX specification.",
- "x-optimade-support": "optional",
- "x-optimade-queryable": "optional"
+ "x-optimade-queryable": "optional",
+ "x-optimade-support": "optional"
},
"series": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Series",
"description": "Meaning of property matches the BiBTeX specification.",
- "x-optimade-support": "optional",
- "x-optimade-queryable": "optional"
+ "x-optimade-queryable": "optional",
+ "x-optimade-support": "optional"
},
"title": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Title",
"description": "Meaning of property matches the BiBTeX specification.",
- "x-optimade-support": "optional",
- "x-optimade-queryable": "optional"
+ "x-optimade-queryable": "optional",
+ "x-optimade-support": "optional"
},
"bib_type": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Bib Type",
"description": "Type of the reference, corresponding to the **type** property in the BiBTeX specification.",
- "x-optimade-support": "optional",
- "x-optimade-queryable": "optional"
+ "x-optimade-queryable": "optional",
+ "x-optimade-support": "optional"
},
"volume": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Volume",
"description": "Meaning of property matches the BiBTeX specification.",
- "x-optimade-support": "optional",
- "x-optimade-queryable": "optional"
+ "x-optimade-queryable": "optional",
+ "x-optimade-support": "optional"
},
"year": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Year",
"description": "Meaning of property matches the BiBTeX specification.",
- "x-optimade-support": "optional",
- "x-optimade-queryable": "optional"
+ "x-optimade-queryable": "optional",
+ "x-optimade-support": "optional"
}
},
+ "additionalProperties": true,
"type": "object",
"required": [
"last_modified"
@@ -2927,14 +3417,20 @@
"$ref": "#/components/schemas/ResponseMeta"
}
],
- "title": "Meta",
"description": "A meta object containing non-standard information"
},
"errors": {
- "items": {
- "$ref": "#/components/schemas/Error"
- },
- "type": "array",
+ "anyOf": [
+ {
+ "items": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
"uniqueItems": true,
"title": "Errors",
"description": "A list of unique errors"
@@ -2952,27 +3448,35 @@
"type": "object"
},
"type": "array"
+ },
+ {
+ "type": "null"
}
],
"uniqueItems": true,
- "title": "Included"
+ "title": "Included",
+ "description": "A list of unique included OPTIMADE entry resources."
},
"links": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/ToplevelLinks"
+ },
+ {
+ "type": "null"
}
],
- "title": "Links",
"description": "Links associated with the primary data or errors"
},
"jsonapi": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/JsonApi"
+ },
+ {
+ "type": "null"
}
],
- "title": "Jsonapi",
"description": "Information about the JSON API used"
}
},
@@ -2981,8 +3485,7 @@
"data",
"meta"
],
- "title": "ReferenceResponseMany",
- "description": "errors are not allowed"
+ "title": "ReferenceResponseMany"
},
"ReferenceResponseOne": {
"properties": {
@@ -2993,6 +3496,9 @@
},
{
"type": "object"
+ },
+ {
+ "type": "null"
}
],
"title": "Data",
@@ -3004,14 +3510,20 @@
"$ref": "#/components/schemas/ResponseMeta"
}
],
- "title": "Meta",
"description": "A meta object containing non-standard information"
},
"errors": {
- "items": {
- "$ref": "#/components/schemas/Error"
- },
- "type": "array",
+ "anyOf": [
+ {
+ "items": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
"uniqueItems": true,
"title": "Errors",
"description": "A list of unique errors"
@@ -3029,27 +3541,35 @@
"type": "object"
},
"type": "array"
+ },
+ {
+ "type": "null"
}
],
"uniqueItems": true,
- "title": "Included"
+ "title": "Included",
+ "description": "A list of unique included OPTIMADE entry resources."
},
"links": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/ToplevelLinks"
+ },
+ {
+ "type": "null"
}
],
- "title": "Links",
"description": "Links associated with the primary data or errors"
},
"jsonapi": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/JsonApi"
+ },
+ {
+ "type": "null"
}
],
- "title": "Jsonapi",
"description": "Information about the JSON API used"
}
},
@@ -3058,8 +3578,7 @@
"data",
"meta"
],
- "title": "ReferenceResponseOne",
- "description": "errors are not allowed"
+ "title": "ReferenceResponseOne"
},
"RelationshipLinks": {
"properties": {
@@ -3067,12 +3586,14 @@
"anyOf": [
{
"type": "string",
- "maxLength": 65536,
"minLength": 1,
"format": "uri"
},
{
"$ref": "#/components/schemas/Link"
+ },
+ {
+ "type": "null"
}
],
"title": "Self",
@@ -3082,12 +3603,14 @@
"anyOf": [
{
"type": "string",
- "maxLength": 65536,
"minLength": 1,
"format": "uri"
},
{
"$ref": "#/components/schemas/Link"
+ },
+ {
+ "type": "null"
}
],
"title": "Related",
@@ -3117,39 +3640,47 @@
"description": "Resource type"
},
"links": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/ResourceLinks"
+ },
+ {
+ "type": "null"
}
],
- "title": "Links",
"description": "a links object containing links related to the resource."
},
"meta": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/Meta"
+ },
+ {
+ "type": "null"
}
],
- "title": "Meta",
"description": "a meta object containing non-standard meta-information about a resource that can not be represented as an attribute or relationship."
},
"attributes": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/Attributes"
+ },
+ {
+ "type": "null"
}
],
- "title": "Attributes",
"description": "an attributes object representing some of the resource\u2019s data."
},
"relationships": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/Relationships"
+ },
+ {
+ "type": "null"
}
],
- "title": "Relationships",
"description": "[Relationships object](https://jsonapi.org/format/1.0/#document-resource-object-relationships)\ndescribing relationships between the resource and other JSON API resources."
}
},
@@ -3167,12 +3698,14 @@
"anyOf": [
{
"type": "string",
- "maxLength": 65536,
"minLength": 1,
"format": "uri"
},
{
"$ref": "#/components/schemas/Link"
+ },
+ {
+ "type": "null"
}
],
"title": "Self",
@@ -3191,7 +3724,6 @@
"$ref": "#/components/schemas/ResponseMetaQuery"
}
],
- "title": "Query",
"description": "Information on the Query that was requested"
},
"api_version": {
@@ -3199,7 +3731,7 @@
"pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$",
"title": "Api Version",
"description": "Presently used full version of the OPTIMADE API.\nThe version number string MUST NOT be prefixed by, e.g., \"v\".\nExamples: `1.0.0`, `1.0.0-rc.2`.",
- "example": [
+ "examples": [
"0.10.1",
"1.0.0-rc.2",
"1.2.3-rc.5+develop"
@@ -3214,72 +3746,121 @@
"anyOf": [
{
"type": "string",
- "maxLength": 65536,
"minLength": 1,
"format": "uri"
},
{
"$ref": "#/components/schemas/Link"
+ },
+ {
+ "type": "null"
}
],
"title": "Schema",
"description": "A [JSON API links object](http://jsonapi.org/format/1.0/#document-links) that points to a schema for the response.\nIf it is a string, or a dictionary containing no `meta` field, the provided URL MUST point at an [OpenAPI](https://swagger.io/specification/) schema.\nIt is possible that future versions of this specification allows for alternative schema types.\nHence, if the `meta` field of the JSON API links object is provided and contains a field `schema_type` that is not equal to the string `OpenAPI` the client MUST not handle failures to parse the schema or to validate the response against the schema as errors."
},
"time_stamp": {
- "type": "string",
- "format": "date-time",
+ "anyOf": [
+ {
+ "type": "string",
+ "format": "date-time"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Time Stamp",
"description": "A timestamp containing the date and time at which the query was executed."
},
"data_returned": {
- "type": "integer",
- "minimum": 0.0,
+ "anyOf": [
+ {
+ "type": "integer",
+ "minimum": 0.0
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Data Returned",
"description": "An integer containing the total number of data resource objects returned for the current `filter` query, independent of pagination."
},
"provider": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/Provider"
+ },
+ {
+ "type": "null"
}
],
- "title": "Provider",
"description": "information on the database provider of the implementation."
},
"data_available": {
- "type": "integer",
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Data Available",
"description": "An integer containing the total number of data resource objects available in the database for the endpoint."
},
"last_id": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Last Id",
"description": "a string containing the last ID returned"
},
"response_message": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Response Message",
"description": "response string from the server"
},
"implementation": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/Implementation"
+ },
+ {
+ "type": "null"
}
],
- "title": "Implementation",
"description": "a dictionary describing the server implementation"
},
"warnings": {
- "items": {
- "$ref": "#/components/schemas/Warnings"
- },
- "type": "array",
+ "anyOf": [
+ {
+ "items": {
+ "$ref": "#/components/schemas/Warnings"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
"uniqueItems": true,
"title": "Warnings",
"description": "A list of warning resource objects representing non-critical errors or warnings.\nA warning resource object is defined similarly to a [JSON API error object](http://jsonapi.org/format/1.0/#error-objects), but MUST also include the field `type`, which MUST have the value `\"warning\"`.\nThe field `detail` MUST be present and SHOULD contain a non-critical message, e.g., reporting unrecognized search attributes or deprecated features.\nThe field `status`, representing a HTTP response status code, MUST NOT be present for a warning resource object.\nThis is an exclusive field for error resource objects."
}
},
+ "additionalProperties": true,
"type": "object",
"required": [
"query",
@@ -3310,18 +3891,19 @@
"type": "string",
"title": "Name",
"description": "Gives the name of the species; the **name** value MUST be unique in the `species` list.",
- "x-optimade-support": "must",
- "x-optimade-queryable": "optional"
+ "x-optimade-queryable": "optional",
+ "x-optimade-support": "must"
},
"chemical_symbols": {
"items": {
- "type": "string"
+ "type": "string",
+ "pattern": "(H|He|Li|Be|B|C|N|O|F|Ne|Na|Mg|Al|Si|P|S|Cl|Ar|K|Ca|Sc|Ti|V|Cr|Mn|Fe|Co|Ni|Cu|Zn|Ga|Ge|As|Se|Br|Kr|Rb|Sr|Y|Zr|Nb|Mo|Tc|Ru|Rh|Pd|Ag|Cd|In|Sn|Sb|Te|I|Xe|Cs|Ba|La|Ce|Pr|Nd|Pm|Sm|Eu|Gd|Tb|Dy|Ho|Er|Tm|Yb|Lu|Hf|Ta|W|Re|Os|Ir|Pt|Au|Hg|Tl|Pb|Bi|Po|At|Rn|Fr|Ra|Ac|Th|Pa|U|Np|Pu|Am|Cm|Bk|Cf|Es|Fm|Md|No|Lr|Rf|Db|Sg|Bh|Hs|Mt|Ds|Rg|Cn|Nh|Fl|Mc|Lv|Ts|Og|X|vacancy)"
},
"type": "array",
"title": "Chemical Symbols",
"description": "MUST be a list of strings of all chemical elements composing this species. Each item of the list MUST be one of the following:\n\n- a valid chemical-element symbol, or\n- the special value `\"X\"` to represent a non-chemical element, or\n- the special value `\"vacancy\"` to represent that this site has a non-zero probability of having a vacancy (the respective probability is indicated in the `concentration` list, see below).\n\nIf any one entry in the `species` list has a `chemical_symbols` list that is longer than 1 element, the correct flag MUST be set in the list `structure_features`.",
- "x-optimade-support": "must",
- "x-optimade-queryable": "optional"
+ "x-optimade-queryable": "optional",
+ "x-optimade-support": "must"
},
"concentration": {
"items": {
@@ -3330,46 +3912,74 @@
"type": "array",
"title": "Concentration",
"description": "MUST be a list of floats, with same length as `chemical_symbols`. The numbers represent the relative concentration of the corresponding chemical symbol in this species. The numbers SHOULD sum to one. Cases in which the numbers do not sum to one typically fall only in the following two categories:\n\n- Numerical errors when representing float numbers in fixed precision, e.g. for two chemical symbols with concentrations `1/3` and `2/3`, the concentration might look something like `[0.33333333333, 0.66666666666]`. If the client is aware that the sum is not one because of numerical precision, it can renormalize the values so that the sum is exactly one.\n- Experimental errors in the data present in the database. In this case, it is the responsibility of the client to decide how to process the data.\n\nNote that concentrations are uncorrelated between different site (even of the same species).",
- "x-optimade-support": "must",
- "x-optimade-queryable": "optional"
+ "x-optimade-queryable": "optional",
+ "x-optimade-support": "must"
},
"mass": {
- "items": {
- "type": "number"
- },
- "type": "array",
+ "anyOf": [
+ {
+ "items": {
+ "type": "number"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Mass",
"description": "If present MUST be a list of floats expressed in a.m.u.\nElements denoting vacancies MUST have masses equal to 0.",
- "x-optimade-support": "optional",
"x-optimade-queryable": "optional",
+ "x-optimade-support": "optional",
"x-optimade-unit": "a.m.u."
},
"original_name": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Original Name",
"description": "Can be any valid Unicode string, and SHOULD contain (if specified) the name of the species that is used internally in the source database.\n\nNote: With regards to \"source database\", we refer to the immediate source being queried via the OPTIMADE API implementation.",
- "x-optimade-support": "optional",
- "x-optimade-queryable": "optional"
+ "x-optimade-queryable": "optional",
+ "x-optimade-support": "optional"
},
"attached": {
- "items": {
- "type": "string"
- },
- "type": "array",
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Attached",
"description": "If provided MUST be a list of length 1 or more of strings of chemical symbols for the elements attached to this site, or \"X\" for a non-chemical element.",
- "x-optimade-support": "optional",
- "x-optimade-queryable": "optional"
+ "x-optimade-queryable": "optional",
+ "x-optimade-support": "optional"
},
"nattached": {
- "items": {
- "type": "integer"
- },
- "type": "array",
+ "anyOf": [
+ {
+ "items": {
+ "type": "integer"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Nattached",
"description": "If provided MUST be a list of length 1 or more of integers indicating the number of attached atoms of the kind specified in the value of the :field:`attached` key.",
- "x-optimade-support": "optional",
- "x-optimade-queryable": "optional"
+ "x-optimade-queryable": "optional",
+ "x-optimade-support": "optional"
}
},
"type": "object",
@@ -3382,6 +3992,7 @@
"description": "A list describing the species of the sites of this structure.\n\nSpecies can represent pure chemical elements, virtual-crystal atoms representing a\nstatistical occupation of a given site by multiple chemical elements, and/or a\nlocation to which there are attached atoms, i.e., atoms whose precise location are\nunknown beyond that they are attached to that position (frequently used to indicate\nhydrogen atoms attached to another element, e.g., a carbon with three attached\nhydrogens might represent a methyl group, -CH3).\n\n- **Examples**:\n - `[ {\"name\": \"Ti\", \"chemical_symbols\": [\"Ti\"], \"concentration\": [1.0]} ]`: any site with this species is occupied by a Ti atom.\n - `[ {\"name\": \"Ti\", \"chemical_symbols\": [\"Ti\", \"vacancy\"], \"concentration\": [0.9, 0.1]} ]`: any site with this species is occupied by a Ti atom with 90 % probability, and has a vacancy with 10 % probability.\n - `[ {\"name\": \"BaCa\", \"chemical_symbols\": [\"vacancy\", \"Ba\", \"Ca\"], \"concentration\": [0.05, 0.45, 0.5], \"mass\": [0.0, 137.327, 40.078]} ]`: any site with this species is occupied by a Ba atom with 45 % probability, a Ca atom with 50 % probability, and by a vacancy with 5 % probability. The mass of this site is (on average) 88.5 a.m.u.\n - `[ {\"name\": \"C12\", \"chemical_symbols\": [\"C\"], \"concentration\": [1.0], \"mass\": [12.0]} ]`: any site with this species is occupied by a carbon isotope with mass 12.\n - `[ {\"name\": \"C13\", \"chemical_symbols\": [\"C\"], \"concentration\": [1.0], \"mass\": [13.0]} ]`: any site with this species is occupied by a carbon isotope with mass 13.\n - `[ {\"name\": \"CH3\", \"chemical_symbols\": [\"C\"], \"concentration\": [1.0], \"attached\": [\"H\"], \"nattached\": [3]} ]`: any site with this species is occupied by a methyl group, -CH3, which is represented without specifying precise positions of the hydrogen atoms."
},
"StructureFeatures": {
+ "type": "string",
"enum": [
"disorder",
"implicit_atoms",
@@ -3394,12 +4005,14 @@
"StructureRelationship": {
"properties": {
"links": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/RelationshipLinks"
+ },
+ {
+ "type": "null"
}
],
- "title": "Links",
"description": "a links object containing at least one of the following: self, related"
},
"data": {
@@ -3412,6 +4025,9 @@
"$ref": "#/components/schemas/BaseRelationshipResource"
},
"type": "array"
+ },
+ {
+ "type": "null"
}
],
"uniqueItems": true,
@@ -3419,18 +4035,19 @@
"description": "Resource linkage"
},
"meta": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/Meta"
+ },
+ {
+ "type": "null"
}
],
- "title": "Meta",
"description": "a meta object that contains non-standard meta-information about the relationship."
}
},
"type": "object",
- "title": "StructureRelationship",
- "description": "Similar to normal JSON API relationship, but with addition of OPTIONAL meta field for a resource."
+ "title": "StructureRelationship"
},
"StructureResource": {
"properties": {
@@ -3438,46 +4055,56 @@
"type": "string",
"title": "Id",
"description": "An entry's ID as defined in section Definition of Terms.\n\n- **Type**: string.\n\n- **Requirements/Conventions**:\n - **Support**: MUST be supported by all implementations, MUST NOT be `null`.\n - **Query**: MUST be a queryable property with support for all mandatory filter features.\n - **Response**: REQUIRED in the response.\n\n- **Examples**:\n - `\"db/1234567\"`\n - `\"cod/2000000\"`\n - `\"cod/2000000@1234567\"`\n - `\"nomad/L1234567890\"`\n - `\"42\"`",
- "x-optimade-support": "must",
- "x-optimade-queryable": "must"
+ "x-optimade-queryable": "must",
+ "x-optimade-support": "must"
},
"type": {
"type": "string",
+ "enum": [
+ "structures"
+ ],
+ "const": "structures",
"pattern": "^structures$",
"title": "Type",
"description": "The name of the type of an entry.\n\n- **Type**: string.\n\n- **Requirements/Conventions**:\n - **Support**: MUST be supported by all implementations, MUST NOT be `null`.\n - **Query**: MUST be a queryable property with support for all mandatory filter features.\n - **Response**: REQUIRED in the response.\n - MUST be an existing entry type.\n - The entry of type `` and ID `` MUST be returned in response to a request for `//` under the versioned base URL.\n\n- **Examples**:\n - `\"structures\"`",
"default": "structures",
- "x-optimade-support": "must",
- "x-optimade-queryable": "must"
+ "x-optimade-queryable": "must",
+ "x-optimade-support": "must"
},
"links": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/ResourceLinks"
+ },
+ {
+ "type": "null"
}
],
- "title": "Links",
"description": "a links object containing links related to the resource."
},
"meta": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/Meta"
+ },
+ {
+ "type": "null"
}
],
- "title": "Meta",
"description": "a meta object containing non-standard meta-information about a resource that can not be represented as an attribute or relationship."
},
"attributes": {
"$ref": "#/components/schemas/StructureResourceAttributes"
},
"relationships": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/EntryRelationships"
+ },
+ {
+ "type": "null"
}
],
- "title": "Relationships",
"description": "A dictionary containing references to other entries according to the description in section Relationships encoded as [JSON API Relationships](https://jsonapi.org/format/1.0/#document-resource-object-relationships).\nThe OPTIONAL human-readable description of the relationship MAY be provided in the `description` field inside the `meta` dictionary of the JSON API resource identifier object."
}
},
@@ -3493,181 +4120,372 @@
"StructureResourceAttributes": {
"properties": {
"immutable_id": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Immutable Id",
"description": "The entry's immutable ID (e.g., an UUID). This is important for databases having preferred IDs that point to \"the latest version\" of a record, but still offer access to older variants. This ID maps to the version-specific record, in case it changes in the future.\n\n- **Type**: string.\n\n- **Requirements/Conventions**:\n - **Support**: OPTIONAL support in implementations, i.e., MAY be `null`.\n - **Query**: MUST be a queryable property with support for all mandatory filter features.\n\n- **Examples**:\n - `\"8bd3e750-b477-41a0-9b11-3a799f21b44f\"`\n - `\"fjeiwoj,54;@=%<>#32\"` (Strings that are not URL-safe are allowed.)",
- "x-optimade-support": "optional",
- "x-optimade-queryable": "must"
+ "x-optimade-queryable": "must",
+ "x-optimade-support": "optional"
},
"last_modified": {
- "type": "string",
- "format": "date-time",
+ "anyOf": [
+ {
+ "type": "string",
+ "format": "date-time"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Last Modified",
"description": "Date and time representing when the entry was last modified.\n\n- **Type**: timestamp.\n\n- **Requirements/Conventions**:\n - **Support**: SHOULD be supported by all implementations, i.e., SHOULD NOT be `null`.\n - **Query**: MUST be a queryable property with support for all mandatory filter features.\n - **Response**: REQUIRED in the response unless the query parameter `response_fields` is present and does not include this property.\n\n- **Example**:\n - As part of JSON response format: `\"2007-04-05T14:30:20Z\"` (i.e., encoded as an [RFC 3339 Internet Date/Time Format](https://tools.ietf.org/html/rfc3339#section-5.6) string.)",
- "x-optimade-support": "should",
"x-optimade-queryable": "must",
- "nullable": true
+ "x-optimade-support": "should"
},
"elements": {
- "items": {
- "type": "string"
- },
- "type": "array",
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Elements",
"description": "The chemical symbols of the different elements present in the structure.\n\n- **Type**: list of strings.\n\n- **Requirements/Conventions**:\n - **Support**: SHOULD be supported by all implementations, i.e., SHOULD NOT be `null`.\n - **Query**: MUST be a queryable property with support for all mandatory filter features.\n - The strings are the chemical symbols, i.e., either a single uppercase letter or an uppercase letter followed by a number of lowercase letters.\n - The order MUST be alphabetical.\n - MUST refer to the same elements in the same order, and therefore be of the same length, as `elements_ratios`, if the latter is provided.\n - Note: This property SHOULD NOT contain the string \"X\" to indicate non-chemical elements or \"vacancy\" to indicate vacancies (in contrast to the field `chemical_symbols` for the `species` property).\n\n- **Examples**:\n - `[\"Si\"]`\n - `[\"Al\",\"O\",\"Si\"]`\n\n- **Query examples**:\n - A filter that matches all records of structures that contain Si, Al **and** O, and possibly other elements: `elements HAS ALL \"Si\", \"Al\", \"O\"`.\n - To match structures with exactly these three elements, use `elements HAS ALL \"Si\", \"Al\", \"O\" AND elements LENGTH 3`.\n - Note: length queries on this property can be equivalently formulated by filtering on the `nelements`_ property directly.",
- "x-optimade-support": "should",
"x-optimade-queryable": "must",
- "nullable": true
+ "x-optimade-support": "should"
},
"nelements": {
- "type": "integer",
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Nelements",
"description": "Number of different elements in the structure as an integer.\n\n- **Type**: integer\n\n- **Requirements/Conventions**:\n - **Support**: SHOULD be supported by all implementations, i.e., SHOULD NOT be `null`.\n - **Query**: MUST be a queryable property with support for all mandatory filter features.\n - MUST be equal to the lengths of the list properties `elements` and `elements_ratios`, if they are provided.\n\n- **Examples**:\n - `3`\n\n- **Querying**:\n - Note: queries on this property can equivalently be formulated using `elements LENGTH`.\n - A filter that matches structures that have exactly 4 elements: `nelements=4`.\n - A filter that matches structures that have between 2 and 7 elements: `nelements>=2 AND nelements<=7`.",
- "x-optimade-support": "should",
"x-optimade-queryable": "must",
- "nullable": true
+ "x-optimade-support": "should"
},
"elements_ratios": {
- "items": {
- "type": "number"
- },
- "type": "array",
+ "anyOf": [
+ {
+ "items": {
+ "type": "number"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Elements Ratios",
"description": "Relative proportions of different elements in the structure.\n\n- **Type**: list of floats\n\n- **Requirements/Conventions**:\n - **Support**: SHOULD be supported by all implementations, i.e., SHOULD NOT be `null`.\n - **Query**: MUST be a queryable property with support for all mandatory filter features.\n - Composed by the proportions of elements in the structure as a list of floating point numbers.\n - The sum of the numbers MUST be 1.0 (within floating point accuracy)\n - MUST refer to the same elements in the same order, and therefore be of the same length, as `elements`, if the latter is provided.\n\n- **Examples**:\n - `[1.0]`\n - `[0.3333333333333333, 0.2222222222222222, 0.4444444444444444]`\n\n- **Query examples**:\n - Note: Useful filters can be formulated using the set operator syntax for correlated values.\n However, since the values are floating point values, the use of equality comparisons is generally inadvisable.\n - OPTIONAL: a filter that matches structures where approximately 1/3 of the atoms in the structure are the element Al is: `elements:elements_ratios HAS ALL \"Al\":>0.3333, \"Al\":<0.3334`.",
- "x-optimade-support": "should",
"x-optimade-queryable": "must",
- "nullable": true
+ "x-optimade-support": "should"
},
"chemical_formula_descriptive": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Chemical Formula Descriptive",
"description": "The chemical formula for a structure as a string in a form chosen by the API implementation.\n\n- **Type**: string\n\n- **Requirements/Conventions**:\n - **Support**: SHOULD be supported by all implementations, i.e., SHOULD NOT be `null`.\n - **Query**: MUST be a queryable property with support for all mandatory filter features.\n - The chemical formula is given as a string consisting of properly capitalized element symbols followed by integers or decimal numbers, balanced parentheses, square, and curly brackets `(`,`)`, `[`,`]`, `{`, `}`, commas, the `+`, `-`, `:` and `=` symbols. The parentheses are allowed to be followed by a number. Spaces are allowed anywhere except within chemical symbols. The order of elements and any groupings indicated by parentheses or brackets are chosen freely by the API implementation.\n - The string SHOULD be arithmetically consistent with the element ratios in the `chemical_formula_reduced` property.\n - It is RECOMMENDED, but not mandatory, that symbols, parentheses and brackets, if used, are used with the meanings prescribed by [IUPAC's Nomenclature of Organic Chemistry](https://www.qmul.ac.uk/sbcs/iupac/bibliog/blue.html).\n\n- **Examples**:\n - `\"(H2O)2 Na\"`\n - `\"NaCl\"`\n - `\"CaCO3\"`\n - `\"CCaO3\"`\n - `\"(CH3)3N+ - [CH2]2-OH = Me3N+ - CH2 - CH2OH\"`\n\n- **Query examples**:\n - Note: the free-form nature of this property is likely to make queries on it across different databases inconsistent.\n - A filter that matches an exactly given formula: `chemical_formula_descriptive=\"(H2O)2 Na\"`.\n - A filter that does a partial match: `chemical_formula_descriptive CONTAINS \"H2O\"`.",
- "x-optimade-support": "should",
"x-optimade-queryable": "must",
- "nullable": true
+ "x-optimade-support": "should"
},
"chemical_formula_reduced": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string",
+ "pattern": "(^$)|^([A-Z][a-z]?([2-9]|[1-9]\\d+)?)+$"
+ },
+ {
+ "type": "null"
+ }
+ ],
"pattern": "(^$)|^([A-Z][a-z]?([2-9]|[1-9]\\d+)?)+$",
"title": "Chemical Formula Reduced",
"description": "The reduced chemical formula for a structure as a string with element symbols and integer chemical proportion numbers.\nThe proportion number MUST be omitted if it is 1.\n\n- **Type**: string\n\n- **Requirements/Conventions**:\n - **Support**: SHOULD be supported by all implementations, i.e., SHOULD NOT be `null`.\n - **Query**: MUST be a queryable property.\n However, support for filters using partial string matching with this property is OPTIONAL (i.e., BEGINS WITH, ENDS WITH, and CONTAINS).\n Intricate queries on formula components are instead suggested to be formulated using set-type filter operators on the multi valued `elements` and `elements_ratios` properties.\n - Element symbols MUST have proper capitalization (e.g., `\"Si\"`, not `\"SI\"` for \"silicon\").\n - Elements MUST be placed in alphabetical order, followed by their integer chemical proportion number.\n - For structures with no partial occupation, the chemical proportion numbers are the smallest integers for which the chemical proportion is exactly correct.\n - For structures with partial occupation, the chemical proportion numbers are integers that within reasonable approximation indicate the correct chemical proportions. The precise details of how to perform the rounding is chosen by the API implementation.\n - No spaces or separators are allowed.\n\n- **Examples**:\n - `\"H2NaO\"`\n - `\"ClNa\"`\n - `\"CCaO3\"`\n\n- **Query examples**:\n - A filter that matches an exactly given formula is `chemical_formula_reduced=\"H2NaO\"`.",
- "x-optimade-support": "should",
"x-optimade-queryable": "must",
- "nullable": true
+ "x-optimade-support": "should"
},
"chemical_formula_hill": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string",
+ "pattern": "(^$)|^([A-Z][a-z]?([2-9]|[1-9]\\d+)?)+$"
+ },
+ {
+ "type": "null"
+ }
+ ],
"pattern": "(^$)|^([A-Z][a-z]?([2-9]|[1-9]\\d+)?)+$",
"title": "Chemical Formula Hill",
"description": "The chemical formula for a structure in [Hill form](https://dx.doi.org/10.1021/ja02046a005) with element symbols followed by integer chemical proportion numbers. The proportion number MUST be omitted if it is 1.\n\n- **Type**: string\n\n- **Requirements/Conventions**:\n - **Support**: OPTIONAL support in implementations, i.e., MAY be `null`.\n - **Query**: Support for queries on this property is OPTIONAL.\n If supported, only a subset of the filter features MAY be supported.\n - The overall scale factor of the chemical proportions is chosen such that the resulting values are integers that indicate the most chemically relevant unit of which the system is composed.\n For example, if the structure is a repeating unit cell with four hydrogens and four oxygens that represents two hydroperoxide molecules, `chemical_formula_hill` is `\"H2O2\"` (i.e., not `\"HO\"`, nor `\"H4O4\"`).\n - If the chemical insight needed to ascribe a Hill formula to the system is not present, the property MUST be handled as unset.\n - Element symbols MUST have proper capitalization (e.g., `\"Si\"`, not `\"SI\"` for \"silicon\").\n - Elements MUST be placed in [Hill order](https://dx.doi.org/10.1021/ja02046a005), followed by their integer chemical proportion number.\n Hill order means: if carbon is present, it is placed first, and if also present, hydrogen is placed second.\n After that, all other elements are ordered alphabetically.\n If carbon is not present, all elements are ordered alphabetically.\n - If the system has sites with partial occupation and the total occupations of each element do not all sum up to integers, then the Hill formula SHOULD be handled as unset.\n - No spaces or separators are allowed.\n\n- **Examples**:\n - `\"H2O2\"`\n\n- **Query examples**:\n - A filter that matches an exactly given formula is `chemical_formula_hill=\"H2O2\"`.",
- "x-optimade-support": "optional",
- "x-optimade-queryable": "optional"
+ "x-optimade-queryable": "optional",
+ "x-optimade-support": "optional"
},
"chemical_formula_anonymous": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string",
+ "pattern": "(^$)|^([A-Z][a-z]?([2-9]|[1-9]\\d+)?)+$"
+ },
+ {
+ "type": "null"
+ }
+ ],
"pattern": "(^$)|^([A-Z][a-z]?([2-9]|[1-9]\\d+)?)+$",
"title": "Chemical Formula Anonymous",
"description": "The anonymous formula is the `chemical_formula_reduced`, but where the elements are instead first ordered by their chemical proportion number, and then, in order left to right, replaced by anonymous symbols A, B, C, ..., Z, Aa, Ba, ..., Za, Ab, Bb, ... and so on.\n\n- **Type**: string\n\n- **Requirements/Conventions**:\n - **Support**: SHOULD be supported by all implementations, i.e., SHOULD NOT be `null`.\n - **Query**: MUST be a queryable property.\n However, support for filters using partial string matching with this property is OPTIONAL (i.e., BEGINS WITH, ENDS WITH, and CONTAINS).\n\n- **Examples**:\n - `\"A2B\"`\n - `\"A42B42C16D12E10F9G5\"`\n\n- **Querying**:\n - A filter that matches an exactly given formula is `chemical_formula_anonymous=\"A2B\"`.",
- "x-optimade-support": "should",
"x-optimade-queryable": "must",
- "nullable": true
+ "x-optimade-support": "should"
},
"dimension_types": {
- "items": {
- "$ref": "#/components/schemas/Periodicity"
- },
- "type": "array",
- "maxItems": 3,
- "minItems": 3,
+ "anyOf": [
+ {
+ "items": {
+ "$ref": "#/components/schemas/Periodicity"
+ },
+ "type": "array",
+ "maxItems": 3,
+ "minItems": 3
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Dimension Types",
"description": "List of three integers.\nFor each of the three directions indicated by the three lattice vectors (see property `lattice_vectors`), this list indicates if the direction is periodic (value `1`) or non-periodic (value `0`).\nNote: the elements in this list each refer to the direction of the corresponding entry in `lattice_vectors` and *not* the Cartesian x, y, z directions.\n\n- **Type**: list of integers.\n\n- **Requirements/Conventions**:\n - **Support**: SHOULD be supported by all implementations, i.e., SHOULD NOT be `null`.\n - **Query**: Support for queries on this property is OPTIONAL.\n - MUST be a list of length 3.\n - Each integer element MUST assume only the value 0 or 1.\n\n- **Examples**:\n - For a molecule: `[0, 0, 0]`\n - For a wire along the direction specified by the third lattice vector: `[0, 0, 1]`\n - For a 2D surface/slab, periodic on the plane defined by the first and third lattice vectors: `[1, 0, 1]`\n - For a bulk 3D system: `[1, 1, 1]`",
- "x-optimade-support": "should",
"x-optimade-queryable": "optional",
- "nullable": true
+ "x-optimade-support": "should"
},
"nperiodic_dimensions": {
- "type": "integer",
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Nperiodic Dimensions",
"description": "An integer specifying the number of periodic dimensions in the structure, equivalent to the number of non-zero entries in `dimension_types`.\n\n- **Type**: integer\n\n- **Requirements/Conventions**:\n - **Support**: SHOULD be supported by all implementations, i.e., SHOULD NOT be `null`.\n - **Query**: MUST be a queryable property with support for all mandatory filter features.\n - The integer value MUST be between 0 and 3 inclusive and MUST be equal to the sum of the items in the `dimension_types` property.\n - This property only reflects the treatment of the lattice vectors provided for the structure, and not any physical interpretation of the dimensionality of its contents.\n\n- **Examples**:\n - `2` should be indicated in cases where `dimension_types` is any of `[1, 1, 0]`, `[1, 0, 1]`, `[0, 1, 1]`.\n\n- **Query examples**:\n - Match only structures with exactly 3 periodic dimensions: `nperiodic_dimensions=3`\n - Match all structures with 2 or fewer periodic dimensions: `nperiodic_dimensions<=2`",
- "x-optimade-support": "should",
"x-optimade-queryable": "must",
- "nullable": true
+ "x-optimade-support": "should"
},
"lattice_vectors": {
- "items": {
- "items": {
- "type": "number"
+ "anyOf": [
+ {
+ "items": {
+ "items": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "type": "array",
+ "maxItems": 3,
+ "minItems": 3
+ },
+ "type": "array",
+ "maxItems": 3,
+ "minItems": 3
},
- "type": "array",
- "maxItems": 3,
- "minItems": 3
- },
- "type": "array",
- "maxItems": 3,
- "minItems": 3,
+ {
+ "type": "null"
+ }
+ ],
"title": "Lattice Vectors",
"description": "The three lattice vectors in Cartesian coordinates, in \u00e5ngstr\u00f6m (\u00c5).\n\n- **Type**: list of list of floats or unknown values.\n\n- **Requirements/Conventions**:\n - **Support**: SHOULD be supported by all implementations, i.e., SHOULD NOT be `null`.\n - **Query**: Support for queries on this property is OPTIONAL.\n If supported, filters MAY support only a subset of comparison operators.\n - MUST be a list of three vectors *a*, *b*, and *c*, where each of the vectors MUST BE a list of the vector's coordinates along the x, y, and z Cartesian coordinates.\n (Therefore, the first index runs over the three lattice vectors and the second index runs over the x, y, z Cartesian coordinates).\n - For databases that do not define an absolute Cartesian system (e.g., only defining the length and angles between vectors), the first lattice vector SHOULD be set along *x* and the second on the *xy*-plane.\n - MUST always contain three vectors of three coordinates each, independently of the elements of property `dimension_types`.\n The vectors SHOULD by convention be chosen so the determinant of the `lattice_vectors` matrix is different from zero.\n The vectors in the non-periodic directions have no significance beyond fulfilling these requirements.\n - The coordinates of the lattice vectors of non-periodic dimensions (i.e., those dimensions for which `dimension_types` is `0`) MAY be given as a list of all `null` values.\n If a lattice vector contains the value `null`, all coordinates of that lattice vector MUST be `null`.\n\n- **Examples**:\n - `[[4.0,0.0,0.0],[0.0,4.0,0.0],[0.0,1.0,4.0]]` represents a cell, where the first vector is `(4, 0, 0)`, i.e., a vector aligned along the `x` axis of length 4 \u00c5; the second vector is `(0, 4, 0)`; and the third vector is `(0, 1, 4)`.",
- "x-optimade-support": "should",
"x-optimade-queryable": "optional",
- "nullable": true,
+ "x-optimade-support": "should",
"x-optimade-unit": "\u00c5"
},
"cartesian_site_positions": {
- "items": {
- "items": {
- "type": "number"
+ "anyOf": [
+ {
+ "items": {
+ "items": {
+ "type": "number"
+ },
+ "type": "array",
+ "maxItems": 3,
+ "minItems": 3
+ },
+ "type": "array"
},
- "type": "array",
- "maxItems": 3,
- "minItems": 3
- },
- "type": "array",
+ {
+ "type": "null"
+ }
+ ],
"title": "Cartesian Site Positions",
"description": "Cartesian positions of each site in the structure.\nA site is usually used to describe positions of atoms; what atoms can be encountered at a given site is conveyed by the `species_at_sites` property, and the species themselves are described in the `species` property.\n\n- **Type**: list of list of floats\n\n- **Requirements/Conventions**:\n - **Support**: SHOULD be supported by all implementations, i.e., SHOULD NOT be `null`.\n - **Query**: Support for queries on this property is OPTIONAL.\n If supported, filters MAY support only a subset of comparison operators.\n - It MUST be a list of length equal to the number of sites in the structure, where every element is a list of the three Cartesian coordinates of a site expressed as float values in the unit angstrom (\u00c5).\n - An entry MAY have multiple sites at the same Cartesian position (for a relevant use of this, see e.g., the property `assemblies`).\n\n- **Examples**:\n - `[[0,0,0],[0,0,2]]` indicates a structure with two sites, one sitting at the origin and one along the (positive) *z*-axis, 2 \u00c5 away from the origin.",
- "x-optimade-support": "should",
"x-optimade-queryable": "optional",
- "nullable": true,
+ "x-optimade-support": "should",
"x-optimade-unit": "\u00c5"
},
+ "space_group_symmetry_operations_xyz": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string",
+ "pattern": "^([-+]?[xyz]([-+][xyz])?([-+](1/2|[12]/3|[1-3]/4|[1-5]/6))?|[-+]?(1/2|[12]/3|[1-3]/4|[1-5]/6)([-+][xyz]([-+][xyz])?)?),([-+]?[xyz]([-+][xyz])?([-+](1/2|[12]/3|[1-3]/4|[1-5]/6))?|[-+]?(1/2|[12]/3|[1-3]/4|[1-5]/6)([-+][xyz]([-+][xyz])?)?),([-+]?[xyz]([-+][xyz])?([-+](1/2|[12]/3|[1-3]/4|[1-5]/6))?|[-+]?(1/2|[12]/3|[1-3]/4|[1-5]/6)([-+][xyz]([-+][xyz])?)?)$"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Space Group Symmetry Operations Xyz",
+ "description": "A list of symmetry operations given as general position x, y and z coordinates in algebraic form.\n\nEach symmetry operation is described by a string that gives that symmetry operation in Jones' faithful representation (Bradley & Cracknell, 1972: pp. 35-37), adapted for computer string notation.\nThe letters x, y and z that are typesetted with overbars in printed text represent coordinate values multiplied by -1 and are encoded as -x, -y and -z, respectively.\nThe syntax of the strings representing symmetry operations MUST conform to regular expressions given in appendix The Symmetry Operation String Regular Expressions.\nThe interpretation of the strings MUST follow the conventions of the IUCr CIF core dictionary (IUCr, 2023).\nIn particular, this property MUST explicitly provide all symmetry operations needed to generate all the atoms in the unit cell from the atoms in the asymmetric unit, for the setting used.\nThis symmetry operation set MUST always include the `\"x,y,z\"` identity operation.\nThe symmetry operations are to be applied to fractional atom coordinates.\nIn case only Cartesian coordinates are available, these Cartesian coordinates must be converted to fractional coordinates before the application of the provided symmetry operations.\nIf the symmetry operation list is present, it MUST be compatible with other space group specifications (e.g. the ITC space group number, the Hall symbol, the Hermann-Mauguin symbol) if these are present.\n\n- **Type**: list of strings\n\n- **Requirements/Conventions**:\n - **Support**: OPTIONAL support in implementations, i.e., MAY be `null`.\n\n - The property is RECOMMENDED if coordinates are returned in a form to which these operations can or must be applied (e.g. fractional atom coordinates of an asymmetric unit).\n - The property is REQUIRED if symmetry operations are necessary to reconstruct the full model of the material and no other symmetry information (e.g., the Hall symbol) is provided that would allow the user to derive symmetry operations unambiguously.\n - MUST be null if `nperiodic_dimensions` is equal to 0.\n\n- **Examples**:\n\n - Space group operations for the space group with ITC number 3 (H-M symbol `P 2`, extended H-M symbol `P 1 2 1`, Hall symbol `P 2y`): `[\"x,y,z\", \"-x,y,-z\"]`\n - Space group operations for the space group with ITC number 5 (H-M symbol `C 2`, extended H-M symbol `C 1 2 1`, Hall symbol `C 2y`): `[\"x,y,z\", \"-x,y,-z\", \"x+1/2,y+1/2,z\", \"-x+1/2,y+1/2,-z\"]`\n\n- **Notes**:\n The list of space group symmetry operations applies to the whole periodic array of atoms and together with the lattice translations given in the `lattice_vectors` property provides the necessary information to reconstruct all atom site positions of the periodic material.\n Thus, the symmetry operations described in this property are only applicable to material models with at least one periodic dimension.\n This property is not meant to represent arbitrary symmetries of molecules, non-periodic (finite) collections of atoms or non-crystallographic symmetry.\n\n- **Bibliographic References**:\n\n Bradley, C. J. and Cracknell, A. P. (1972) The Mathematical Theory of Symmetry in Solids. Oxford, Clarendon Press (paperback edition 2010) 745 p. ISBN 978-0-19-958258-7.\n\n IUCr (2023) Core dictionary (coreCIF) version 2.4.5; data name _space_group_symop_operation_xyz. Available from: https://www.iucr.org/__data/iucr/cifdic_html/1/cif_core.dic/Ispace_group_symop_operation_xyz.html [Accessed 2023-06-18T16:46+03:00].",
+ "x-optimade-queryable": "optional",
+ "x-optimade-support": "optional"
+ },
+ "space_group_symbol_hall": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Space Group Symbol Hall",
+ "description": "A Hall space group symbol representing the symmetry of the structure as defined in (Hall, 1981, 1981a).\n\n- **Type**: string\n\n- **Requirements/Conventions**:\n - **Support**: OPTIONAL support in implementations, i.e., MAY be `null`.\n - **Query**: Support for queries on this property is OPTIONAL.\n - The change-of-basis operations are used as defined in the International Tables of Crystallography (ITC) Vol. B, Sect. 1.4, Appendix A1.4.2 (IUCr, 2001).\n - Each component of the Hall symbol MUST be separated by a single space symbol.\n - If there exists a standard Hall symbol which represents the symmetry it SHOULD be used.\n - MUST be null if `nperiodic_dimensions` is not equal to 3.\n\n- **Examples**:\n\n - Space group symbols with explicit origin (the Hall symbols):\n\n - `P 2c -2ac`\n - `-I 4bd 2ab 3`\n\n - Space group symbols with change-of-basis operations:\n\n - `P 2yb (-1/2*x+z,1/2*x,y)`\n - `-I 4 2 (1/2*x+1/2*y,-1/2*x+1/2*y,z)`\n\n- **Bibliographic References**:\n\n Hall, S. R. (1981) Space-group notation with an explicit origin. Acta Crystallographica Section A, 37, 517-525, International Union of Crystallography (IUCr), DOI: https://doi.org/10.1107/s0567739481001228\n\n Hall, S. R. (1981a) Space-group notation with an explicit origin; erratum. Acta Crystallographica Section A, 37, 921-921, International Union of Crystallography (IUCr), DOI: https://doi.org/10.1107/s0567739481001976\n\n IUCr (2001). International Tables for Crystallography vol. B. Reciprocal Space. Ed. U. Shmueli. 2-nd edition. Dordrecht/Boston/London, Kluwer Academic Publishers.",
+ "x-optimade-queryable": "optional",
+ "x-optimade-support": "optional"
+ },
+ "space_group_symbol_hermann_mauguin": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Space Group Symbol Hermann Mauguin",
+ "description": "A human- and machine-readable string containing the short Hermann-Mauguin (H-M) symbol which specifies the space group of the structure in the response.\n- **Type**: string\n\n- **Requirements/Conventions**:\n - **Support**: OPTIONAL support in implementations, i.e., MAY be `null`.\n - **Query**: Support for queries on this property is OPTIONAL.\n - The H-M symbol SHOULD aim to convey the closest representation of the symmetry information that can be specified using the short format used in the International Tables for Crystallography vol. A (IUCr, 2005), Table 4.3.2.1 as described in the accompanying text.\n - The symbol MAY be a non-standard short H-M symbol.\n - The H-M symbol does not unambiguously communicate the axis, cell, and origin choice, and the given symbol SHOULD NOT be amended to convey this information.\n - To encode as character strings, the following adaptations MUST be made when representing H-M symbols given in their typesetted form:\n\n - the overbar above the numbers MUST be changed to the minus sign in front of the digit (e.g. '-2');\n - subscripts that denote screw axes are written as digits immediately after the axis designator without a space (e.g. 'P 32')\n - the space group generators MUST be separated by a single space (e.g. 'P 21 21 2');\n - there MUST be no spaces in the space group generator designation (i.e. use 'P 21/m', not the 'P 21 / m');\n\n- **Examples**:\n - `C 2`\n - `P 21 21 21`\n\n- **Bibliographic References**:\n\n IUCr (2005). International Tables for Crystallography vol. A. Space-Group Symmetry. Ed. Theo Hahn. 5-th edition. Dordrecht, Springer.",
+ "x-optimade-queryable": "optional",
+ "x-optimade-support": "optional"
+ },
+ "space_group_symbol_hermann_mauguin_extended": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Space Group Symbol Hermann Mauguin Extended",
+ "description": "A human- and machine-readable string containing the extended Hermann-Mauguin (H-M) symbol which specifies the space group of the structure in the response.\n\n- **Type**: string\n- **Requirements/Conventions**:\n\n - **Support**: OPTIONAL support in implementations, i.e., MAY be `null`.\n - **Query**: Support for queries on this property is OPTIONAL.\n - The H-M symbols SHOULD be given as specified in the International Tables for Crystallography vol. A (IUCr, 2005), Table 4.3.2.1.\n - The change-of-basis operation SHOULD be provided for the non-standard axis and cell choices.\n - The extended H-M symbol does not unambiguously communicate the origin choice, and the given symbol SHOULD NOT be amended to convey this information.\n - The description of the change-of-basis SHOULD follow conventions of the ITC Vol. B, Sect. 1.4, Appendix A1.4.2 (IUCr, 2001).\n - The same character string encoding conventions MUST be used as for the specification of the `space_group_symbol_hermann_mauguin` property.\n\n- **Examples**:\n\n - `C 1 2 1`\n\n- **Bibliographic References**:\n\n IUCr (2001). International Tables for Crystallography vol. B. Reciprocal Space. Ed. U. Shmueli. 2-nd edition. Dordrecht/Boston/London, Kluwer Academic Publishers.",
+ "x-optimade-queryable": "optional",
+ "x-optimade-support": "optional"
+ },
+ "space_group_it_number": {
+ "anyOf": [
+ {
+ "type": "integer",
+ "maximum": 230.0,
+ "minimum": 1.0
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Space Group It Number",
+ "description": "Space group number for the structure assigned by the International Tables for Crystallography Vol. A.\n- **Type**: integer\n\n- **Requirements/Conventions**:\n - **Support**: OPTIONAL support in implementations, i.e., MAY be `null`.\n - **Query**: Support for queries on this property is OPTIONAL.\n - The integer value MUST be between 1 and 230.\n - MUST be null if `nperiodic_dimensions` is not equal to 3.",
+ "x-optimade-queryable": "optional",
+ "x-optimade-support": "optional"
+ },
"nsites": {
- "type": "integer",
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Nsites",
"description": "An integer specifying the length of the `cartesian_site_positions` property.\n\n- **Type**: integer\n\n- **Requirements/Conventions**:\n - **Support**: SHOULD be supported by all implementations, i.e., SHOULD NOT be `null`.\n - **Query**: MUST be a queryable property with support for all mandatory filter features.\n\n- **Examples**:\n - `42`\n\n- **Query examples**:\n - Match only structures with exactly 4 sites: `nsites=4`\n - Match structures that have between 2 and 7 sites: `nsites>=2 AND nsites<=7`",
- "x-optimade-support": "should",
"x-optimade-queryable": "must",
- "nullable": true
+ "x-optimade-support": "should"
},
"species": {
- "items": {
- "$ref": "#/components/schemas/Species"
- },
- "type": "array",
+ "anyOf": [
+ {
+ "items": {
+ "$ref": "#/components/schemas/Species"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Species",
"description": "A list describing the species of the sites of this structure.\nSpecies can represent pure chemical elements, virtual-crystal atoms representing a statistical occupation of a given site by multiple chemical elements, and/or a location to which there are attached atoms, i.e., atoms whose precise location are unknown beyond that they are attached to that position (frequently used to indicate hydrogen atoms attached to another element, e.g., a carbon with three attached hydrogens might represent a methyl group, -CH3).\n\n- **Type**: list of dictionary with keys:\n - `name`: string (REQUIRED)\n - `chemical_symbols`: list of strings (REQUIRED)\n - `concentration`: list of float (REQUIRED)\n - `attached`: list of strings (REQUIRED)\n - `nattached`: list of integers (OPTIONAL)\n - `mass`: list of floats (OPTIONAL)\n - `original_name`: string (OPTIONAL).\n\n- **Requirements/Conventions**:\n - **Support**: SHOULD be supported by all implementations, i.e., SHOULD NOT be `null`.\n - **Query**: Support for queries on this property is OPTIONAL.\n If supported, filters MAY support only a subset of comparison operators.\n - Each list member MUST be a dictionary with the following keys:\n - **name**: REQUIRED; gives the name of the species; the **name** value MUST be unique in the `species` list;\n - **chemical_symbols**: REQUIRED; MUST be a list of strings of all chemical elements composing this species.\n Each item of the list MUST be one of the following:\n - a valid chemical-element symbol, or\n - the special value `\"X\"` to represent a non-chemical element, or\n - the special value `\"vacancy\"` to represent that this site has a non-zero probability of having a vacancy (the respective probability is indicated in the `concentration` list, see below).\n\n If any one entry in the `species` list has a `chemical_symbols` list that is longer than 1 element, the correct flag MUST be set in the list `structure_features`.\n\n - **concentration**: REQUIRED; MUST be a list of floats, with same length as `chemical_symbols`.\n The numbers represent the relative concentration of the corresponding chemical symbol in this species.\n The numbers SHOULD sum to one. Cases in which the numbers do not sum to one typically fall only in the following two categories:\n\n - Numerical errors when representing float numbers in fixed precision, e.g. for two chemical symbols with concentrations `1/3` and `2/3`, the concentration might look something like `[0.33333333333, 0.66666666666]`. If the client is aware that the sum is not one because of numerical precision, it can renormalize the values so that the sum is exactly one.\n - Experimental errors in the data present in the database. In this case, it is the responsibility of the client to decide how to process the data.\n\n Note that concentrations are uncorrelated between different sites (even of the same species).\n\n - **attached**: OPTIONAL; if provided MUST be a list of length 1 or more of strings of chemical symbols for the elements attached to this site, or \"X\" for a non-chemical element.\n\n - **nattached**: OPTIONAL; if provided MUST be a list of length 1 or more of integers indicating the number of attached atoms of the kind specified in the value of the `attached` key.\n\n The implementation MUST include either both or none of the `attached` and `nattached` keys, and if they are provided, they MUST be of the same length.\n Furthermore, if they are provided, the `structure_features` property MUST include the string `site_attachments`.\n\n - **mass**: OPTIONAL. If present MUST be a list of floats, with the same length as `chemical_symbols`, providing element masses expressed in a.m.u.\n Elements denoting vacancies MUST have masses equal to 0.\n\n - **original_name**: OPTIONAL. Can be any valid Unicode string, and SHOULD contain (if specified) the name of the species that is used internally in the source database.\n\n Note: With regards to \"source database\", we refer to the immediate source being queried via the OPTIMADE API implementation.\n\n The main use of this field is for source databases that use species names, containing characters that are not allowed (see description of the list property `species_at_sites`).\n\n - For systems that have only species formed by a single chemical symbol, and that have at most one species per chemical symbol, SHOULD use the chemical symbol as species name (e.g., `\"Ti\"` for titanium, `\"O\"` for oxygen, etc.)\n However, note that this is OPTIONAL, and client implementations MUST NOT assume that the key corresponds to a chemical symbol, nor assume that if the species name is a valid chemical symbol, that it represents a species with that chemical symbol.\n This means that a species `{\"name\": \"C\", \"chemical_symbols\": [\"Ti\"], \"concentration\": [1.0]}` is valid and represents a titanium species (and *not* a carbon species).\n - It is NOT RECOMMENDED that a structure includes species that do not have at least one corresponding site.\n\n- **Examples**:\n - `[ {\"name\": \"Ti\", \"chemical_symbols\": [\"Ti\"], \"concentration\": [1.0]} ]`: any site with this species is occupied by a Ti atom.\n - `[ {\"name\": \"Ti\", \"chemical_symbols\": [\"Ti\", \"vacancy\"], \"concentration\": [0.9, 0.1]} ]`: any site with this species is occupied by a Ti atom with 90 % probability, and has a vacancy with 10 % probability.\n - `[ {\"name\": \"BaCa\", \"chemical_symbols\": [\"vacancy\", \"Ba\", \"Ca\"], \"concentration\": [0.05, 0.45, 0.5], \"mass\": [0.0, 137.327, 40.078]} ]`: any site with this species is occupied by a Ba atom with 45 % probability, a Ca atom with 50 % probability, and by a vacancy with 5 % probability. The mass of this site is (on average) 88.5 a.m.u.\n - `[ {\"name\": \"C12\", \"chemical_symbols\": [\"C\"], \"concentration\": [1.0], \"mass\": [12.0]} ]`: any site with this species is occupied by a carbon isotope with mass 12.\n - `[ {\"name\": \"C13\", \"chemical_symbols\": [\"C\"], \"concentration\": [1.0], \"mass\": [13.0]} ]`: any site with this species is occupied by a carbon isotope with mass 13.\n - `[ {\"name\": \"CH3\", \"chemical_symbols\": [\"C\"], \"concentration\": [1.0], \"attached\": [\"H\"], \"nattached\": [3]} ]`: any site with this species is occupied by a methyl group, -CH3, which is represented without specifying precise positions of the hydrogen atoms.",
- "x-optimade-support": "should",
"x-optimade-queryable": "optional",
- "nullable": true
+ "x-optimade-support": "should"
},
"species_at_sites": {
- "items": {
- "type": "string"
- },
- "type": "array",
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Species At Sites",
"description": "Name of the species at each site (where values for sites are specified with the same order of the property `cartesian_site_positions`).\nThe properties of the species are found in the property `species`.\n\n- **Type**: list of strings.\n\n- **Requirements/Conventions**:\n - **Support**: SHOULD be supported by all implementations, i.e., SHOULD NOT be `null`.\n - **Query**: Support for queries on this property is OPTIONAL.\n If supported, filters MAY support only a subset of comparison operators.\n - MUST have length equal to the number of sites in the structure (first dimension of the list property `cartesian_site_positions`).\n - Each species name mentioned in the `species_at_sites` list MUST be described in the list property `species` (i.e. for each value in the `species_at_sites` list there MUST exist exactly one dictionary in the `species` list with the `name` attribute equal to the corresponding `species_at_sites` value).\n - Each site MUST be associated only to a single species.\n **Note**: However, species can represent mixtures of atoms, and multiple species MAY be defined for the same chemical element.\n This latter case is useful when different atoms of the same type need to be grouped or distinguished, for instance in simulation codes to assign different initial spin states.\n\n- **Examples**:\n - `[\"Ti\",\"O2\"]` indicates that the first site is hosting a species labeled `\"Ti\"` and the second a species labeled `\"O2\"`.\n - `[\"Ac\", \"Ac\", \"Ag\", \"Ir\"]` indicating the first two sites contains the `\"Ac\"` species, while the third and fourth sites contain the `\"Ag\"` and `\"Ir\"` species, respectively.",
- "x-optimade-support": "should",
"x-optimade-queryable": "optional",
- "nullable": true
+ "x-optimade-support": "should"
},
"assemblies": {
- "items": {
- "$ref": "#/components/schemas/Assembly"
- },
- "type": "array",
+ "anyOf": [
+ {
+ "items": {
+ "$ref": "#/components/schemas/Assembly"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Assemblies",
"description": "A description of groups of sites that are statistically correlated.\n\n- **Type**: list of dictionary with keys:\n - `sites_in_groups`: list of list of integers (REQUIRED)\n - `group_probabilities`: list of floats (REQUIRED)\n\n- **Requirements/Conventions**:\n - **Support**: OPTIONAL support in implementations, i.e., MAY be `null`.\n - **Query**: Support for queries on this property is OPTIONAL.\n If supported, filters MAY support only a subset of comparison operators.\n - The property SHOULD be `null` for entries that have no partial occupancies.\n - If present, the correct flag MUST be set in the list `structure_features`.\n - Client implementations MUST check its presence (as its presence changes the interpretation of the structure).\n - If present, it MUST be a list of dictionaries, each of which represents an assembly and MUST have the following two keys:\n - **sites_in_groups**: Index of the sites (0-based) that belong to each group for each assembly.\n\n Example: `[[1], [2]]`: two groups, one with the second site, one with the third.\n Example: `[[1,2], [3]]`: one group with the second and third site, one with the fourth.\n\n - **group_probabilities**: Statistical probability of each group. It MUST have the same length as `sites_in_groups`.\n It SHOULD sum to one.\n See below for examples of how to specify the probability of the occurrence of a vacancy.\n The possible reasons for the values not to sum to one are the same as already specified above for the `concentration` of each `species`.\n\n - If a site is not present in any group, it means that it is present with 100 % probability (as if no assembly was specified).\n - A site MUST NOT appear in more than one group.\n\n- **Examples** (for each entry of the assemblies list):\n - `{\"sites_in_groups\": [[0], [1]], \"group_probabilities: [0.3, 0.7]}`: the first site and the second site never occur at the same time in the unit cell.\n Statistically, 30 % of the times the first site is present, while 70 % of the times the second site is present.\n - `{\"sites_in_groups\": [[1,2], [3]], \"group_probabilities: [0.3, 0.7]}`: the second and third site are either present together or not present; they form the first group of atoms for this assembly.\n The second group is formed by the fourth site.\n Sites of the first group (the second and the third) are never present at the same time as the fourth site.\n 30 % of times sites 1 and 2 are present (and site 3 is absent); 70 % of times site 3 is present (and sites 1 and 2 are absent).\n\n- **Notes**:\n - Assemblies are essential to represent, for instance, the situation where an atom can statistically occupy two different positions (sites).\n\n - By defining groups, it is possible to represent, e.g., the case where a functional molecule (and not just one atom) is either present or absent (or the case where it it is present in two conformations)\n\n - Considerations on virtual alloys and on vacancies: In the special case of a virtual alloy, these specifications allow two different, equivalent ways of specifying them.\n For instance, for a site at the origin with 30 % probability of being occupied by Si, 50 % probability of being occupied by Ge, and 20 % of being a vacancy, the following two representations are possible:\n\n - Using a single species:\n ```json\n {\n \"cartesian_site_positions\": [[0,0,0]],\n \"species_at_sites\": [\"SiGe-vac\"],\n \"species\": [\n {\n \"name\": \"SiGe-vac\",\n \"chemical_symbols\": [\"Si\", \"Ge\", \"vacancy\"],\n \"concentration\": [0.3, 0.5, 0.2]\n }\n ]\n // ...\n }\n ```\n\n - Using multiple species and the assemblies:\n ```json\n {\n \"cartesian_site_positions\": [ [0,0,0], [0,0,0], [0,0,0] ],\n \"species_at_sites\": [\"Si\", \"Ge\", \"vac\"],\n \"species\": [\n { \"name\": \"Si\", \"chemical_symbols\": [\"Si\"], \"concentration\": [1.0] },\n { \"name\": \"Ge\", \"chemical_symbols\": [\"Ge\"], \"concentration\": [1.0] },\n { \"name\": \"vac\", \"chemical_symbols\": [\"vacancy\"], \"concentration\": [1.0] }\n ],\n \"assemblies\": [\n {\n \"sites_in_groups\": [ [0], [1], [2] ],\n \"group_probabilities\": [0.3, 0.5, 0.2]\n }\n ]\n // ...\n }\n ```\n\n - It is up to the database provider to decide which representation to use, typically depending on the internal format in which the structure is stored.\n However, given a structure identified by a unique ID, the API implementation MUST always provide the same representation for it.\n\n - The probabilities of occurrence of different assemblies are uncorrelated.\n So, for instance in the following case with two assemblies:\n ```json\n {\n \"assemblies\": [\n {\n \"sites_in_groups\": [ [0], [1] ],\n \"group_probabilities\": [0.2, 0.8],\n },\n {\n \"sites_in_groups\": [ [2], [3] ],\n \"group_probabilities\": [0.3, 0.7]\n }\n ]\n }\n ```\n\n Site 0 is present with a probability of 20 % and site 1 with a probability of 80 %. These two sites are correlated (either site 0 or 1 is present). Similarly, site 2 is present with a probability of 30 % and site 3 with a probability of 70 %.\n These two sites are correlated (either site 2 or 3 is present).\n However, the presence or absence of sites 0 and 1 is not correlated with the presence or absence of sites 2 and 3 (in the specific example, the pair of sites (0, 2) can occur with 0.2*0.3 = 6 % probability; the pair (0, 3) with 0.2*0.7 = 14 % probability; the pair (1, 2) with 0.8*0.3 = 24 % probability; and the pair (1, 3) with 0.8*0.7 = 56 % probability).",
- "x-optimade-support": "optional",
- "x-optimade-queryable": "optional"
+ "x-optimade-queryable": "optional",
+ "x-optimade-support": "optional"
},
"structure_features": {
"items": {
@@ -3676,26 +4494,14 @@
"type": "array",
"title": "Structure Features",
"description": "A list of strings that flag which special features are used by the structure.\n\n- **Type**: list of strings\n\n- **Requirements/Conventions**:\n - **Support**: MUST be supported by all implementations, MUST NOT be `null`.\n - **Query**: MUST be a queryable property.\n Filters on the list MUST support all mandatory HAS-type queries.\n Filter operators for comparisons on the string components MUST support equality, support for other comparison operators are OPTIONAL.\n - MUST be an empty list if no special features are used.\n - MUST be sorted alphabetically.\n - If a special feature listed below is used, the list MUST contain the corresponding string.\n - If a special feature listed below is not used, the list MUST NOT contain the corresponding string.\n - **List of strings used to indicate special structure features**:\n - `disorder`: this flag MUST be present if any one entry in the `species` list has a `chemical_symbols` list that is longer than 1 element.\n - `implicit_atoms`: this flag MUST be present if the structure contains atoms that are not assigned to sites via the property `species_at_sites` (e.g., because their positions are unknown).\n When this flag is present, the properties related to the chemical formula will likely not match the type and count of atoms represented by the `species_at_sites`, `species` and `assemblies` properties.\n - `site_attachments`: this flag MUST be present if any one entry in the `species` list includes `attached` and `nattached`.\n - `assemblies`: this flag MUST be present if the property `assemblies` is present.\n\n- **Examples**: A structure having implicit atoms and using assemblies: `[\"assemblies\", \"implicit_atoms\"]`",
- "x-optimade-support": "must",
- "x-optimade-queryable": "must"
+ "x-optimade-queryable": "must",
+ "x-optimade-support": "must"
}
},
+ "additionalProperties": true,
"type": "object",
"required": [
"last_modified",
- "elements",
- "nelements",
- "elements_ratios",
- "chemical_formula_descriptive",
- "chemical_formula_reduced",
- "chemical_formula_anonymous",
- "dimension_types",
- "nperiodic_dimensions",
- "lattice_vectors",
- "cartesian_site_positions",
- "nsites",
- "species",
- "species_at_sites",
"structure_features"
],
"title": "StructureResourceAttributes",
@@ -3728,14 +4534,20 @@
"$ref": "#/components/schemas/ResponseMeta"
}
],
- "title": "Meta",
"description": "A meta object containing non-standard information"
},
"errors": {
- "items": {
- "$ref": "#/components/schemas/Error"
- },
- "type": "array",
+ "anyOf": [
+ {
+ "items": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
"uniqueItems": true,
"title": "Errors",
"description": "A list of unique errors"
@@ -3753,27 +4565,35 @@
"type": "object"
},
"type": "array"
+ },
+ {
+ "type": "null"
}
],
"uniqueItems": true,
- "title": "Included"
+ "title": "Included",
+ "description": "A list of unique included OPTIMADE entry resources."
},
"links": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/ToplevelLinks"
+ },
+ {
+ "type": "null"
}
],
- "title": "Links",
"description": "Links associated with the primary data or errors"
},
"jsonapi": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/JsonApi"
+ },
+ {
+ "type": "null"
}
],
- "title": "Jsonapi",
"description": "Information about the JSON API used"
}
},
@@ -3782,8 +4602,7 @@
"data",
"meta"
],
- "title": "StructureResponseMany",
- "description": "errors are not allowed"
+ "title": "StructureResponseMany"
},
"StructureResponseOne": {
"properties": {
@@ -3794,6 +4613,9 @@
},
{
"type": "object"
+ },
+ {
+ "type": "null"
}
],
"title": "Data",
@@ -3805,14 +4627,20 @@
"$ref": "#/components/schemas/ResponseMeta"
}
],
- "title": "Meta",
"description": "A meta object containing non-standard information"
},
"errors": {
- "items": {
- "$ref": "#/components/schemas/Error"
- },
- "type": "array",
+ "anyOf": [
+ {
+ "items": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
"uniqueItems": true,
"title": "Errors",
"description": "A list of unique errors"
@@ -3830,27 +4658,35 @@
"type": "object"
},
"type": "array"
+ },
+ {
+ "type": "null"
}
],
"uniqueItems": true,
- "title": "Included"
+ "title": "Included",
+ "description": "A list of unique included OPTIMADE entry resources."
},
"links": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/ToplevelLinks"
+ },
+ {
+ "type": "null"
}
],
- "title": "Links",
"description": "Links associated with the primary data or errors"
},
"jsonapi": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/JsonApi"
+ },
+ {
+ "type": "null"
}
],
- "title": "Jsonapi",
"description": "Information about the JSON API used"
}
},
@@ -3859,8 +4695,7 @@
"data",
"meta"
],
- "title": "StructureResponseOne",
- "description": "errors are not allowed"
+ "title": "StructureResponseOne"
},
"ToplevelLinks": {
"properties": {
@@ -3868,12 +4703,14 @@
"anyOf": [
{
"type": "string",
- "maxLength": 65536,
"minLength": 1,
"format": "uri"
},
{
"$ref": "#/components/schemas/Link"
+ },
+ {
+ "type": "null"
}
],
"title": "Self",
@@ -3883,12 +4720,14 @@
"anyOf": [
{
"type": "string",
- "maxLength": 65536,
"minLength": 1,
"format": "uri"
},
{
"$ref": "#/components/schemas/Link"
+ },
+ {
+ "type": "null"
}
],
"title": "Related",
@@ -3898,12 +4737,14 @@
"anyOf": [
{
"type": "string",
- "maxLength": 65536,
"minLength": 1,
"format": "uri"
},
{
"$ref": "#/components/schemas/Link"
+ },
+ {
+ "type": "null"
}
],
"title": "First",
@@ -3913,12 +4754,14 @@
"anyOf": [
{
"type": "string",
- "maxLength": 65536,
"minLength": 1,
"format": "uri"
},
{
"$ref": "#/components/schemas/Link"
+ },
+ {
+ "type": "null"
}
],
"title": "Last",
@@ -3928,12 +4771,14 @@
"anyOf": [
{
"type": "string",
- "maxLength": 65536,
"minLength": 1,
"format": "uri"
},
{
"$ref": "#/components/schemas/Link"
+ },
+ {
+ "type": "null"
}
],
"title": "Prev",
@@ -3943,18 +4788,21 @@
"anyOf": [
{
"type": "string",
- "maxLength": 65536,
"minLength": 1,
"format": "uri"
},
{
"$ref": "#/components/schemas/Link"
+ },
+ {
+ "type": "null"
}
],
"title": "Next",
"description": "The next page of data"
}
},
+ "additionalProperties": true,
"type": "object",
"title": "ToplevelLinks",
"description": "A set of Links objects, possibly including pagination"
@@ -3962,26 +4810,49 @@
"Warnings": {
"properties": {
"id": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Id",
"description": "A unique identifier for this particular occurrence of the problem."
},
"links": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/ErrorLinks"
+ },
+ {
+ "type": "null"
}
],
- "title": "Links",
"description": "A links object storing about"
},
"code": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Code",
"description": "an application-specific error code, expressed as a string value."
},
"title": {
- "type": "string",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Title",
"description": "A short, human-readable summary of the problem. It **SHOULD NOT** change from occurrence to occurrence of the problem, except for purposes of localization."
},
@@ -3991,25 +4862,33 @@
"description": "A human-readable explanation specific to this occurrence of the problem."
},
"source": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/ErrorSource"
+ },
+ {
+ "type": "null"
}
],
- "title": "Source",
"description": "An object containing references to the source of the error"
},
"meta": {
- "allOf": [
+ "anyOf": [
{
"$ref": "#/components/schemas/Meta"
+ },
+ {
+ "type": "null"
}
],
- "title": "Meta",
"description": "a meta object containing non-standard meta-information about the error."
},
"type": {
"type": "string",
+ "enum": [
+ "warning"
+ ],
+ "const": "warning",
"pattern": "^warning$",
"title": "Type",
"description": "Warnings must be of type \"warning\"",
diff --git a/optimade/__init__.py b/optimade/__init__.py
index f9e0f3d47..b0b335088 100644
--- a/optimade/__init__.py
+++ b/optimade/__init__.py
@@ -1,2 +1,2 @@
-__version__ = "0.25.3"
+__version__ = "1.0.5"
__api_version__ = "1.1.0"
diff --git a/optimade/adapters/__init__.py b/optimade/adapters/__init__.py
index 6de640943..f3b35d6ee 100644
--- a/optimade/adapters/__init__.py
+++ b/optimade/adapters/__init__.py
@@ -1,4 +1,3 @@
-# pylint: disable=undefined-variable
from .exceptions import * # noqa: F403
from .references import * # noqa: F403
from .structures import * # noqa: F403
diff --git a/optimade/adapters/base.py b/optimade/adapters/base.py
index f1d4bcb63..71d274203 100644
--- a/optimade/adapters/base.py
+++ b/optimade/adapters/base.py
@@ -18,12 +18,12 @@
resources can be converted to for [`ReferenceResource`][optimade.models.references.ReferenceResource]s
and [`StructureResource`][optimade.models.structures.StructureResource]s, respectively.
"""
+
import re
-from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union
+from typing import Any, Callable, Optional, Union
-from pydantic import BaseModel # pylint: disable=no-name-in-module
+from pydantic import BaseModel
-from optimade.adapters.logger import LOGGER
from optimade.models.entries import EntryResource
@@ -42,27 +42,26 @@ class EntryAdapter:
"""
- ENTRY_RESOURCE: Type[EntryResource] = EntryResource
- _type_converters: Dict[str, Callable] = {}
- _type_ingesters: Dict[str, Callable] = {}
- _type_ingesters_by_type: Dict[str, Type] = {}
+ ENTRY_RESOURCE: type[EntryResource] = EntryResource
+ _type_converters: dict[str, Callable] = {}
+ _type_ingesters: dict[str, Callable] = {}
+ _type_ingesters_by_type: dict[str, type] = {}
- def __init__(self, entry: dict) -> None:
+ def __init__(self, entry: dict[str, Any]) -> None:
"""
Parameters:
entry (dict): A JSON OPTIMADE single resource entry.
"""
- self._entry: Optional[EntryResource] = None
- self._converted: Dict[str, Any] = {}
+ self._converted: dict[str, Any] = {}
- self.entry: EntryResource = entry # type: ignore[assignment]
+ self._entry = self.ENTRY_RESOURCE(**entry)
# Note that these return also the default values for otherwise non-provided properties.
self._common_converters = {
# Return JSON serialized string, see https://pydantic-docs.helpmanual.io/usage/exporting_models/#modeljson
- "json": self.entry.json, # type: ignore[attr-defined]
+ "json": self.entry.model_dump_json,
# Return Python dict, see https://pydantic-docs.helpmanual.io/usage/exporting_models/#modeldict
- "dict": self.entry.dict, # type: ignore[attr-defined]
+ "dict": self.entry.model_dump,
}
@property
@@ -73,22 +72,7 @@ def entry(self) -> EntryResource:
The entry resource.
"""
- return self._entry # type: ignore[return-value]
-
- @entry.setter
- def entry(self, value: dict) -> None:
- """Set OPTIMADE entry.
-
- If already set, print that this can _only_ be set once.
-
- Parameters:
- value (dict): Raw entry to wrap in the relevant pydantic model represented by `ENTRY_RESOURCE`.
-
- """
- if self._entry is None:
- self._entry = self.ENTRY_RESOURCE(**value)
- else:
- LOGGER.warning("entry can only be set once and is already set.")
+ return self._entry
def convert(self, format: str) -> Any:
"""Convert OPTIMADE entry to desired format.
@@ -108,8 +92,8 @@ def convert(self, format: str) -> Any:
and format not in self._common_converters
):
raise AttributeError(
- f"Non-valid entry type to convert to: {format}\n"
- f"Valid entry types: {tuple(self._type_converters.keys()) + tuple(self._common_converters.keys())}"
+ f"Non-valid entry type to convert to: {format}\nValid entry types: "
+ f"{tuple(self._type_converters.keys()) + tuple(self._common_converters.keys())}"
)
if self._converted.get(format, None) is None:
@@ -156,7 +140,7 @@ def ingest_from(cls, data: Any, format: Optional[str] = None) -> Any:
return cls(
{
- "attributes": cls._type_ingesters[format](data).dict(),
+ "attributes": cls._type_ingesters[format](data).model_dump(),
"id": "",
"type": "structures",
}
@@ -164,13 +148,13 @@ def ingest_from(cls, data: Any, format: Optional[str] = None) -> Any:
@staticmethod
def _get_model_attributes(
- starting_instances: Union[Tuple[BaseModel, ...], List[BaseModel]], name: str
+ starting_instances: Union[tuple[BaseModel, ...], list[BaseModel]], name: str
) -> Any:
"""Helper method for retrieving the OPTIMADE model's attribute, supporting "."-nested attributes"""
for res in starting_instances:
nested_attributes = name.split(".")
for nested_attribute in nested_attributes:
- if nested_attribute in getattr(res, "__fields__", {}):
+ if nested_attribute in getattr(res, "model_fields", {}):
res = getattr(res, nested_attribute)
else:
res = None
diff --git a/optimade/adapters/logger.py b/optimade/adapters/logger.py
index 8859d5ed9..4b02c1721 100644
--- a/optimade/adapters/logger.py
+++ b/optimade/adapters/logger.py
@@ -1,4 +1,5 @@
"""Logger for optimade.adapters"""
+
import logging
LOGGER = logging.getLogger("optimade").getChild("adapters")
diff --git a/optimade/adapters/references/adapter.py b/optimade/adapters/references/adapter.py
index ef03e5396..cd3ebbae6 100644
--- a/optimade/adapters/references/adapter.py
+++ b/optimade/adapters/references/adapter.py
@@ -1,5 +1,3 @@
-from typing import Type
-
from optimade.adapters.base import EntryAdapter
from optimade.models import ReferenceResource
@@ -21,4 +19,4 @@ class Reference(EntryAdapter):
"""
- ENTRY_RESOURCE: Type[ReferenceResource] = ReferenceResource
+ ENTRY_RESOURCE: type[ReferenceResource] = ReferenceResource
diff --git a/optimade/adapters/structures/adapter.py b/optimade/adapters/structures/adapter.py
index 4ae622497..f7641c437 100644
--- a/optimade/adapters/structures/adapter.py
+++ b/optimade/adapters/structures/adapter.py
@@ -1,4 +1,4 @@
-from typing import Callable, Dict, Type
+from typing import Callable
from optimade.adapters.base import EntryAdapter
from optimade.models import StructureResource
@@ -44,8 +44,8 @@ class Structure(EntryAdapter):
"""
- ENTRY_RESOURCE: Type[StructureResource] = StructureResource
- _type_converters: Dict[str, Callable] = {
+ ENTRY_RESOURCE: type[StructureResource] = StructureResource
+ _type_converters: dict[str, Callable] = {
"aiida_structuredata": get_aiida_structure_data,
"ase": get_ase_atoms,
"cif": get_cif,
@@ -55,12 +55,12 @@ class Structure(EntryAdapter):
"jarvis": get_jarvis_atoms,
}
- _type_ingesters: Dict[str, Callable] = {
+ _type_ingesters: dict[str, Callable] = {
"pymatgen": from_pymatgen,
"ase": from_ase_atoms,
}
- _type_ingesters_by_type: Dict[str, Type] = {
+ _type_ingesters_by_type: dict[str, type] = {
"pymatgen": PymatgenStructure,
"ase": ASEAtoms,
}
diff --git a/optimade/adapters/structures/aiida.py b/optimade/adapters/structures/aiida.py
index f65c0babb..b26b2f23b 100644
--- a/optimade/adapters/structures/aiida.py
+++ b/optimade/adapters/structures/aiida.py
@@ -7,7 +7,8 @@
This conversion function relies on the [`aiida-core`](https://github.com/aiidateam/aiida-core) package.
"""
-from typing import List, Optional
+
+from typing import Optional
from warnings import warn
from optimade.adapters.structures.utils import pad_cell, species_from_species_at_sites
@@ -48,7 +49,7 @@ def get_aiida_structure_data(optimade_structure: OptimadeStructure) -> Structure
structure = StructureData(cell=lattice_vectors)
# If species not provided, infer data from species_at_sites
- species: Optional[List[OptimadeStructureSpecies]] = attributes.species
+ species: Optional[list[OptimadeStructureSpecies]] = attributes.species
if not species:
species = species_from_species_at_sites(attributes.species_at_sites) # type: ignore[arg-type]
diff --git a/optimade/adapters/structures/ase.py b/optimade/adapters/structures/ase.py
index e049663d6..d371ac492 100644
--- a/optimade/adapters/structures/ase.py
+++ b/optimade/adapters/structures/ase.py
@@ -7,7 +7,6 @@
For more information on the ASE code see [their documentation](https://wiki.fysik.dtu.dk/ase/).
"""
-from typing import Dict
from optimade.adapters.exceptions import ConversionError
from optimade.adapters.structures.utils import (
@@ -66,7 +65,7 @@ def get_ase_atoms(optimade_structure: OptimadeStructure) -> Atoms:
if not species:
species = species_from_species_at_sites(attributes.species_at_sites) # type: ignore[arg-type]
- optimade_species: Dict[str, OptimadeStructureSpecies] = {_.name: _ for _ in species}
+ optimade_species: dict[str, OptimadeStructureSpecies] = {_.name: _ for _ in species}
# Since we've made sure there are no species with more than 1 chemical symbol,
# asking for index 0 will always work.
@@ -92,7 +91,7 @@ def get_ase_atoms(optimade_structure: OptimadeStructure) -> Atoms:
)
info = {}
- for key in attributes.dict().keys():
+ for key in attributes.model_dump().keys():
if key.startswith("_"):
ase_key = key
if key.startswith(f"_{EXTRA_FIELD_PREFIX}_"):
@@ -115,7 +114,7 @@ def from_ase_atoms(atoms: Atoms) -> StructureResourceAttributes:
Returns:
An OPTIMADE `StructureResourceAttributes` model, which can be converted to a raw Python
- dictionary with `.dict()` or to JSON with `.json()`.
+ dictionary with `.model_dump()` or to JSON with `.model_dump_json()`.
"""
if not isinstance(atoms, Atoms):
diff --git a/optimade/adapters/structures/cif.py b/optimade/adapters/structures/cif.py
index cbe35901c..497109908 100644
--- a/optimade/adapters/structures/cif.py
+++ b/optimade/adapters/structures/cif.py
@@ -16,7 +16,6 @@
This conversion function relies on the [NumPy](https://numpy.org/) library.
"""
-from typing import Dict
from optimade.adapters.structures.utils import (
cell_to_cellpar,
@@ -40,7 +39,7 @@
__all__ = ("get_cif",)
-def get_cif( # pylint: disable=too-many-locals,too-many-branches
+def get_cif(
optimade_structure: OptimadeStructure,
) -> str:
"""Get CIF file as string from OPTIMADE structure.
@@ -123,11 +122,12 @@ def get_cif( # pylint: disable=too-many-locals,too-many-branches
else:
sites = attributes.cartesian_site_positions
- species: Dict[str, OptimadeStructureSpecies] = {
- species.name: species for species in attributes.species # type: ignore[union-attr]
+ species: dict[str, OptimadeStructureSpecies] = {
+ species.name: species
+ for species in attributes.species # type: ignore[union-attr]
}
- symbol_occurences: Dict[str, int] = {}
+ symbol_occurences: dict[str, int] = {}
for site_number in range(attributes.nsites): # type: ignore[arg-type]
species_name = attributes.species_at_sites[site_number] # type: ignore[index]
site = sites[site_number]
diff --git a/optimade/adapters/structures/jarvis.py b/optimade/adapters/structures/jarvis.py
index ac7efd34b..006466314 100644
--- a/optimade/adapters/structures/jarvis.py
+++ b/optimade/adapters/structures/jarvis.py
@@ -10,6 +10,7 @@
!!! success "Contributing author"
This conversion function was contributed by Kamal Choudhary ([@knc6](https://github.com/knc6)).
"""
+
from optimade.adapters.exceptions import ConversionError
from optimade.models import StructureFeatures
from optimade.models import StructureResource as OptimadeStructure
diff --git a/optimade/adapters/structures/proteindatabank.py b/optimade/adapters/structures/proteindatabank.py
index f2e699408..28254c730 100644
--- a/optimade/adapters/structures/proteindatabank.py
+++ b/optimade/adapters/structures/proteindatabank.py
@@ -21,7 +21,6 @@
Warning:
Currently, the PDBx/mmCIF conversion function is not parsing as a complete PDBx/mmCIF file.
"""
-from typing import Dict
try:
import numpy as np
@@ -46,7 +45,7 @@
__all__ = ("get_pdb", "get_pdbx_mmcif")
-def get_pdbx_mmcif( # pylint: disable=too-many-locals
+def get_pdbx_mmcif(
optimade_structure: OptimadeStructure,
) -> str:
"""Write Protein Data Bank (PDB) structure in the PDBx/mmCIF format from OPTIMADE structure.
@@ -164,8 +163,9 @@ def get_pdbx_mmcif( # pylint: disable=too-many-locals
else:
sites = attributes.cartesian_site_positions
- species: Dict[str, OptimadeStructureSpecies] = {
- species.name: species for species in attributes.species # type: ignore[union-attr]
+ species: dict[str, OptimadeStructureSpecies] = {
+ species.name: species
+ for species in attributes.species # type: ignore[union-attr]
}
for site_number in range(attributes.nsites): # type: ignore[arg-type]
@@ -197,7 +197,7 @@ def get_pdbx_mmcif( # pylint: disable=too-many-locals
return cif
-def get_pdb( # pylint: disable=too-many-locals
+def get_pdb(
optimade_structure: OptimadeStructure,
) -> str:
"""Write Protein Data Bank (PDB) structure in the old PDB format from OPTIMADE structure.
@@ -240,7 +240,7 @@ def get_pdb( # pylint: disable=too-many-locals
pdb += "MODEL 1\n"
- species: Dict[str, OptimadeStructureSpecies] = {
+ species: dict[str, OptimadeStructureSpecies] = {
species.name: species
for species in attributes.species # type:ignore[union-attr]
}
diff --git a/optimade/adapters/structures/pymatgen.py b/optimade/adapters/structures/pymatgen.py
index 8d46bf7fc..e3609c228 100644
--- a/optimade/adapters/structures/pymatgen.py
+++ b/optimade/adapters/structures/pymatgen.py
@@ -7,7 +7,9 @@
For more information on the pymatgen code see [their documentation](https://pymatgen.org).
"""
-from typing import Dict, List, Optional, Union
+
+from typing import Optional, Union
+from warnings import warn
from optimade.adapters.structures.utils import (
species_from_species_at_sites,
@@ -22,13 +24,12 @@
from pymatgen.core import Lattice, Molecule, Structure
except (ImportError, ModuleNotFoundError):
- from warnings import warn
-
from optimade.adapters.warnings import AdapterPackageNotFound
Structure = type("Structure", (), {})
Molecule = type("Molecule", (), {})
Composition = type("Composition", (), {})
+ Lattice = type("Lattice", (), {})
PYMATGEN_NOT_FOUND = "Pymatgen not found, cannot convert structure to a pymatgen Structure or Molecule"
@@ -77,7 +78,10 @@ def _get_structure(optimade_structure: OptimadeStructure) -> Structure:
attributes = optimade_structure.attributes
return Structure(
- lattice=Lattice(attributes.lattice_vectors, attributes.dimension_types),
+ lattice=Lattice(
+ attributes.lattice_vectors,
+ [bool(d) for d in attributes.dimension_types], # type: ignore[union-attr]
+ ),
species=_pymatgen_species(
nsites=attributes.nsites, # type: ignore[arg-type]
species=attributes.species,
@@ -105,14 +109,14 @@ def _get_molecule(optimade_structure: OptimadeStructure) -> Molecule:
def _pymatgen_species(
nsites: int,
- species: Optional[List[OptimadeStructureSpecies]],
- species_at_sites: List[str],
-) -> List[Dict[str, float]]:
+ species: Optional[list[OptimadeStructureSpecies]],
+ species_at_sites: list[str],
+) -> list[dict[str, float]]:
"""
Create list of {"symbol": "concentration"} per site for values to pymatgen species parameters.
Remove vacancies, if they are present.
"""
- if not species:
+ if species is None:
# If species is missing, infer data from species_at_sites
species = species_from_species_at_sites(species_at_sites)
@@ -147,7 +151,7 @@ def from_pymatgen(pmg_structure: Structure) -> StructureResourceAttributes:
Returns:
An OPTIMADE `StructureResourceAttributes` model, which can be converted to a raw Python
- dictionary with `.dict()` or to JSON with `.json()`.
+ dictionary with `.model_dump()` or to JSON with `.model_dump_json()`.
"""
diff --git a/optimade/adapters/structures/utils.py b/optimade/adapters/structures/utils.py
index 2b36570bd..39a57e360 100644
--- a/optimade/adapters/structures/utils.py
+++ b/optimade/adapters/structures/utils.py
@@ -3,7 +3,9 @@
Most of these functions rely on the [NumPy](https://numpy.org/) library.
"""
-from typing import Iterable, List, Optional, Tuple, Type
+
+from collections.abc import Iterable
+from typing import Optional
from optimade.models.structures import Species as OptimadeStructureSpecies
from optimade.models.structures import Vector3D
@@ -19,7 +21,7 @@
NUMPY_NOT_FOUND = "NumPy not found, cannot convert structure to your desired format"
-def valid_lattice_vector(lattice_vec: Tuple[Vector3D, Vector3D, Vector3D]):
+def valid_lattice_vector(lattice_vec: tuple[Vector3D, Vector3D, Vector3D]):
if len(lattice_vec) != 3:
return False
for vector in lattice_vec:
@@ -31,8 +33,8 @@ def valid_lattice_vector(lattice_vec: Tuple[Vector3D, Vector3D, Vector3D]):
def scaled_cell(
- cell: Tuple[Vector3D, Vector3D, Vector3D]
-) -> Tuple[Vector3D, Vector3D, Vector3D]:
+ cell: tuple[Vector3D, Vector3D, Vector3D],
+) -> tuple[Vector3D, Vector3D, Vector3D]:
"""Return a scaled 3x3 cell from cartesian 3x3 cell (`lattice_vectors`).
This 3x3 matrix can be used to calculate the fractional coordinates from the cartesian_site_positions.
@@ -62,8 +64,8 @@ def scaled_cell(
def fractional_coordinates(
- cell: Tuple[Vector3D, Vector3D, Vector3D], cartesian_positions: List[Vector3D]
-) -> List[Vector3D]:
+ cell: tuple[Vector3D, Vector3D, Vector3D], cartesian_positions: list[Vector3D]
+) -> list[Vector3D]:
"""Returns fractional coordinates and wraps coordinates to `[0,1[`.
Note:
@@ -97,12 +99,12 @@ def fractional_coordinates(
fractional[:, i] %= 1.0
fractional[:, i] %= 1.0
- return [tuple(position) for position in fractional]
+ return [tuple(position) for position in fractional] # type: ignore
def cell_to_cellpar(
- cell: Tuple[Vector3D, Vector3D, Vector3D], radians: bool = False
-) -> List[float]:
+ cell: tuple[Vector3D, Vector3D, Vector3D], radians: bool = False
+) -> list[float]:
"""Returns the cell parameters `[a, b, c, alpha, beta, gamma]`.
Angles are in degrees unless `radian=True` is used.
@@ -157,14 +159,14 @@ def unit_vector(x: Vector3D) -> Vector3D:
return None # type: ignore[return-value]
y = np.array(x, dtype="float")
- return y / np.linalg.norm(y)
+ return y / np.linalg.norm(y) # type: ignore
def cellpar_to_cell(
- cellpar: List[float],
- ab_normal: Tuple[int, int, int] = (0, 0, 1),
- a_direction: Optional[Tuple[int, int, int]] = None,
-) -> List[Vector3D]:
+ cellpar: list[float],
+ ab_normal: tuple[int, int, int] = (0, 0, 1),
+ a_direction: Optional[tuple[int, int, int]] = None,
+) -> list[Vector3D]:
"""Return a 3x3 cell matrix from `cellpar=[a,b,c,alpha,beta,gamma]`.
Angles must be in degrees.
@@ -218,7 +220,7 @@ def cellpar_to_cell(
# Define rotated X,Y,Z-system, with Z along ab_normal and X along
# the projection of a_direction onto the normal plane of Z.
a_direction_array = np.array(a_direction)
- Z = unit_vector(ab_normal)
+ Z = unit_vector(ab_normal) # type: ignore
X = unit_vector(a_direction_array - np.dot(a_direction_array, Z) * Z)
Y = np.cross(Z, X)
@@ -277,9 +279,9 @@ def cellpar_to_cell(
def _pad_iter_of_iters(
iterable: Iterable[Iterable],
padding: Optional[float] = None,
- outer: Optional[Type] = None,
- inner: Optional[Type] = None,
-) -> Tuple[Iterable[Iterable], bool]:
+ outer: Optional[type] = None,
+ inner: Optional[type] = None,
+) -> tuple[Iterable[Iterable], bool]:
"""Turn any null/None values into a float in given iterable of iterables"""
try:
padding = float(padding) # type: ignore[arg-type]
@@ -306,7 +308,7 @@ def _pad_iter_of_iters(
def pad_cell(
- lattice_vectors: Tuple[Vector3D, Vector3D, Vector3D],
+ lattice_vectors: tuple[Vector3D, Vector3D, Vector3D],
padding: Optional[float] = None,
) -> tuple: # Setting this properly makes MkDocs fail.
"""Turn any `null`/`None` values into a `float` in given `tuple` of
@@ -333,8 +335,8 @@ def pad_cell(
def species_from_species_at_sites(
- species_at_sites: List[str],
-) -> List[OptimadeStructureSpecies]:
+ species_at_sites: list[str],
+) -> list[OptimadeStructureSpecies]:
"""When a list of species dictionaries is not provided, this function
can be used to infer the species from the provided species_at_sites.
@@ -357,7 +359,7 @@ def species_from_species_at_sites(
]
-def elements_ratios_from_species_at_sites(species_at_sites: List[str]) -> List[float]:
+def elements_ratios_from_species_at_sites(species_at_sites: list[str]) -> list[float]:
"""Compute the OPTIMADE `elements_ratios` field from `species_at_sites` in the case where `species_at_sites` refers
to sites wholly occupied by the given elements, e.g., not arbitrary species labels or with partial/mixed occupancy.
diff --git a/optimade/client/cli.py b/optimade/client/cli.py
index d4f9a923c..6c15ca035 100644
--- a/optimade/client/cli.py
+++ b/optimade/client/cli.py
@@ -1,16 +1,33 @@
import json
import pathlib
import sys
+from typing import TYPE_CHECKING
import click
import rich
+from optimade import __api_version__, __version__
from optimade.client.client import OptimadeClient
+if TYPE_CHECKING: # pragma: no cover
+ from typing import Union
+
+ from optimade.client.utils import QueryResults
+
+ ClientResult = Union[
+ dict[str, list[str]],
+ dict[str, dict[str, dict[str, int]]],
+ dict[str, dict[str, dict[str, QueryResults]]],
+ ]
+
__all__ = ("_get",)
@click.command("optimade-get", no_args_is_help=True)
+@click.version_option(
+ __version__,
+ prog_name=f"optimade-get, an async OPTIMADE v{__api_version__} client",
+)
@click.option(
"--filter",
default=[None],
@@ -88,6 +105,8 @@
default=None,
nargs=-1,
)
+@click.option("-v", "--verbosity", count=True, help="Increase verbosity of output.")
+@click.option("--skip-ssl", is_flag=True, help="Ignore SSL errors in HTTPS requests.")
@click.option(
"--http-timeout",
type=float,
@@ -110,6 +129,8 @@ def get(
include_providers,
exclude_providers,
exclude_databases,
+ verbosity,
+ skip_ssl,
http_timeout,
):
return _get(
@@ -129,6 +150,8 @@ def get(
include_providers,
exclude_providers,
exclude_databases,
+ verbosity,
+ skip_ssl,
http_timeout,
)
@@ -150,6 +173,8 @@ def _get(
include_providers,
exclude_providers,
exclude_databases,
+ verbosity,
+ skip_ssl,
http_timeout,
**kwargs,
):
@@ -162,27 +187,30 @@ def _get(
f"Desired output file {output_file} already exists, not overwriting."
)
- args = dict(
- base_urls=base_url,
- use_async=use_async,
- max_results_per_provider=max_results_per_provider,
- include_providers=set(_.strip() for _ in include_providers.split(","))
+ args = {
+ "base_urls": base_url,
+ "use_async": use_async,
+ "max_results_per_provider": max_results_per_provider,
+ "include_providers": {_.strip() for _ in include_providers.split(",")}
if include_providers
else None,
- exclude_providers=set(_.strip() for _ in exclude_providers.split(","))
+ "exclude_providers": {_.strip() for _ in exclude_providers.split(",")}
if exclude_providers
else None,
- exclude_databases=set(_.strip() for _ in exclude_databases.split(","))
+ "exclude_databases": {_.strip() for _ in exclude_databases.split(",")}
if exclude_databases
else None,
- silent=silent,
- )
+ "silent": silent,
+ "skip_ssl": skip_ssl,
+ }
# Only set http timeout if its not null to avoid overwriting or duplicating the
# default value set on the OptimadeClient class
if http_timeout:
args["http_timeout"] = http_timeout
+ args["verbosity"] = verbosity
+
client = OptimadeClient(
**args,
**kwargs,
@@ -190,6 +218,9 @@ def _get(
if response_fields:
response_fields = response_fields.split(",")
try:
+ if TYPE_CHECKING: # pragma: no cover
+ results: ClientResult
+
if count:
for f in filter:
client.count(f, endpoint=endpoint)
@@ -211,13 +242,15 @@ def _get(
if not output_file:
if pretty_print:
- rich.print_json(data=results, indent=2, default=lambda _: _.dict())
+ rich.print_json(data=results, indent=2, default=lambda _: _.asdict())
else:
- sys.stdout.write(json.dumps(results, indent=2, default=lambda _: _.dict()))
+ sys.stdout.write(
+ json.dumps(results, indent=2, default=lambda _: _.asdict())
+ )
if output_file:
with open(output_file, "w") as f:
- json.dump(results, f, indent=2, default=lambda _: _.dict())
+ json.dump(results, f, indent=2, default=lambda _: _.asdict())
if __name__ == "__main__":
diff --git a/optimade/client/client.py b/optimade/client/client.py
index 1efacca15..2a6663970 100644
--- a/optimade/client/client.py
+++ b/optimade/client/client.py
@@ -8,20 +8,11 @@
import asyncio
import functools
import json
+import math
import time
from collections import defaultdict
-from typing import (
- Any,
- Callable,
- Dict,
- Iterable,
- List,
- Optional,
- Set,
- Tuple,
- Type,
- Union,
-)
+from collections.abc import Iterable
+from typing import Any, Callable, Optional, Union
from urllib.parse import urlparse
# External deps that are only used in the client code
@@ -70,12 +61,12 @@ class OptimadeClient:
base_urls: Union[str, Iterable[str]]
"""A list (or any iterable) of OPTIMADE base URLs to query."""
- all_results: Dict[str, Dict[str, Dict[str, QueryResults]]] = defaultdict(dict)
+ all_results: dict[str, dict[str, dict[str, QueryResults]]] = defaultdict(dict)
"""A nested dictionary keyed by endpoint and OPTIMADE filter string that contains
the results from each base URL for that particular filter.
"""
- count_results: Dict[str, Dict[str, Dict[str, int]]] = defaultdict(dict)
+ count_results: dict[str, dict[str, dict[str, int]]] = defaultdict(dict)
"""A nested dictionary keyed by endpoint and OPTIMADE filter string that contains
the number of results from each base URL for that particular filter.
"""
@@ -85,12 +76,12 @@ class OptimadeClient:
download all.
"""
- property_lists: Dict[str, Dict[str, List[str]]] = defaultdict(dict)
+ property_lists: dict[str, dict[str, list[str]]] = defaultdict(dict)
"""A dictionary containing list of properties served by each database,
broken down by entry type, then database.
"""
- headers: Dict = {"User-Agent": f"optimade-python-tools/{__version__}"}
+ headers: dict = {"User-Agent": f"optimade-python-tools/{__version__}"}
"""Additional HTTP headers."""
http_timeout: httpx.Timeout = httpx.Timeout(10.0, read=1000.0)
@@ -102,7 +93,7 @@ class OptimadeClient:
use_async: bool
"""Whether or not to make all requests asynchronously using asyncio."""
- callbacks: Optional[List[Callable[[str, Dict], Union[None, Dict]]]] = None
+ callbacks: Optional[list[Callable[[str, dict], Union[None, dict]]]] = None
"""A list of callbacks to execute after each successful request, used
to e.g., write to a file, add results to a database or perform additional
filtering.
@@ -118,25 +109,31 @@ class OptimadeClient:
"""
+ count_binary_search: bool = True
+ """Enable binary search count for databases that do not support `meta->data_returned`."""
+
silent: bool
"""Whether to disable progress bar printing."""
- _excluded_providers: Optional[Set[str]] = None
+ skip_ssl: bool = False
+ """Whether to skip SSL verification."""
+
+ _excluded_providers: Optional[set[str]] = None
"""A set of providers IDs excluded from future queries."""
- _included_providers: Optional[Set[str]] = None
+ _included_providers: Optional[set[str]] = None
"""A set of providers IDs included from future queries."""
- _excluded_databases: Optional[Set[str]] = None
+ _excluded_databases: Optional[set[str]] = None
"""A set of child database URLs excluded from future queries."""
__current_endpoint: Optional[str] = None
"""Used internally when querying via `client.structures.get()` to set the
chosen endpoint. Should be reset to `None` outside of all `get()` calls."""
- _http_client: Optional[
- Union[Type[httpx.AsyncClient], Type[requests.Session]]
- ] = None
+ _http_client: Optional[Union[type[httpx.AsyncClient], type[requests.Session]]] = (
+ None
+ )
"""Override the HTTP client class, primarily used for testing."""
__strict_async: bool = False
@@ -144,22 +141,29 @@ class OptimadeClient:
is impossible due to, e.g., a running event loop.
"""
+ _force_binary_search: bool = False
+ """Setting to test binary searches in cases where servers do return
+ the count.
+ """
+
def __init__(
self,
base_urls: Optional[Union[str, Iterable[str]]] = None,
max_results_per_provider: int = 1000,
- headers: Optional[Dict] = None,
+ headers: Optional[dict] = None,
http_timeout: Optional[Union[httpx.Timeout, float]] = None,
max_attempts: int = 5,
use_async: bool = True,
silent: bool = False,
- exclude_providers: Optional[List[str]] = None,
- include_providers: Optional[List[str]] = None,
- exclude_databases: Optional[List[str]] = None,
+ exclude_providers: Optional[list[str]] = None,
+ include_providers: Optional[list[str]] = None,
+ exclude_databases: Optional[list[str]] = None,
http_client: Optional[
- Union[Type[httpx.AsyncClient], Type[requests.Session]]
+ Union[type[httpx.AsyncClient], type[requests.Session]]
] = None,
- callbacks: Optional[List[Callable[[str, Dict], Union[None, Dict]]]] = None,
+ verbosity: int = 0,
+ callbacks: Optional[list[Callable[[str, dict], Union[None, dict]]]] = None,
+ skip_ssl: bool = False,
):
"""Create the OPTIMADE client object.
@@ -180,6 +184,7 @@ def __init__(
callbacks: A list of functions to call after each successful response, see the
attribute [`OptimadeClient.callbacks`][optimade.client.OptimadeClient.callbacks]
docstring for more details.
+ verbosity: The verbosity level of the client.
"""
@@ -191,11 +196,26 @@ def __init__(
self._included_providers = set(include_providers) if include_providers else None
self._excluded_databases = set(exclude_databases) if exclude_databases else None
+ self.max_attempts = max_attempts
+ self.silent = silent
+ self.verbosity = verbosity
+ self.skip_ssl = skip_ssl
+
+ if headers:
+ self.headers.update(headers)
+
if not base_urls:
- self.base_urls = get_all_databases(
- exclude_providers=self._excluded_providers,
- include_providers=self._included_providers,
- exclude_databases=self._excluded_databases,
+ progress = None
+ if not self.silent:
+ progress = OptimadeClientProgress()
+ self.base_urls = list(
+ get_all_databases(
+ exclude_providers=self._excluded_providers,
+ include_providers=self._included_providers,
+ exclude_databases=self._excluded_databases,
+ progress=progress,
+ skip_ssl=self.skip_ssl,
+ )
)
else:
if exclude_providers or include_providers or exclude_databases:
@@ -205,27 +225,21 @@ def __init__(
self.base_urls = base_urls
- if isinstance(self.base_urls, str):
- self.base_urls = [self.base_urls]
- self.base_urls = list(self.base_urls)
+ if isinstance(self.base_urls, str):
+ self.base_urls = [self.base_urls]
+ self.base_urls = list(self.base_urls)
if not self.base_urls:
raise SystemExit(
"Unable to access any OPTIMADE base URLs. If you believe this is an error, try manually specifying some base URLs."
)
- if headers:
- self.headers.update(headers)
-
if http_timeout:
if isinstance(http_timeout, httpx.Timeout):
self.http_timeout = http_timeout
else:
self.http_timeout = httpx.Timeout(http_timeout)
- self.max_attempts = max_attempts
- self.silent = silent
-
self.use_async = use_async
if http_client:
@@ -284,9 +298,9 @@ def get(
self,
filter: Optional[str] = None,
endpoint: Optional[str] = None,
- response_fields: Optional[List[str]] = None,
+ response_fields: Optional[list[str]] = None,
sort: Optional[str] = None,
- ) -> Dict[str, Dict[str, Dict[str, Dict]]]:
+ ) -> dict[str, dict[str, dict[str, dict]]]:
"""Gets the results from the endpoint and filter across the
defined OPTIMADE APIs.
@@ -338,14 +352,15 @@ def get(
sort=sort,
)
self.all_results[endpoint][filter] = results
- return {endpoint: {filter: {k: results[k].dict() for k in results}}}
+ return {endpoint: {filter: {k: results[k].asdict() for k in results}}}
def count(
self, filter: Optional[str] = None, endpoint: Optional[str] = None
- ) -> Dict[str, Dict[str, Dict[str, Optional[int]]]]:
+ ) -> dict[str, dict[str, dict[str, Optional[int]]]]:
"""Counts the number of results for the filter, requiring
only 1 request per provider by making use of the `meta->data_returned`
- key.
+ key. If missing, attempts will be made to perform an exponential/binary
+ search over pagination to count the results.
Raises:
RuntimeError: If the query could not be executed.
@@ -394,18 +409,186 @@ def count(
"data_returned", None
)
- if count_results[base_url] is None:
- self._progress.print(
- f"Warning: {base_url} did not return a value for `meta->data_returned`, unable to count results. Full response: {results[base_url]}"
- )
+ if count_results[base_url] is None or self._force_binary_search:
+ if self.count_binary_search:
+ count_results[base_url] = self.binary_search_count(
+ filter, endpoint, base_url, results
+ )
+ else:
+ self._progress.print(
+ f"Warning: {base_url} did not return a value for `meta->data_returned`, unable to count results. Full response: {results[base_url]}"
+ )
self.count_results[endpoint][filter] = count_results
return {endpoint: {filter: count_results}}
+ def binary_search_count(
+ self, filter: str, endpoint: str, base_url: str, results: Optional[dict] = None
+ ) -> int:
+ """In cases where `data_returned` is not available (due to database limitations or
+ otherwise), iteratively probe the final page of results available for a filter using
+ binary search.
+
+ Note: These queries always happen synchronously across APIs, but can be executed
+ asynchronously within a single API.
+
+ Parameters:
+ filter: The OPTIMADE filter string for the query.
+ endpoint: The endpoint to query.
+ base_url: The base URL to query.
+ results: The results from a previous query for the first page of results.
+
+ Returns:
+ The number of results for the filter.
+
+ """
+ if self.verbosity:
+ self._progress.print(f"Performing binary search count for {base_url}")
+ if self.use_async:
+ return self._binary_search_count_async(filter, endpoint, base_url, results)
+
+ else:
+ raise NotImplementedError(
+ "Binary search count is not yet implemented for synchronous queries."
+ )
+
+ def _binary_search_count_async(
+ self, filter: str, endpoint: str, base_url: str, result: Optional[dict] = None
+ ) -> int:
+ """Run a series of asynchronously queries on a given API to
+ find the number of results for a filter.
+
+ Starting with logarithmically spaced page offsets, iteratively probe
+ the final page of results available for a filter.
+
+ Parameters:
+ filter: The OPTIMADE filter string for the query.
+ endpoint: The endpoint to query.
+ base_url: The base URL to query.
+ result: The results from a previous query for the first page of results.
+
+ Returns:
+ The number of results for the filter.
+
+ """
+ if result is None:
+ # first a check that there are any results at all
+ result = asyncio.run(
+ self.get_one_async(
+ endpoint,
+ filter,
+ base_url,
+ page_limit=1,
+ response_fields=[],
+ paginate=False,
+ )
+ )
+ if self.verbosity:
+ self._progress.print("Definitely found results")
+
+ if not result[base_url].data:
+ return 0
+
+ attempts = 0
+ max_attempts = 100
+
+ window, probe = self._update_probe_and_window()
+
+ while attempts < max_attempts:
+ self._progress.disable = True
+
+ result = asyncio.run(
+ self.get_one_async(
+ endpoint,
+ filter,
+ base_url,
+ page_limit=1,
+ response_fields=[],
+ paginate=False,
+ other_params={"page_offset": probe},
+ )
+ )
+
+ self._progress.disable = self.silent
+
+ window, probe = self._update_probe_and_window(
+ window, probe, bool(result[base_url].data)
+ )
+
+ if window[0] == window[1] and window[0] == probe:
+ return probe
+
+ attempts += 1
+
+ if self.verbosity > 2:
+ self._progress.print(f"Binary search debug info: {window=}, {probe=}")
+
+ else:
+ message = f"Exceeded maximum number of attempts for binary search on {base_url}, {filter=}"
+ self._progress.print(message)
+ raise RuntimeError(message)
+
+ @staticmethod
+ def _update_probe_and_window(
+ window: Optional[tuple[int, Optional[int]]] = None,
+ last_probe: Optional[int] = None,
+ below: Optional[bool] = None,
+ ) -> tuple[tuple[int, Optional[int]], int]:
+ """Sets the new range, trial value and exit condition for exponential/binary search.
+ When converged, returns the same value three times.
+
+ Parameters:
+ window: The current window of results.
+ last_probe: The last probe value.
+ below: Whether the last probe was below the target value.
+
+ Returns:
+ A tuple of the new window and probe value,
+ or the count three times if converged.
+
+ """
+
+ if window is None and last_probe is None:
+ return (1, None), 1_000_000
+
+ if window is None or last_probe is None:
+ 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]
+
+ # Enclose the real value in the window, with `None` indicating an open boundary
+ if below:
+ window = (last_probe, window[1])
+ else:
+ window = (window[0], last_probe)
+
+ # If we've not reached the upper bound yet, try 10x
+ 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])):
+ 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))
+
+ return window, probe
+
def list_properties(
self,
entry_type: str,
- ) -> Dict[str, List[str]]:
+ ) -> dict[str, list[str]]:
"""Returns the list of properties reported at `/info/`
for the given entry type, for each database.
@@ -437,7 +620,7 @@ def list_properties(
)
return self.property_lists[entry_type]
- def search_property(self, query: str, entry_type: str) -> Dict[str, List[str]]:
+ def search_property(self, query: str, entry_type: str) -> dict[str, list[str]]:
"""Searches for the query substring within the listed properties
served by each database.
@@ -453,7 +636,7 @@ def search_property(self, query: str, entry_type: str) -> Dict[str, List[str]]:
if not self.property_lists:
self.list_properties(entry_type=entry_type)
- matching_properties: Dict[str, Dict[str, List[str]]] = {
+ matching_properties: dict[str, dict[str, list[str]]] = {
entry_type: defaultdict(list)
}
if entry_type in self.property_lists:
@@ -469,9 +652,9 @@ def _execute_queries(
endpoint: str,
page_limit: Optional[int],
paginate: bool,
- response_fields: Optional[List[str]],
+ response_fields: Optional[list[str]],
sort: Optional[str],
- ) -> Dict[str, QueryResults]:
+ ) -> dict[str, QueryResults]:
"""Executes the queries over the base URLs either asynchronously or
serially, depending on the `self.use_async` setting.
@@ -491,7 +674,6 @@ def _execute_queries(
A mapping from base URL to `QueryResults` for each queried API.
"""
-
if self.use_async:
# Check for a pre-existing event loop (e.g. within a Jupyter notebook)
# and use it if present
@@ -503,9 +685,10 @@ def _execute_queries(
"Detected a running event loop, cannot run in async mode."
)
self._progress.print(
- "Detected a running event loop (e.g., Jupyter, pytest). Trying to use nest_asyncio."
+ "Detected a running event loop (e.g., Jupyter). Attempting to switch to synchronous mode."
)
self.use_async = False
+ self._http_client = requests.Session
except RuntimeError:
event_loop = None
@@ -535,11 +718,12 @@ def get_one(
endpoint: str,
filter: str,
base_url: str,
- response_fields: Optional[List[str]] = None,
+ response_fields: Optional[list[str]] = None,
sort: Optional[str] = None,
page_limit: Optional[int] = None,
paginate: bool = True,
- ) -> Dict[str, QueryResults]:
+ other_params: Optional[dict[str, Any]] = None,
+ ) -> dict[str, QueryResults]:
"""Executes the query synchronously on one API.
Parameters:
@@ -553,6 +737,7 @@ def get_one(
paginate: Whether to pull all pages of results (up to the
value of `max_results_per_provider`) or whether to return
after one page.
+ other_params: Any other parameters to pass to the server.
Returns:
A dictionary mapping from base URL to the results of the query.
@@ -567,6 +752,7 @@ def get_one(
paginate=paginate,
response_fields=response_fields,
sort=sort,
+ other_params=other_params,
)
except Exception as exc:
error_query_results = QueryResults()
@@ -582,11 +768,13 @@ async def _get_all_async(
self,
endpoint: str,
filter: str,
- response_fields: Optional[List[str]] = None,
+ response_fields: Optional[list[str]] = None,
sort: Optional[str] = None,
page_limit: Optional[int] = None,
paginate: bool = True,
- ) -> Dict[str, QueryResults]:
+ base_urls: Optional[Iterable[str]] = None,
+ other_params: Optional[dict[str, Any]] = None,
+ ) -> dict[str, QueryResults]:
"""Executes the query asynchronously across all defined APIs.
Parameters:
@@ -600,11 +788,16 @@ async def _get_all_async(
paginate: Whether to pull all pages of results (up to the
value of `max_results_per_provider`) or whether to return
after one page.
+ base_urls: A list of base URLs to query (defaults to `self.base_urls`).
+ other_params: Any other parameters to pass to the server.
Returns:
A dictionary mapping from base URL to the results of the query.
"""
+ if not base_urls:
+ base_urls = self.base_urls
+
results = await asyncio.gather(
*[
self.get_one_async(
@@ -615,8 +808,9 @@ async def _get_all_async(
paginate=paginate,
response_fields=response_fields,
sort=sort,
+ other_params=other_params,
)
- for base_url in self.base_urls
+ for base_url in base_urls
]
)
return functools.reduce(lambda r1, r2: {**r1, **r2}, results)
@@ -626,10 +820,12 @@ def _get_all(
endpoint: str,
filter: str,
page_limit: Optional[int] = None,
- response_fields: Optional[List[str]] = None,
+ response_fields: Optional[list[str]] = None,
sort: Optional[str] = None,
paginate: bool = True,
- ) -> Dict[str, QueryResults]:
+ base_urls: Optional[Iterable[str]] = None,
+ other_params: Optional[dict[str, Any]] = None,
+ ) -> dict[str, QueryResults]:
"""Executes the query synchronously across all defined APIs.
Parameters:
@@ -643,11 +839,15 @@ def _get_all(
paginate: Whether to pull all pages of results (up to the
value of `max_results_per_provider`) or whether to return
after one page.
+ base_urls: A list of base URLs to query (defaults to `self.base_urls`).
+ other_params: Any other parameters to pass to the server.
Returns:
A dictionary mapping from base URL to the results of the query.
"""
+ if not base_urls:
+ base_urls = self.base_urls
results = [
self.get_one(
endpoint,
@@ -657,8 +857,9 @@ def _get_all(
paginate=paginate,
response_fields=response_fields,
sort=sort,
+ other_params=other_params,
)
- for base_url in self.base_urls
+ for base_url in base_urls
]
if results:
return functools.reduce(lambda r1, r2: {**r1, **r2}, results)
@@ -670,11 +871,12 @@ async def get_one_async(
endpoint: str,
filter: str,
base_url: str,
- response_fields: Optional[List[str]] = None,
+ response_fields: Optional[list[str]] = None,
sort: Optional[str] = None,
page_limit: Optional[int] = None,
paginate: bool = True,
- ) -> Dict[str, QueryResults]:
+ other_params: Optional[dict[str, Any]] = None,
+ ) -> dict[str, QueryResults]:
"""Executes the query asynchronously on one API.
!!! note
@@ -695,6 +897,7 @@ async def get_one_async(
paginate: Whether to pull all pages of results (up to the
value of `max_results_per_provider`) or whether to return
after one page.
+ other_params: Any other parameters to pass to the server.
Returns:
A dictionary mapping from base URL to the results of the query.
@@ -709,6 +912,7 @@ async def get_one_async(
paginate=paginate,
response_fields=response_fields,
sort=sort,
+ other_params=other_params,
)
except Exception as exc:
error_query_results = QueryResults()
@@ -725,11 +929,12 @@ async def _get_one_async(
endpoint: str,
filter: str,
base_url: str,
- response_fields: Optional[List[str]] = None,
+ response_fields: Optional[list[str]] = None,
sort: Optional[str] = None,
page_limit: Optional[int] = None,
paginate: bool = True,
- ) -> Dict[str, QueryResults]:
+ other_params: Optional[dict[str, Any]] = None,
+ ) -> dict[str, QueryResults]:
"""See [`OptimadeClient.get_one_async`][optimade.client.OptimadeClient.get_one_async]."""
next_url, _task = self._setup(
endpoint=endpoint,
@@ -738,6 +943,7 @@ async def _get_one_async(
page_limit=page_limit,
response_fields=response_fields,
sort=sort,
+ other_params=other_params,
)
results = QueryResults()
try:
@@ -745,6 +951,10 @@ async def _get_one_async(
while next_url:
attempts = 0
try:
+ if self.verbosity:
+ self._progress.print(
+ f"Making request to {next_url!r} {attempts=}"
+ )
r = await client.get(
next_url, follow_redirects=True, timeout=self.http_timeout
)
@@ -785,9 +995,10 @@ def _get_one(
base_url: str,
sort: Optional[str] = None,
page_limit: Optional[int] = None,
- response_fields: Optional[List[str]] = None,
+ response_fields: Optional[list[str]] = None,
paginate: bool = True,
- ) -> Dict[str, QueryResults]:
+ other_params: Optional[dict[str, Any]] = None,
+ ) -> dict[str, QueryResults]:
"""See [`OptimadeClient.get_one`][optimade.client.OptimadeClient.get_one]."""
next_url, _task = self._setup(
endpoint=endpoint,
@@ -796,6 +1007,7 @@ def _get_one(
page_limit=page_limit,
response_fields=response_fields,
sort=sort,
+ other_params=other_params,
)
results = QueryResults()
try:
@@ -809,6 +1021,10 @@ def _get_one(
while next_url:
attempts = 0
try:
+ if self.verbosity:
+ self._progress.print(
+ f"Making request to {next_url!r} {attempts=}"
+ )
r = client.get(next_url, timeout=timeout)
page_results, next_url = self._handle_response(r, _task)
except RecoverableHTTPError:
@@ -846,9 +1062,10 @@ def _setup(
base_url: str,
filter: str,
page_limit: Optional[int],
- response_fields: Optional[List[str]],
+ response_fields: Optional[list[str]],
sort: Optional[str],
- ) -> Tuple[str, TaskID]:
+ other_params: Optional[dict[str, Any]] = None,
+ ) -> tuple[str, TaskID]:
"""Constructs the first query URL and creates the progress bar task.
Returns:
@@ -862,6 +1079,7 @@ def _setup(
page_limit=page_limit,
response_fields=response_fields,
sort=sort,
+ other_params=other_params,
)
parsed_url = urlparse(url)
_task = self._progress.add_task(
@@ -876,9 +1094,10 @@ def _build_url(
endpoint: Optional[str] = "structures",
version: Optional[str] = None,
filter: Optional[str] = None,
- response_fields: Optional[List[str]] = None,
+ response_fields: Optional[list[str]] = None,
sort: Optional[str] = None,
page_limit: Optional[int] = None,
+ other_params: Optional[dict[str, Any]] = None,
) -> str:
"""Builds the URL to query based on the passed parameters.
@@ -890,6 +1109,7 @@ def _build_url(
response_fields: A list of response fields to request from the server.
sort: The field by which to sort the results.
page_limit: The page limit for an individual request.
+ other_params: Any other parameters to pass to the server.
Returns:
The overall query URL, including parameters.
@@ -903,28 +1123,29 @@ def _build_url(
url = f"{base_url}/{version}/{endpoint}"
- # Handle params
- _filter: Optional[str] = None
- _response_fields: Optional[str] = None
- _page_limit: Optional[str] = None
- _sort: Optional[str] = None
+ params_dict: dict[str, str] = {}
if filter:
- _filter = f"filter={filter}"
+ params_dict["filter"] = f"filter={filter}"
if response_fields is not None:
# If we have requested no response fields (e.g., in the case of --count) then just ask for IDs
if len(response_fields) == 0:
- _response_fields = "response_fields=id"
+ params_dict["response_fields"] = "response_fields=id"
else:
- _response_fields = f'response_fields={",".join(response_fields)}'
+ params_dict["response_fields"] = (
+ f'response_fields={",".join(response_fields)}'
+ )
+
if page_limit:
- _page_limit = f"page_limit={page_limit}"
+ params_dict["page_limit"] = f"page_limit={page_limit}"
if sort:
- _sort = f"sort={sort}"
+ params_dict["sort"] = f"sort={sort}"
- params = "&".join(
- p for p in (_filter, _response_fields, _page_limit, _sort) if p
- )
+ if other_params:
+ for p in other_params:
+ params_dict[p] = f"{p}={other_params[p]}"
+
+ params = "&".join(p for p in params_dict.values() if p)
if params:
url += f"?{params}"
@@ -957,7 +1178,7 @@ def _check_filter(self, filter: str, endpoint: str) -> None:
def _handle_response(
self, response: Union[httpx.Response, requests.Response], _task: TaskID
- ) -> Tuple[Dict[str, Any], str]:
+ ) -> tuple[dict[str, Any], str]:
"""Handle the response from the server.
Parameters:
@@ -1036,8 +1257,8 @@ def _teardown(self, _task: TaskID, num_results: int) -> None:
)
def _execute_callbacks(
- self, results: Dict, response: Union[httpx.Response, requests.Response]
- ) -> Union[None, Dict]:
+ self, results: dict, response: Union[httpx.Response, requests.Response]
+ ) -> Union[None, dict]:
"""Execute any callbacks registered with the client.
Parameters:
diff --git a/optimade/client/utils.py b/optimade/client/utils.py
index bfa139b9d..6aa9f3d43 100644
--- a/optimade/client/utils.py
+++ b/optimade/client/utils.py
@@ -1,7 +1,7 @@
import sys
from contextlib import contextmanager
from dataclasses import asdict, dataclass, field
-from typing import Dict, List, Set, Union
+from typing import Union
from rich.console import Console
from rich.progress import (
@@ -34,22 +34,22 @@ class TooManyRequestsException(RecoverableHTTPError):
class QueryResults:
"""A container dataclass for the results from a given query."""
- data: Union[Dict, List[Dict]] = field(default_factory=list, init=False) # type: ignore[assignment]
- errors: List[str] = field(default_factory=list, init=False)
- links: Dict = field(default_factory=dict, init=False)
- included: List[Dict] = field(default_factory=list, init=False)
- meta: Dict = field(default_factory=dict, init=False)
+ data: Union[dict, list[dict]] = field(default_factory=list, init=False) # type: ignore[assignment]
+ errors: list[str] = field(default_factory=list, init=False)
+ links: dict = field(default_factory=dict, init=False)
+ included: list[dict] = field(default_factory=list, init=False)
+ meta: dict = field(default_factory=dict, init=False)
@property
- def included_index(self) -> Set[str]:
+ def included_index(self) -> set[str]:
if not getattr(self, "_included_index", None):
- self._included_index: Set[str] = set()
+ self._included_index: set[str] = set()
return self._included_index
- def dict(self):
+ def asdict(self):
return asdict(self)
- def update(self, page_results: Dict) -> None:
+ def update(self, page_results: dict) -> None:
"""Combine the results from one page with the existing results for a given query.
Parameters:
diff --git a/optimade/exceptions.py b/optimade/exceptions.py
index 2a75cfd5b..4274c1c8d 100644
--- a/optimade/exceptions.py
+++ b/optimade/exceptions.py
@@ -1,5 +1,5 @@
from abc import ABC
-from typing import Any, Dict, Optional, Tuple, Type
+from typing import Any, Optional
__all__ = (
"OptimadeHTTPException",
@@ -34,7 +34,7 @@ class OptimadeHTTPException(Exception, ABC):
status_code: int
title: str
detail: Optional[str] = None
- headers: Optional[Dict[str, Any]] = None
+ headers: Optional[dict[str, Any]] = None
def __init__(
self, detail: Optional[str] = None, headers: Optional[dict] = None
@@ -104,7 +104,7 @@ class NotImplementedResponse(OptimadeHTTPException):
"""A tuple of the possible errors that can be returned by an OPTIMADE API."""
-POSSIBLE_ERRORS: Tuple[Type[OptimadeHTTPException], ...] = (
+POSSIBLE_ERRORS: tuple[type[OptimadeHTTPException], ...] = (
BadRequest,
Forbidden,
NotFound,
diff --git a/optimade/filterparser/lark_parser.py b/optimade/filterparser/lark_parser.py
index 01d5044cd..67d159916 100644
--- a/optimade/filterparser/lark_parser.py
+++ b/optimade/filterparser/lark_parser.py
@@ -5,7 +5,7 @@
"""
from pathlib import Path
-from typing import Dict, Optional, Tuple
+from typing import Optional
from lark import Lark, Tree
@@ -20,7 +20,7 @@ class ParserError(Exception):
"""
-def get_versions() -> Dict[Tuple[int, int, int], Dict[str, Path]]:
+def get_versions() -> dict[tuple[int, int, int], dict[str, Path]]:
"""Find grammar files within this package's grammar directory,
returning a dictionary broken down by scraped grammar version
(major, minor, patch) and variant (a string tag).
@@ -29,10 +29,10 @@ def get_versions() -> Dict[Tuple[int, int, int], Dict[str, Path]]:
A mapping from version, variant to grammar file name.
"""
- dct: Dict[Tuple[int, int, int], Dict[str, Path]] = {}
+ dct: dict[tuple[int, int, int], dict[str, Path]] = {}
for filename in Path(__file__).parent.joinpath("../grammar").glob("*.lark"):
tags = filename.stem.lstrip("v").split(".")
- version: Tuple[int, int, int] = (int(tags[0]), int(tags[1]), int(tags[2]))
+ version: tuple[int, int, int] = (int(tags[0]), int(tags[1]), int(tags[2]))
variant: str = "default" if len(tags) == 3 else str(tags[-1])
if version not in dct:
dct[version] = {}
@@ -50,7 +50,7 @@ class LarkParser:
"""
def __init__(
- self, version: Optional[Tuple[int, int, int]] = None, variant: str = "default"
+ self, version: Optional[tuple[int, int, int]] = None, variant: str = "default"
):
"""For a given version and variant, try to load the corresponding grammar.
diff --git a/optimade/filtertransformers/__init__.py b/optimade/filtertransformers/__init__.py
index 2e9a99f72..07e031ffc 100644
--- a/optimade/filtertransformers/__init__.py
+++ b/optimade/filtertransformers/__init__.py
@@ -1,4 +1,4 @@
-""" This module implements filter transformer classes for different backends. These
+"""This module implements filter transformer classes for different backends. These
classes typically parse the filter with Lark and produce an appropriate query for the
given backend.
diff --git a/optimade/filtertransformers/base_transformer.py b/optimade/filtertransformers/base_transformer.py
index 2bb89086c..1936b37c5 100644
--- a/optimade/filtertransformers/base_transformer.py
+++ b/optimade/filtertransformers/base_transformer.py
@@ -7,7 +7,7 @@
import abc
import warnings
-from typing import Any, Dict, Optional, Type
+from typing import TYPE_CHECKING, Any, Optional
from lark import Transformer, Tree, v_args
@@ -15,6 +15,9 @@
from optimade.server.mappers import BaseResourceMapper
from optimade.warnings import UnknownProviderProperty
+if TYPE_CHECKING: # pragma: no cover
+ from typing import Union
+
__all__ = (
"BaseTransformer",
"Quantity",
@@ -79,8 +82,8 @@ class BaseTransformer(Transformer, abc.ABC):
"""
- mapper: Optional[Type[BaseResourceMapper]] = None
- operator_map: Dict[str, Optional[str]] = {
+ mapper: Optional[type[BaseResourceMapper]] = None
+ operator_map: dict[str, Optional[str]] = {
"<": None,
"<=": None,
">": None,
@@ -100,12 +103,10 @@ class BaseTransformer(Transformer, abc.ABC):
"!=": "!=",
}
- _quantity_type: Type[Quantity] = Quantity
+ _quantity_type: type[Quantity] = Quantity
_quantities = None
- def __init__(
- self, mapper: Optional[Type[BaseResourceMapper]] = None
- ): # pylint: disable=super-init-not-called
+ def __init__(self, mapper: Optional[type[BaseResourceMapper]] = None):
"""Initialise the transformer object, optionally loading in a
resource mapper for use when post-processing.
@@ -113,16 +114,17 @@ def __init__(
self.mapper = mapper
@property
- def backend_mapping(self) -> Dict[str, Quantity]:
+ def backend_mapping(self) -> dict[str, Quantity]:
"""A mapping between backend field names (aliases) and the corresponding
[`Quantity`][optimade.filtertransformers.base_transformer.Quantity] object.
"""
return {
- quantity.backend_field: quantity for _, quantity in self.quantities.items() # type: ignore[misc]
+ quantity.backend_field: quantity # type: ignore[misc]
+ for _, quantity in self.quantities.items()
}
@property
- def quantities(self) -> Dict[str, Quantity]:
+ def quantities(self) -> dict[str, Quantity]:
"""A mapping from the OPTIMADE field name to the corresponding
[`Quantity`][optimade.filtertransformers.base_transformer.Quantity] objects.
"""
@@ -132,10 +134,10 @@ def quantities(self) -> Dict[str, Quantity]:
return self._quantities
@quantities.setter
- def quantities(self, quantities: Dict[str, Quantity]) -> None:
+ def quantities(self, quantities: dict[str, Quantity]) -> None:
self._quantities = quantities
- def _build_quantities(self) -> Dict[str, Quantity]:
+ def _build_quantities(self) -> dict[str, Quantity]:
"""Creates a dictionary of field names mapped to
[`Quantity`][optimade.filtertransformers.base_transformer.Quantity] objects from the
fields registered by the mapper.
@@ -285,6 +287,9 @@ def signed_int(self, number):
@v_args(inline=True)
def number(self, number):
"""number: SIGNED_INT | SIGNED_FLOAT"""
+ if TYPE_CHECKING: # pragma: no cover
+ type_: Union[type[int], type[float]]
+
if number.type == "SIGNED_INT":
type_ = int
elif number.type == "SIGNED_FLOAT":
diff --git a/optimade/filtertransformers/elasticsearch.py b/optimade/filtertransformers/elasticsearch.py
index 195e77748..898c00d84 100644
--- a/optimade/filtertransformers/elasticsearch.py
+++ b/optimade/filtertransformers/elasticsearch.py
@@ -1,4 +1,4 @@
-from typing import Dict, Optional, Type, Union
+from typing import TYPE_CHECKING, Optional, Union
from elasticsearch_dsl import Field, Integer, Keyword, Q, Text
from lark import v_args
@@ -97,12 +97,12 @@ class ElasticTransformer(BaseTransformer):
">=": "gte",
}
- _quantity_type: Type[ElasticsearchQuantity] = ElasticsearchQuantity
+ _quantity_type: type[ElasticsearchQuantity] = ElasticsearchQuantity
def __init__(
self,
- mapper: Type[BaseResourceMapper],
- quantities: Optional[Dict[str, Quantity]] = None,
+ mapper: type[BaseResourceMapper],
+ quantities: Optional[dict[str, Quantity]] = None,
):
if quantities is not None:
self.quantities = quantities
@@ -143,7 +143,7 @@ def _field(
return quantity
if nested is not None:
- return "%s.%s" % (nested.backend_field, quantity.name) # type: ignore[union-attr]
+ return f"{nested.backend_field}.{quantity.name}" # type: ignore[union-attr]
return quantity.backend_field # type: ignore[union-attr, return-value]
@@ -188,7 +188,7 @@ def _query_op(
# != queries must also include an existence check
# Note that for MongoDB, `$exists` will include null-valued fields,
# where as in ES `exists` excludes them.
- # pylint: disable=invalid-unary-operand-type
+
return ~Q(query_type, **{field: value}) & Q("exists", field=field)
def _has_query_op(self, quantities, op, predicate_zip_list):
@@ -330,7 +330,7 @@ def property_first_comparison(self, quantity, query):
@v_args(inline=True)
def constant_first_comparison(self, value, op, quantity):
# constant_first_comparison: constant OPERATOR ( non_string_value | ...not_implemented_string )
- if not isinstance(quantity, Quantity):
+ if not isinstance(quantity, ElasticsearchQuantity):
raise TypeError("Only quantities can be compared to constant values.")
return self._query_op(quantity, self._reversed_operator_map[op], value)
@@ -372,7 +372,7 @@ def query(quantity):
if value == "KNOWN":
return query
elif value == "UNKNOWN":
- return ~query # pylint: disable=invalid-unary-operand-type
+ return ~query
raise NotImplementedError
return query
@@ -461,6 +461,9 @@ def signed_int(self, number):
@v_args(inline=True)
def number(self, number):
# number: SIGNED_INT | SIGNED_FLOAT
+ if TYPE_CHECKING: # pragma: no cover
+ type_: Union[type[int], type[float]]
+
if number.type == "SIGNED_INT":
type_ = int
elif number.type == "SIGNED_FLOAT":
diff --git a/optimade/filtertransformers/mongo.py b/optimade/filtertransformers/mongo.py
index 862e21f5e..84402f943 100755
--- a/optimade/filtertransformers/mongo.py
+++ b/optimade/filtertransformers/mongo.py
@@ -3,11 +3,10 @@
which takes the parsed filter and converts it to a valid pymongo/BSON query.
"""
-
import copy
import itertools
import warnings
-from typing import Any, Dict, List, Union
+from typing import Any, Union
from lark import Token, v_args
@@ -56,7 +55,7 @@ class MongoTransformer(BaseTransformer):
"$nin": "$in",
}
- def postprocess(self, query: Dict[str, Any]):
+ def postprocess(self, query: dict[str, Any]):
"""Used to post-process the nested dictionary of the parsed query."""
query = self._apply_relationship_filtering(query)
query = self._apply_length_operators(query)
@@ -123,7 +122,7 @@ def property_first_comparison(self, quantity, query):
is not None
):
size_query = {
- self.backend_mapping[
+ self.backend_mapping[ # type: ignore[union-attr]
quantity
].length_quantity.backend_field: query.pop("$size")
}
@@ -229,7 +228,7 @@ def property_zip_addon(self, arg):
# property_zip_addon: ":" property (":" property)*
raise NotImplementedError("Correlated list queries are not supported.")
- def _recursive_expression_phrase(self, arg: List) -> Dict[str, Any]:
+ def _recursive_expression_phrase(self, arg: list) -> dict[str, Any]:
"""Helper function for parsing `expression_phrase`. Recursively sorts out
the correct precedence for `$not`, `$and` and `$or`.
@@ -242,7 +241,7 @@ def _recursive_expression_phrase(self, arg: List) -> Dict[str, Any]:
"""
- def handle_not_and(arg: Dict[str, List]) -> Dict[str, List]:
+ def handle_not_and(arg: dict[str, list]) -> dict[str, list]:
"""Handle the case of `~(A & B) -> (~A | ~B)`.
We have to check for the special case in which the "and" was created
@@ -271,7 +270,7 @@ def handle_not_and(arg: Dict[str, List]) -> Dict[str, List]:
]
}
- def handle_not_or(arg: Dict[str, List]) -> Dict[str, List]:
+ def handle_not_or(arg: dict[str, list]) -> dict[str, list]:
"""Handle the case of ~(A | B) -> (~A & ~B).
!!! note
@@ -568,7 +567,7 @@ def replace_str_date_with_datetime(subdict, prop, expr):
)
-def recursive_postprocessing(filter_: Union[Dict, List], condition, replacement):
+def recursive_postprocessing(filter_: Union[dict, list], condition, replacement):
"""Recursively descend into the query, checking each dictionary
(contained in a list, or as an entry in another dictionary) for
the condition passed. If the condition is true, apply the
diff --git a/optimade/models/__init__.py b/optimade/models/__init__.py
index 018560412..e8e04800e 100644
--- a/optimade/models/__init__.py
+++ b/optimade/models/__init__.py
@@ -1,4 +1,3 @@
-# pylint: disable=undefined-variable
from .baseinfo import * # noqa: F403
from .entries import * # noqa: F403
from .index_metadb import * # noqa: F403
diff --git a/optimade/models/baseinfo.py b/optimade/models/baseinfo.py
index 3374b6a02..355425584 100644
--- a/optimade/models/baseinfo.py
+++ b/optimade/models/baseinfo.py
@@ -1,104 +1,128 @@
-# pylint: disable=no-self-argument,no-name-in-module
import re
-from typing import Dict, List, Optional
+from typing import Annotated, Literal, Optional
-from pydantic import AnyHttpUrl, BaseModel, Field, root_validator, validator
+from pydantic import AnyHttpUrl, BaseModel, field_validator, model_validator
from optimade.models.jsonapi import Resource
-from optimade.models.utils import SemanticVersion, StrictField
+from optimade.models.types import SemanticVersion
+from optimade.models.utils import StrictField
__all__ = ("AvailableApiVersion", "BaseInfoAttributes", "BaseInfoResource")
+VERSIONED_BASE_URL_PATTERN = r"^.+/v[0-1](\.[0-9]+)*/?$"
+
+
class AvailableApiVersion(BaseModel):
"""A JSON object containing information about an available API version"""
- url: AnyHttpUrl = StrictField(
- ...,
- description="A string specifying a versioned base URL that MUST adhere to the rules in section Base URL",
- pattern=r".+/v[0-1](\.[0-9]+)*/?$",
- )
+ url: Annotated[
+ AnyHttpUrl,
+ StrictField(
+ description="A string specifying a versioned base URL that MUST adhere to the rules in section Base URL",
+ json_schema_extra={
+ "pattern": VERSIONED_BASE_URL_PATTERN,
+ },
+ ),
+ ]
- version: SemanticVersion = StrictField(
- ...,
- description="""A string containing the full version number of the API served at that versioned base URL.
+ version: Annotated[
+ SemanticVersion,
+ StrictField(
+ description="""A string containing the full version number of the API served at that versioned base URL.
The version number string MUST NOT be prefixed by, e.g., 'v'.
Examples: `1.0.0`, `1.0.0-rc.2`.""",
- )
+ ),
+ ]
- @validator("url")
- def url_must_be_versioned_base_url(cls, v):
- """The URL must be a valid versioned Base URL"""
- if not re.match(r".+/v[0-1](\.[0-9]+)*/?$", v):
- raise ValueError(f"url MUST be a versioned base URL. It is: {v}")
- return v
+ @field_validator("url", mode="after")
+ @classmethod
+ def url_must_be_versioned_base_Url(cls, value: AnyHttpUrl) -> AnyHttpUrl:
+ """The URL must be a versioned base URL"""
+ if not re.match(VERSIONED_BASE_URL_PATTERN, str(value)):
+ raise ValueError(
+ f"URL {value} must be a versioned base URL (i.e., must match the "
+ f"pattern '{VERSIONED_BASE_URL_PATTERN}')"
+ )
+ return value
- @root_validator(pre=False, skip_on_failure=True)
- def crosscheck_url_and_version(cls, values):
+ @model_validator(mode="after")
+ def crosscheck_url_and_version(self) -> "AvailableApiVersion":
"""Check that URL version and API version are compatible."""
- url_version = (
- values["url"]
- .split("/")[-2 if values["url"].endswith("/") else -1]
+ url = (
+ str(self.url)
+ .split("/")[-2 if str(self.url).endswith("/") else -1]
.replace("v", "")
)
# as with version urls, we need to split any release tags or build metadata out of these URLs
url_version = tuple(
- int(val) for val in url_version.split("-")[0].split("+")[0].split(".")
+ int(val) for val in url.split("-")[0].split("+")[0].split(".")
)
api_version = tuple(
- int(val) for val in values["version"].split("-")[0].split("+")[0].split(".")
+ int(val) for val in str(self.version).split("-")[0].split("+")[0].split(".")
)
if any(a != b for a, b in zip(url_version, api_version)):
raise ValueError(
f"API version {api_version} is not compatible with url version {url_version}."
)
- return values
+ return self
class BaseInfoAttributes(BaseModel):
"""Attributes for Base URL Info endpoint"""
- api_version: SemanticVersion = StrictField(
- ...,
- description="""Presently used full version of the OPTIMADE API.
+ api_version: Annotated[
+ SemanticVersion,
+ StrictField(
+ description="""Presently used full version of the OPTIMADE API.
The version number string MUST NOT be prefixed by, e.g., "v".
Examples: `1.0.0`, `1.0.0-rc.2`.""",
- )
- available_api_versions: List[AvailableApiVersion] = StrictField(
- ...,
- description="A list of dictionaries of available API versions at other base URLs",
- )
- formats: List[str] = StrictField(
- default=["json"], description="List of available output formats."
- )
- available_endpoints: List[str] = StrictField(
- ...,
- description="List of available endpoints (i.e., the string to be appended to the versioned base URL).",
- )
- entry_types_by_format: Dict[str, List[str]] = StrictField(
- ..., description="Available entry endpoints as a function of output formats."
- )
- is_index: Optional[bool] = StrictField(
- default=False,
- description="If true, this is an index meta-database base URL (see section Index Meta-Database). "
- "If this member is not provided, the client MUST assume this is not an index meta-database base URL "
- "(i.e., the default is for `is_index` to be `false`).",
- )
-
- @validator("entry_types_by_format", check_fields=False)
- def formats_and_endpoints_must_be_valid(cls, v, values):
- for format_, endpoints in v.items():
- if format_ not in values["formats"]:
+ ),
+ ]
+ available_api_versions: Annotated[
+ list[AvailableApiVersion],
+ StrictField(
+ description="A list of dictionaries of available API versions at other base URLs",
+ ),
+ ]
+ formats: Annotated[
+ list[str], StrictField(description="List of available output formats.")
+ ] = ["json"]
+ available_endpoints: Annotated[
+ list[str],
+ StrictField(
+ description="List of available endpoints (i.e., the string to be appended to the versioned base URL).",
+ ),
+ ]
+ entry_types_by_format: Annotated[
+ dict[str, list[str]],
+ StrictField(
+ description="Available entry endpoints as a function of output formats."
+ ),
+ ]
+ is_index: Annotated[
+ Optional[bool],
+ StrictField(
+ description="If true, this is an index meta-database base URL (see section Index Meta-Database). "
+ "If this member is not provided, the client MUST assume this is not an index meta-database base URL "
+ "(i.e., the default is for `is_index` to be `false`).",
+ ),
+ ] = False
+
+ @model_validator(mode="after")
+ def formats_and_endpoints_must_be_valid(self) -> "BaseInfoAttributes":
+ for format_, endpoints in self.entry_types_by_format.items():
+ if format_ not in self.formats:
raise ValueError(f"'{format_}' must be listed in formats to be valid")
for endpoint in endpoints:
- if endpoint not in values["available_endpoints"]:
+ if endpoint not in self.available_endpoints:
raise ValueError(
f"'{endpoint}' must be listed in available_endpoints to be valid"
)
- return v
+ return self
class BaseInfoResource(Resource):
- id: str = Field("/", regex="^/$")
- type: str = Field("info", regex="^info$")
- attributes: BaseInfoAttributes = Field(...)
+ id: Literal["/"] = "/"
+ type: Literal["info"] = "info"
+ attributes: BaseInfoAttributes
diff --git a/optimade/models/entries.py b/optimade/models/entries.py
index 7850565ea..8728a7af0 100644
--- a/optimade/models/entries.py
+++ b/optimade/models/entries.py
@@ -1,11 +1,14 @@
-# pylint: disable=line-too-long,no-self-argument
from datetime import datetime
-from typing import Dict, List, Optional
+from typing import Annotated, Any, ClassVar, Literal, Optional, Union
-from pydantic import BaseModel, validator # pylint: disable=no-name-in-module
+from pydantic import BaseModel, field_validator
from optimade.models.jsonapi import Attributes, Relationships, Resource
-from optimade.models.optimade_json import DataType, Relationship
+from optimade.models.optimade_json import (
+ BaseRelationshipResource,
+ DataType,
+ Relationship,
+)
from optimade.models.utils import OptimadeField, StrictField, SupportLevel
__all__ = (
@@ -18,48 +21,57 @@
class TypedRelationship(Relationship):
- # This may be updated when moving to Python 3.8
- @validator("data")
- def check_rel_type(cls, data):
+ _req_type: ClassVar[str]
+
+ @field_validator("data", mode="after")
+ @classmethod
+ def check_rel_type(
+ cls, data: Union[BaseRelationshipResource, list[BaseRelationshipResource]]
+ ) -> list[BaseRelationshipResource]:
if not isinstance(data, list):
# All relationships at this point are empty-to-many relationships in JSON:API:
# https://jsonapi.org/format/1.0/#document-resource-object-linkage
raise ValueError("`data` key in a relationship must always store a list.")
- if hasattr(cls, "_req_type") and any(
- getattr(obj, "type", None) != cls._req_type for obj in data
- ):
+
+ if any(obj.type != cls._req_type for obj in data):
raise ValueError("Object stored in relationship data has wrong type")
+
return data
class ReferenceRelationship(TypedRelationship):
- _req_type = "references"
+ _req_type: ClassVar[Literal["references"]] = "references"
class StructureRelationship(TypedRelationship):
- _req_type = "structures"
+ _req_type: ClassVar[Literal["structures"]] = "structures"
class EntryRelationships(Relationships):
"""This model wraps the JSON API Relationships to include type-specific top level keys."""
- references: Optional[ReferenceRelationship] = StrictField(
- None,
- description="Object containing links to relationships with entries of the `references` type.",
- )
+ references: Annotated[
+ Optional[ReferenceRelationship],
+ StrictField(
+ description="Object containing links to relationships with entries of the `references` type.",
+ ),
+ ] = None
- structures: Optional[StructureRelationship] = StrictField(
- None,
- description="Object containing links to relationships with entries of the `structures` type.",
- )
+ structures: Annotated[
+ Optional[StructureRelationship],
+ StrictField(
+ description="Object containing links to relationships with entries of the `structures` type.",
+ ),
+ ] = None
class EntryResourceAttributes(Attributes):
"""Contains key-value pairs representing the entry's properties."""
- immutable_id: Optional[str] = OptimadeField(
- None,
- description="""The entry's immutable ID (e.g., an UUID). This is important for databases having preferred IDs that point to "the latest version" of a record, but still offer access to older variants. This ID maps to the version-specific record, in case it changes in the future.
+ immutable_id: Annotated[
+ Optional[str],
+ OptimadeField(
+ description="""The entry's immutable ID (e.g., an UUID). This is important for databases having preferred IDs that point to "the latest version" of a record, but still offer access to older variants. This ID maps to the version-specific record, in case it changes in the future.
- **Type**: string.
@@ -70,13 +82,15 @@ class EntryResourceAttributes(Attributes):
- **Examples**:
- `"8bd3e750-b477-41a0-9b11-3a799f21b44f"`
- `"fjeiwoj,54;@=%<>#32"` (Strings that are not URL-safe are allowed.)""",
- support=SupportLevel.OPTIONAL,
- queryable=SupportLevel.MUST,
- )
+ support=SupportLevel.OPTIONAL,
+ queryable=SupportLevel.MUST,
+ ),
+ ] = None
- last_modified: Optional[datetime] = OptimadeField(
- ...,
- description="""Date and time representing when the entry was last modified.
+ last_modified: Annotated[
+ Optional[datetime],
+ OptimadeField(
+ description="""Date and time representing when the entry was last modified.
- **Type**: timestamp.
@@ -87,12 +101,14 @@ class EntryResourceAttributes(Attributes):
- **Example**:
- As part of JSON response format: `"2007-04-05T14:30:20Z"` (i.e., encoded as an [RFC 3339 Internet Date/Time Format](https://tools.ietf.org/html/rfc3339#section-5.6) string.)""",
- support=SupportLevel.SHOULD,
- queryable=SupportLevel.MUST,
- )
-
- @validator("immutable_id", pre=True)
- def cast_immutable_id_to_str(cls, value):
+ support=SupportLevel.SHOULD,
+ queryable=SupportLevel.MUST,
+ ),
+ ]
+
+ @field_validator("immutable_id", mode="before")
+ @classmethod
+ def cast_immutable_id_to_str(cls, value: Any) -> str:
"""Convenience validator for casting `immutable_id` to a string."""
if value is not None and not isinstance(value, str):
value = str(value)
@@ -103,9 +119,10 @@ def cast_immutable_id_to_str(cls, value):
class EntryResource(Resource):
"""The base model for an entry resource."""
- id: str = OptimadeField(
- ...,
- description="""An entry's ID as defined in section Definition of Terms.
+ id: Annotated[
+ str,
+ OptimadeField(
+ description="""An entry's ID as defined in section Definition of Terms.
- **Type**: string.
@@ -120,12 +137,15 @@ class EntryResource(Resource):
- `"cod/2000000@1234567"`
- `"nomad/L1234567890"`
- `"42"`""",
- support=SupportLevel.MUST,
- queryable=SupportLevel.MUST,
- )
+ support=SupportLevel.MUST,
+ queryable=SupportLevel.MUST,
+ ),
+ ]
- type: str = OptimadeField(
- description="""The name of the type of an entry.
+ type: Annotated[
+ str,
+ OptimadeField(
+ description="""The name of the type of an entry.
- **Type**: string.
@@ -137,65 +157,84 @@ class EntryResource(Resource):
- The entry of type `` and ID `` MUST be returned in response to a request for `//` under the versioned base URL.
- **Example**: `"structures"`""",
- support=SupportLevel.MUST,
- queryable=SupportLevel.MUST,
- )
-
- attributes: EntryResourceAttributes = StrictField(
- ...,
- description="""A dictionary, containing key-value pairs representing the entry's properties, except for `type` and `id`.
+ support=SupportLevel.MUST,
+ queryable=SupportLevel.MUST,
+ ),
+ ]
+
+ attributes: Annotated[
+ EntryResourceAttributes,
+ StrictField(
+ description="""A dictionary, containing key-value pairs representing the entry's properties, except for `type` and `id`.
Database-provider-specific properties need to include the database-provider-specific prefix (see section on Database-Provider-Specific Namespace Prefixes).""",
- )
+ ),
+ ]
- relationships: Optional[EntryRelationships] = StrictField(
- None,
- description="""A dictionary containing references to other entries according to the description in section Relationships encoded as [JSON API Relationships](https://jsonapi.org/format/1.0/#document-resource-object-relationships).
+ relationships: Annotated[
+ Optional[EntryRelationships],
+ StrictField(
+ description="""A dictionary containing references to other entries according to the description in section Relationships encoded as [JSON API Relationships](https://jsonapi.org/format/1.0/#document-resource-object-relationships).
The OPTIONAL human-readable description of the relationship MAY be provided in the `description` field inside the `meta` dictionary of the JSON API resource identifier object.""",
- )
+ ),
+ ] = None
class EntryInfoProperty(BaseModel):
- description: str = StrictField(
- ..., description="A human-readable description of the entry property"
- )
-
- unit: Optional[str] = StrictField(
- None,
- description="""The physical unit of the entry property.
+ description: Annotated[
+ str,
+ StrictField(description="A human-readable description of the entry property"),
+ ]
+
+ unit: Annotated[
+ Optional[str],
+ StrictField(
+ description="""The physical unit of the entry property.
This MUST be a valid representation of units according to version 2.1 of [The Unified Code for Units of Measure](https://unitsofmeasure.org/ucum.html).
It is RECOMMENDED that non-standard (non-SI) units are described in the description for the property.""",
- )
+ ),
+ ] = None
- sortable: Optional[bool] = StrictField(
- None,
- description="""Defines whether the entry property can be used for sorting with the "sort" parameter.
+ sortable: Annotated[
+ Optional[bool],
+ StrictField(
+ description="""Defines whether the entry property can be used for sorting with the "sort" parameter.
If the entry listing endpoint supports sorting, this key MUST be present for sortable properties with value `true`.""",
- )
-
- type: Optional[DataType] = StrictField(
- None,
- title="Type",
- description="""The type of the property's value.
+ ),
+ ] = None
+
+ type: Annotated[
+ Optional[DataType],
+ StrictField(
+ title="Type",
+ description="""The type of the property's value.
This MUST be any of the types defined in the Data types section.
For the purpose of compatibility with future versions of this specification, a client MUST accept values that are not `string` values specifying any of the OPTIMADE Data types, but MUST then also disregard the `type` field.
Note, if the value is a nested type, only the outermost type should be reported.
E.g., for the entry resource `structures`, the `species` property is defined as a list of dictionaries, hence its `type` value would be `list`.""",
- )
+ ),
+ ] = None
class EntryInfoResource(BaseModel):
- formats: List[str] = StrictField(
- ..., description="List of output formats available for this type of entry."
- )
-
- description: str = StrictField(..., description="Description of the entry.")
-
- properties: Dict[str, EntryInfoProperty] = StrictField(
- ...,
- description="A dictionary describing queryable properties for this entry type, where each key is a property name.",
- )
-
- output_fields_by_format: Dict[str, List[str]] = StrictField(
- ...,
- description="Dictionary of available output fields for this entry type, where the keys are the values of the `formats` list and the values are the keys of the `properties` dictionary.",
- )
+ formats: Annotated[
+ list[str],
+ StrictField(
+ description="List of output formats available for this type of entry."
+ ),
+ ]
+
+ description: Annotated[str, StrictField(description="Description of the entry.")]
+
+ properties: Annotated[
+ dict[str, EntryInfoProperty],
+ StrictField(
+ description="A dictionary describing queryable properties for this entry type, where each key is a property name.",
+ ),
+ ]
+
+ output_fields_by_format: Annotated[
+ dict[str, list[str]],
+ StrictField(
+ description="Dictionary of available output fields for this entry type, where the keys are the values of the `formats` list and the values are the keys of the `properties` dictionary.",
+ ),
+ ]
diff --git a/optimade/models/index_metadb.py b/optimade/models/index_metadb.py
index 7c48d666c..83733f5fe 100644
--- a/optimade/models/index_metadb.py
+++ b/optimade/models/index_metadb.py
@@ -1,8 +1,6 @@
-# pylint: disable=no-self-argument
-from enum import Enum
-from typing import Dict, Union
+from typing import Annotated, Literal, Optional
-from pydantic import BaseModel, Field # pylint: disable=no-name-in-module
+from pydantic import BaseModel
from optimade.models.baseinfo import BaseInfoAttributes, BaseInfoResource
from optimade.models.jsonapi import BaseResource
@@ -16,46 +14,44 @@
)
-class DefaultRelationship(Enum):
- """Enumeration of key(s) for relationship dictionary in IndexInfoResource"""
-
- DEFAULT = "default"
-
-
class IndexInfoAttributes(BaseInfoAttributes):
"""Attributes for Base URL Info endpoint for an Index Meta-Database"""
- is_index: bool = StrictField(
- True,
- description="This must be `true` since this is an index meta-database (see section Index Meta-Database).",
- )
+ is_index: Annotated[
+ bool,
+ StrictField(
+ description="This must be `true` since this is an index meta-database (see section Index Meta-Database).",
+ ),
+ ] = True
class RelatedLinksResource(BaseResource):
"""A related Links resource object"""
- type: str = Field("links", regex="^links$")
+ type: Literal["links"] = "links"
class IndexRelationship(BaseModel):
"""Index Meta-Database relationship"""
- data: Union[None, RelatedLinksResource] = StrictField(
- ...,
- description="""[JSON API resource linkage](http://jsonapi.org/format/1.0/#document-links).
+ data: Annotated[
+ Optional[RelatedLinksResource],
+ StrictField(
+ description="""[JSON API resource linkage](http://jsonapi.org/format/1.0/#document-links).
It MUST be either `null` or contain a single Links identifier object with the fields `id` and `type`""",
- )
+ ),
+ ]
class IndexInfoResource(BaseInfoResource):
"""Index Meta-Database Base URL Info endpoint resource"""
- attributes: IndexInfoAttributes = Field(...)
- relationships: Union[
- None, Dict[DefaultRelationship, IndexRelationship]
- ] = StrictField( # type: ignore[assignment]
- ...,
- title="Relationships",
- description="""Reference to the Links identifier object under the `links` endpoint that the provider has chosen as their 'default' OPTIMADE API database.
+ attributes: IndexInfoAttributes
+ relationships: Annotated[ # type: ignore[assignment]
+ Optional[dict[Literal["default"], IndexRelationship]],
+ StrictField(
+ title="Relationships",
+ description="""Reference to the Links identifier object under the `links` endpoint that the provider has chosen as their 'default' OPTIMADE API database.
A client SHOULD present this database as the first choice when an end-user chooses this provider.""",
- )
+ ),
+ ]
diff --git a/optimade/models/jsonapi.py b/optimade/models/jsonapi.py
index 23e0db241..dc21314ba 100644
--- a/optimade/models/jsonapi.py
+++ b/optimade/models/jsonapi.py
@@ -1,13 +1,15 @@
"""This module should reproduce JSON API v1.0 https://jsonapi.org/format/1.0/"""
-# pylint: disable=no-self-argument
+
from datetime import datetime, timezone
-from typing import Any, Dict, List, Optional, Type, Union
+from typing import Annotated, Any, Optional, Union
-from pydantic import ( # pylint: disable=no-name-in-module
+from pydantic import (
AnyUrl,
BaseModel,
- parse_obj_as,
- root_validator,
+ BeforeValidator,
+ ConfigDict,
+ TypeAdapter,
+ model_validator,
)
from optimade.models.utils import StrictField
@@ -33,160 +35,193 @@
class Meta(BaseModel):
"""Non-standard meta-information that can not be represented as an attribute or relationship."""
- class Config:
- extra = "allow"
+ model_config = ConfigDict(extra="allow")
class Link(BaseModel):
"""A link **MUST** be represented as either: a string containing the link's URL or a link object."""
- href: AnyUrl = StrictField(..., description="a string containing the link’s URL.")
- meta: Optional[Meta] = StrictField(
- None,
- description="a meta object containing non-standard meta-information about the link.",
- )
+ href: Annotated[
+ AnyUrl, StrictField(description="a string containing the link's URL.")
+ ]
+ meta: Annotated[
+ Optional[Meta],
+ StrictField(
+ description="a meta object containing non-standard meta-information about the link.",
+ ),
+ ] = None
+
+
+JsonLinkType = Union[AnyUrl, Link]
class JsonApi(BaseModel):
"""An object describing the server's implementation"""
- version: str = StrictField(
- default="1.0", description="Version of the json API used"
- )
- meta: Optional[Meta] = StrictField(
- None, description="Non-standard meta information"
+ version: Annotated[str, StrictField(description="Version of the json API used")] = (
+ "1.0"
)
+ meta: Annotated[
+ Optional[Meta], StrictField(description="Non-standard meta information")
+ ] = None
class ToplevelLinks(BaseModel):
"""A set of Links objects, possibly including pagination"""
- self: Optional[Union[AnyUrl, Link]] = StrictField(
- None, description="A link to itself"
- )
- related: Optional[Union[AnyUrl, Link]] = StrictField(
- None, description="A related resource link"
- )
+ model_config = ConfigDict(extra="allow")
- # Pagination
- first: Optional[Union[AnyUrl, Link]] = StrictField(
- None, description="The first page of data"
- )
- last: Optional[Union[AnyUrl, Link]] = StrictField(
- None, description="The last page of data"
- )
- prev: Optional[Union[AnyUrl, Link]] = StrictField(
- None, description="The previous page of data"
- )
- next: Optional[Union[AnyUrl, Link]] = StrictField(
- None, description="The next page of data"
- )
+ self: Annotated[
+ Optional[JsonLinkType], StrictField(description="A link to itself")
+ ] = None
+ related: Annotated[
+ Optional[JsonLinkType], StrictField(description="A related resource link")
+ ] = None
- @root_validator(pre=False)
- def check_additional_keys_are_links(cls, values):
+ # Pagination
+ first: Annotated[
+ Optional[JsonLinkType], StrictField(description="The first page of data")
+ ] = None
+ last: Annotated[
+ Optional[JsonLinkType], StrictField(description="The last page of data")
+ ] = None
+ prev: Annotated[
+ Optional[JsonLinkType], StrictField(description="The previous page of data")
+ ] = None
+ next: Annotated[
+ Optional[JsonLinkType], StrictField(description="The next page of data")
+ ] = None
+
+ @model_validator(mode="after")
+ def check_additional_keys_are_links(self) -> "ToplevelLinks":
"""The `ToplevelLinks` class allows any additional keys, as long as
they are also Links or Urls themselves.
"""
- for key, value in values.items():
- if key not in cls.schema()["properties"]:
- values[key] = parse_obj_as(Optional[Union[AnyUrl, Link]], value)
-
- return values
+ for field, value in self:
+ if field not in self.model_fields:
+ setattr(
+ self,
+ field,
+ TypeAdapter(Optional[JsonLinkType]).validate_python(value),
+ )
- class Config:
- extra = "allow"
+ return self
class ErrorLinks(BaseModel):
"""A Links object specific to Error objects"""
- about: Optional[Union[AnyUrl, Link]] = StrictField(
- None,
- description="A link that leads to further details about this particular occurrence of the problem.",
- )
+ about: Annotated[
+ Optional[JsonLinkType],
+ StrictField(
+ description="A link that leads to further details about this particular occurrence of the problem.",
+ ),
+ ] = None
class ErrorSource(BaseModel):
"""an object containing references to the source of the error"""
- pointer: Optional[str] = StrictField(
- None,
- description="a JSON Pointer [RFC6901] to the associated entity in the request document "
- '[e.g. "/data" for a primary data object, or "/data/attributes/title" for a specific attribute].',
- )
- parameter: Optional[str] = StrictField(
- None,
- description="a string indicating which URI query parameter caused the error.",
- )
+ pointer: Annotated[
+ Optional[str],
+ StrictField(
+ description="a JSON Pointer [RFC6901] to the associated entity in the request document "
+ '[e.g. "/data" for a primary data object, or "/data/attributes/title" for a specific attribute].',
+ ),
+ ] = None
+ parameter: Annotated[
+ Optional[str],
+ StrictField(
+ description="a string indicating which URI query parameter caused the error.",
+ ),
+ ] = None
class Error(BaseModel):
"""An error response"""
- id: Optional[str] = StrictField(
- None,
- description="A unique identifier for this particular occurrence of the problem.",
- )
- links: Optional[ErrorLinks] = StrictField(
- None, description="A links object storing about"
- )
- status: Optional[str] = StrictField(
- None,
- description="the HTTP status code applicable to this problem, expressed as a string value.",
- )
- code: Optional[str] = StrictField(
- None,
- description="an application-specific error code, expressed as a string value.",
- )
- title: Optional[str] = StrictField(
- None,
- description="A short, human-readable summary of the problem. "
- "It **SHOULD NOT** change from occurrence to occurrence of the problem, except for purposes of localization.",
- )
- detail: Optional[str] = StrictField(
- None,
- description="A human-readable explanation specific to this occurrence of the problem.",
- )
- source: Optional[ErrorSource] = StrictField(
- None, description="An object containing references to the source of the error"
- )
- meta: Optional[Meta] = StrictField(
- None,
- description="a meta object containing non-standard meta-information about the error.",
- )
+ id: Annotated[
+ Optional[str],
+ StrictField(
+ description="A unique identifier for this particular occurrence of the problem.",
+ ),
+ ] = None
+ links: Annotated[
+ Optional[ErrorLinks], StrictField(description="A links object storing about")
+ ] = None
+ status: Annotated[
+ Optional[Annotated[str, BeforeValidator(str)]],
+ StrictField(
+ description="the HTTP status code applicable to this problem, expressed as a string value.",
+ ),
+ ] = None
+ code: Annotated[
+ Optional[str],
+ StrictField(
+ description="an application-specific error code, expressed as a string value.",
+ ),
+ ] = None
+ title: Annotated[
+ Optional[str],
+ StrictField(
+ description="A short, human-readable summary of the problem. "
+ "It **SHOULD NOT** change from occurrence to occurrence of the problem, except for purposes of localization.",
+ ),
+ ] = None
+ detail: Annotated[
+ Optional[str],
+ StrictField(
+ description="A human-readable explanation specific to this occurrence of the problem.",
+ ),
+ ] = None
+ source: Annotated[
+ Optional[ErrorSource],
+ StrictField(
+ description="An object containing references to the source of the error"
+ ),
+ ] = None
+ meta: Annotated[
+ Optional[Meta],
+ StrictField(
+ description="a meta object containing non-standard meta-information about the error.",
+ ),
+ ] = None
def __hash__(self):
- return hash(self.json())
+ return hash(self.model_dump_json())
-class BaseResource(BaseModel):
- """Minimum requirements to represent a Resource"""
+def resource_json_schema_extra(
+ schema: dict[str, Any], model: type["BaseResource"]
+) -> None:
+ """Ensure `id` and `type` are the first two entries in the list required properties.
+
+ Note:
+ This _requires_ that `id` and `type` are the _first_ model fields defined
+ for all sub-models of `BaseResource`.
+
+ """
+ if "id" not in schema.get("required", []):
+ schema["required"] = ["id"] + schema.get("required", [])
+ if "type" not in schema.get("required", []):
+ required = []
+ for field in schema.get("required", []):
+ required.append(field)
+ if field == "id":
+ # To make sure the property order match the listed properties,
+ # this ensures "type" is added immediately after "id".
+ required.append("type")
+ schema["required"] = required
- id: str = StrictField(..., description="Resource ID")
- type: str = StrictField(..., description="Resource type")
- class Config:
- @staticmethod
- def schema_extra(schema: Dict[str, Any], model: Type["BaseResource"]) -> None:
- """Ensure `id` and `type` are the first two entries in the list required properties.
+class BaseResource(BaseModel):
+ """Minimum requirements to represent a Resource"""
- Note:
- This _requires_ that `id` and `type` are the _first_ model fields defined
- for all sub-models of `BaseResource`.
+ model_config = ConfigDict(json_schema_extra=resource_json_schema_extra)
- """
- if "id" not in schema.get("required", []):
- schema["required"] = ["id"] + schema.get("required", [])
- if "type" not in schema.get("required", []):
- required = []
- for field in schema.get("required", []):
- required.append(field)
- if field == "id":
- # To make sure the property order match the listed properties,
- # this ensures "type" is added immediately after "id".
- required.append("type")
- schema["required"] = required
+ id: Annotated[str, StrictField(description="Resource ID")]
+ type: Annotated[str, StrictField(description="Resource type")]
class RelationshipLinks(BaseModel):
@@ -196,55 +231,58 @@ class RelationshipLinks(BaseModel):
"""
- self: Optional[Union[AnyUrl, Link]] = StrictField(
- None,
- description="""A link for the relationship itself (a 'relationship link').
+ self: Annotated[
+ Optional[JsonLinkType],
+ StrictField(
+ description="""A link for the relationship itself (a 'relationship link').
This link allows the client to directly manipulate the relationship.
When fetched successfully, this link returns the [linkage](https://jsonapi.org/format/1.0/#document-resource-object-linkage) for the related resources as its primary data.
(See [Fetching Relationships](https://jsonapi.org/format/1.0/#fetching-relationships).)""",
- )
- related: Optional[Union[AnyUrl, Link]] = StrictField(
- None,
- description="A [related resource link](https://jsonapi.org/format/1.0/#document-resource-object-related-resource-links).",
- )
-
- @root_validator(pre=True)
- def either_self_or_related_must_be_specified(cls, values):
- for value in values.values():
- if value is not None:
- break
- else:
+ ),
+ ] = None
+ related: Annotated[
+ Optional[JsonLinkType],
+ StrictField(
+ description="A [related resource link](https://jsonapi.org/format/1.0/#document-resource-object-related-resource-links).",
+ ),
+ ] = None
+
+ @model_validator(mode="after")
+ def either_self_or_related_must_be_specified(self) -> "RelationshipLinks":
+ if self.self is None and self.related is None:
raise ValueError(
"Either 'self' or 'related' MUST be specified for RelationshipLinks"
)
- return values
+ return self
class Relationship(BaseModel):
- """Representation references from the resource object in which it’s defined to other resource objects."""
-
- links: Optional[RelationshipLinks] = StrictField(
- None,
- description="a links object containing at least one of the following: self, related",
- )
- data: Optional[Union[BaseResource, List[BaseResource]]] = StrictField(
- None, description="Resource linkage"
- )
- meta: Optional[Meta] = StrictField(
- None,
- description="a meta object that contains non-standard meta-information about the relationship.",
- )
-
- @root_validator(pre=True)
- def at_least_one_relationship_key_must_be_set(cls, values):
- for value in values.values():
- if value is not None:
- break
- else:
+ """Representation references from the resource object in which it's defined to other resource objects."""
+
+ links: Annotated[
+ Optional[RelationshipLinks],
+ StrictField(
+ description="a links object containing at least one of the following: self, related",
+ ),
+ ] = None
+ data: Annotated[
+ Optional[Union[BaseResource, list[BaseResource]]],
+ StrictField(description="Resource linkage"),
+ ] = None
+ meta: Annotated[
+ Optional[Meta],
+ StrictField(
+ description="a meta object that contains non-standard meta-information about the relationship.",
+ ),
+ ] = None
+
+ @model_validator(mode="after")
+ def at_least_one_relationship_key_must_be_set(self) -> "Relationship":
+ if self.links is None and self.data is None and self.meta is None:
raise ValueError(
"Either 'links', 'data', or 'meta' MUST be specified for Relationship"
)
- return values
+ return self
class Relationships(BaseModel):
@@ -255,24 +293,26 @@ class Relationships(BaseModel):
id
"""
- @root_validator(pre=True)
- def check_illegal_relationships_fields(cls, values):
+ @model_validator(mode="after")
+ def check_illegal_relationships_fields(self) -> "Relationships":
illegal_fields = ("id", "type")
for field in illegal_fields:
- if field in values:
+ if hasattr(self, field):
raise ValueError(
f"{illegal_fields} MUST NOT be fields under Relationships"
)
- return values
+ return self
class ResourceLinks(BaseModel):
"""A Resource Links object"""
- self: Optional[Union[AnyUrl, Link]] = StrictField(
- None,
- description="A link that identifies the resource represented by the resource object.",
- )
+ self: Annotated[
+ Optional[JsonLinkType],
+ StrictField(
+ description="A link that identifies the resource represented by the resource object.",
+ ),
+ ] = None
class Attributes(BaseModel):
@@ -285,85 +325,102 @@ class Attributes(BaseModel):
type
"""
- class Config:
- extra = "allow"
+ model_config = ConfigDict(extra="allow")
- @root_validator(pre=True)
- def check_illegal_attributes_fields(cls, values):
+ @model_validator(mode="after")
+ def check_illegal_attributes_fields(self) -> "Attributes":
illegal_fields = ("relationships", "links", "id", "type")
for field in illegal_fields:
- if field in values:
+ if hasattr(self, field):
raise ValueError(
f"{illegal_fields} MUST NOT be fields under Attributes"
)
- return values
+ return self
class Resource(BaseResource):
"""Resource objects appear in a JSON API document to represent resources."""
- links: Optional[ResourceLinks] = StrictField(
- None, description="a links object containing links related to the resource."
- )
- meta: Optional[Meta] = StrictField(
- None,
- description="a meta object containing non-standard meta-information about a resource that can not be represented as an attribute or relationship.",
- )
- attributes: Optional[Attributes] = StrictField(
- None,
- description="an attributes object representing some of the resource’s data.",
- )
- relationships: Optional[Relationships] = StrictField(
- None,
- description="""[Relationships object](https://jsonapi.org/format/1.0/#document-resource-object-relationships)
+ links: Annotated[
+ Optional[ResourceLinks],
+ StrictField(
+ description="a links object containing links related to the resource."
+ ),
+ ] = None
+ meta: Annotated[
+ Optional[Meta],
+ StrictField(
+ description="a meta object containing non-standard meta-information about a resource that can not be represented as an attribute or relationship.",
+ ),
+ ] = None
+ attributes: Annotated[
+ Optional[Attributes],
+ StrictField(
+ description="an attributes object representing some of the resource’s data.",
+ ),
+ ] = None
+ relationships: Annotated[
+ Optional[Relationships],
+ StrictField(
+ description="""[Relationships object](https://jsonapi.org/format/1.0/#document-resource-object-relationships)
describing relationships between the resource and other JSON API resources.""",
- )
+ ),
+ ] = None
class Response(BaseModel):
- """A top-level response"""
-
- data: Optional[Union[None, Resource, List[Resource]]] = StrictField(
- None, description="Outputted Data", uniqueItems=True
- )
- meta: Optional[Meta] = StrictField(
- None,
- description="A meta object containing non-standard information related to the Success",
- )
- errors: Optional[List[Error]] = StrictField(
- None, description="A list of unique errors", uniqueItems=True
- )
- included: Optional[List[Resource]] = StrictField(
- None, description="A list of unique included resources", uniqueItems=True
- )
- links: Optional[ToplevelLinks] = StrictField(
- None, description="Links associated with the primary data or errors"
- )
- jsonapi: Optional[JsonApi] = StrictField(
- None, description="Information about the JSON API used"
- )
-
- @root_validator(pre=True)
- def either_data_meta_or_errors_must_be_set(cls, values):
+ """A top-level response."""
+
+ data: Annotated[
+ Optional[Union[None, Resource, list[Resource]]],
+ StrictField(description="Outputted Data", uniqueItems=True),
+ ] = None
+ meta: Annotated[
+ Optional[Meta],
+ StrictField(
+ description="A meta object containing non-standard information related to the Success",
+ ),
+ ] = None
+ errors: Annotated[
+ Optional[list[Error]],
+ StrictField(description="A list of unique errors", uniqueItems=True),
+ ] = None
+ included: Annotated[
+ Optional[list[Resource]],
+ StrictField(
+ description="A list of unique included resources", uniqueItems=True
+ ),
+ ] = None
+ links: Annotated[
+ Optional[ToplevelLinks],
+ StrictField(description="Links associated with the primary data or errors"),
+ ] = None
+ jsonapi: Annotated[
+ Optional[JsonApi],
+ StrictField(description="Information about the JSON API used"),
+ ] = None
+
+ @model_validator(mode="after")
+ def either_data_meta_or_errors_must_be_set(self) -> "Response":
required_fields = ("data", "meta", "errors")
- if not any(field in values for field in required_fields):
+ if not any(field in self.model_fields_set for field in required_fields):
raise ValueError(
f"At least one of {required_fields} MUST be specified in the top-level response"
)
- if "errors" in values and not values.get("errors"):
+ if "errors" in self.model_fields_set and not self.errors:
raise ValueError("Errors MUST NOT be an empty or 'null' value.")
- return values
-
- class Config:
- """The specification mandates that datetimes must be encoded following
- [RFC3339](https://tools.ietf.org/html/rfc3339), which does not support
- fractional seconds, thus they must be stripped in the response. This can
- cause issues when the underlying database contains fields that do include
- microseconds, as filters may return unexpected results.
- """
+ return self
- json_encoders = {
+ model_config = ConfigDict(
+ json_encoders={
datetime: lambda v: v.astimezone(timezone.utc).strftime(
"%Y-%m-%dT%H:%M:%SZ"
- ),
+ )
}
+ )
+ """The specification mandates that datetimes must be encoded following
+ [RFC3339](https://tools.ietf.org/html/rfc3339), which does not support
+ fractional seconds, thus they must be stripped in the response. This can
+ cause issues when the underlying database contains fields that do include
+ microseconds, as filters may return unexpected results.
+ """
diff --git a/optimade/models/links.py b/optimade/models/links.py
index 9cb316112..2605f4737 100644
--- a/optimade/models/links.py
+++ b/optimade/models/links.py
@@ -1,11 +1,10 @@
-# pylint: disable=no-self-argument
from enum import Enum
-from typing import Optional, Union
+from typing import Annotated, Literal, Optional
-from pydantic import AnyUrl, root_validator # pylint: disable=no-name-in-module
+from pydantic import model_validator
from optimade.models.entries import EntryResource
-from optimade.models.jsonapi import Attributes, Link
+from optimade.models.jsonapi import Attributes, JsonLinkType
from optimade.models.utils import StrictField
__all__ = (
@@ -35,35 +34,46 @@ class Aggregate(Enum):
class LinksResourceAttributes(Attributes):
"""Links endpoint resource object attributes"""
- name: str = StrictField(
- ...,
- description="Human-readable name for the OPTIMADE API implementation, e.g., for use in clients to show the name to the end-user.",
- )
- description: str = StrictField(
- ...,
- description="Human-readable description for the OPTIMADE API implementation, e.g., for use in clients to show a description to the end-user.",
- )
- base_url: Optional[Union[AnyUrl, Link]] = StrictField(
- ...,
- description="JSON API links object, pointing to the base URL for this implementation",
- )
-
- homepage: Optional[Union[AnyUrl, Link]] = StrictField(
- ...,
- description="JSON API links object, pointing to a homepage URL for this implementation",
- )
-
- link_type: LinkType = StrictField(
- ...,
- title="Link Type",
- description="""The type of the linked relation.
+ name: Annotated[
+ str,
+ StrictField(
+ description="Human-readable name for the OPTIMADE API implementation, e.g., for use in clients to show the name to the end-user.",
+ ),
+ ]
+ description: Annotated[
+ str,
+ StrictField(
+ description="Human-readable description for the OPTIMADE API implementation, e.g., for use in clients to show a description to the end-user.",
+ ),
+ ]
+ base_url: Annotated[
+ Optional[JsonLinkType],
+ StrictField(
+ description="JSON API links object, pointing to the base URL for this implementation",
+ ),
+ ]
+
+ homepage: Annotated[
+ Optional[JsonLinkType],
+ StrictField(
+ description="JSON API links object, pointing to a homepage URL for this implementation",
+ ),
+ ]
+
+ link_type: Annotated[
+ LinkType,
+ StrictField(
+ title="Link Type",
+ description="""The type of the linked relation.
MUST be one of these values: 'child', 'root', 'external', 'providers'.""",
- )
-
- aggregate: Optional[Aggregate] = StrictField(
- Aggregate.OK,
- title="Aggregate",
- description="""A string indicating whether a client that is following links to aggregate results from different OPTIMADE implementations should follow this link or not.
+ ),
+ ]
+
+ aggregate: Annotated[
+ Optional[Aggregate],
+ StrictField(
+ title="Aggregate",
+ description="""A string indicating whether a client that is following links to aggregate results from different OPTIMADE implementations should follow this link or not.
This flag SHOULD NOT be indicated for links where `link_type` is not `child`.
If not specified, clients MAY assume that the value is `ok`.
@@ -73,31 +83,38 @@ class LinksResourceAttributes(Attributes):
A client MAY follow the link anyway if it has reason to do so (e.g., if the client is looking for all test databases, it MAY follow the links marked with `aggregate`=`test`).
If specified, it MUST be one of the values listed in section Link Aggregate Options.""",
- )
+ ),
+ ] = Aggregate.OK
- no_aggregate_reason: Optional[str] = StrictField(
- None,
- description="""An OPTIONAL human-readable string indicating the reason for suggesting not to aggregate results following the link.
+ no_aggregate_reason: Annotated[
+ Optional[str],
+ StrictField(
+ description="""An OPTIONAL human-readable string indicating the reason for suggesting not to aggregate results following the link.
It SHOULD NOT be present if `aggregate`=`ok`.""",
- )
+ ),
+ ] = None
class LinksResource(EntryResource):
"""A Links endpoint resource object"""
- type: str = StrictField(
- "links",
- description="These objects are described in detail in the section Links Endpoint",
- regex="^links$",
- )
-
- attributes: LinksResourceAttributes = StrictField(
- ...,
- description="A dictionary containing key-value pairs representing the Links resource's properties.",
- )
-
- @root_validator(pre=True)
- def relationships_must_not_be_present(cls, values):
- if values.get("relationships", None) is not None:
+ type: Annotated[
+ Literal["links"],
+ StrictField(
+ description="These objects are described in detail in the section Links Endpoint",
+ pattern="^links$",
+ ),
+ ] = "links"
+
+ attributes: Annotated[
+ LinksResourceAttributes,
+ StrictField(
+ description="A dictionary containing key-value pairs representing the Links resource's properties.",
+ ),
+ ]
+
+ @model_validator(mode="after")
+ def relationships_must_not_be_present(self) -> "LinksResource":
+ if self.relationships or "relationships" in self.model_fields_set:
raise ValueError('"relationships" is not allowed for links resources')
- return values
+ return self
diff --git a/optimade/models/optimade_json.py b/optimade/models/optimade_json.py
index bad738057..348cb904c 100644
--- a/optimade/models/optimade_json.py
+++ b/optimade/models/optimade_json.py
@@ -1,13 +1,14 @@
"""Modified JSON API v1.0 for OPTIMADE API"""
-# pylint: disable=no-self-argument,no-name-in-module
+
from datetime import datetime
from enum import Enum
-from typing import Any, Dict, List, Optional, Type, Union
+from typing import Annotated, Any, Literal, Optional, Union
-from pydantic import AnyHttpUrl, AnyUrl, BaseModel, EmailStr, root_validator
+from pydantic import BaseModel, ConfigDict, EmailStr, model_validator
from optimade.models import jsonapi
-from optimade.models.utils import SemanticVersion, StrictField
+from optimade.models.types import SemanticVersion
+from optimade.models.utils import StrictField
__all__ = (
"DataType",
@@ -26,7 +27,7 @@
class DataType(Enum):
- """Optimade Data Types
+ """Optimade Data types
See the section "Data types" in the OPTIMADE API specification for more information.
"""
@@ -41,12 +42,14 @@ class DataType(Enum):
UNKNOWN = "unknown"
@classmethod
- def get_values(cls):
+ def get_values(cls) -> list[str]:
"""Get OPTIMADE data types (enum values) as a (sorted) list"""
- return sorted((_.value for _ in cls))
+ return sorted(_.value for _ in cls)
@classmethod
- def from_python_type(cls, python_type: Union[type, str, object]):
+ def from_python_type(
+ cls, python_type: Union[type, str, object]
+ ) -> Optional["DataType"]:
"""Get OPTIMADE data type from a Python type"""
mapping = {
"bool": cls.BOOLEAN,
@@ -68,7 +71,7 @@ def from_python_type(cls, python_type: Union[type, str, object]):
"dict_keys": cls.LIST,
"dict_values": cls.LIST,
"dict_items": cls.LIST,
- "NoneType": cls.UNKNOWN,
+ "Nonetype": cls.UNKNOWN,
"None": cls.UNKNOWN,
"datetime": cls.TIMESTAMP,
"date": cls.TIMESTAMP,
@@ -89,7 +92,7 @@ def from_python_type(cls, python_type: Union[type, str, object]):
return mapping.get(python_type, None)
@classmethod
- def from_json_type(cls, json_type: str):
+ def from_json_type(cls, json_type: str) -> Optional["DataType"]:
"""Get OPTIMADE data type from a named JSON type"""
mapping = {
"string": cls.STRING,
@@ -124,10 +127,32 @@ def from_json_type(cls, json_type: str):
class OptimadeError(jsonapi.Error):
"""detail MUST be present"""
- detail: str = StrictField(
- ...,
- description="A human-readable explanation specific to this occurrence of the problem.",
- )
+ detail: Annotated[
+ str,
+ StrictField(
+ description="A human-readable explanation specific to this occurrence of the problem.",
+ ),
+ ]
+
+
+def warnings_json_schema_extra(schema: dict[str, Any], model: type["Warnings"]) -> None:
+ """Update OpenAPI JSON schema model for `Warning`.
+
+ * Ensure `type` is in the list required properties and in the correct place.
+ * Remove `status` property.
+ This property is not allowed for `Warning`, nor is it a part of the OPTIMADE
+ definition of the `Warning` object.
+
+ Note:
+ Since `type` is the _last_ model field defined, it will simply be appended.
+
+ """
+ if "required" in schema:
+ if "type" not in schema["required"]:
+ schema["required"].append("type")
+ else:
+ schema["required"] = ["type"]
+ schema.get("properties", {}).pop("status", None)
class Warnings(OptimadeError):
@@ -142,109 +167,113 @@ class Warnings(OptimadeError):
"""
- type: str = StrictField(
- "warning",
- description='Warnings must be of type "warning"',
- regex="^warning$",
- )
-
- @root_validator(pre=True)
- def status_must_not_be_specified(cls, values):
- if values.get("status", None) is not None:
- raise ValueError("status MUST NOT be specified for warnings")
- return values
-
- class Config:
- @staticmethod
- def schema_extra(schema: Dict[str, Any], model: Type["Warnings"]) -> None:
- """Update OpenAPI JSON schema model for `Warning`.
+ model_config = ConfigDict(json_schema_extra=warnings_json_schema_extra)
- * Ensure `type` is in the list required properties and in the correct place.
- * Remove `status` property.
- This property is not allowed for `Warning`, nor is it a part of the OPTIMADE
- definition of the `Warning` object.
+ type: Annotated[
+ Literal["warning"],
+ StrictField(
+ description='Warnings must be of type "warning"',
+ pattern="^warning$",
+ ),
+ ] = "warning"
- Note:
- Since `type` is the _last_ model field defined, it will simply be appended.
-
- """
- if "required" in schema:
- if "type" not in schema["required"]:
- schema["required"].append("type")
- else:
- schema["required"] = ["type"]
- schema.get("properties", {}).pop("status", None)
+ @model_validator(mode="after")
+ def status_must_not_be_specified(self) -> "Warnings":
+ if self.status or "status" in self.model_fields_set:
+ raise ValueError("status MUST NOT be specified for warnings")
+ return self
class ResponseMetaQuery(BaseModel):
"""Information on the query that was requested."""
- representation: str = StrictField(
- ...,
- description="""A string with the part of the URL following the versioned or unversioned base URL that serves the API.
+ representation: Annotated[
+ str,
+ StrictField(
+ description="""A string with the part of the URL following the versioned or unversioned base URL that serves the API.
Query parameters that have not been used in processing the request MAY be omitted.
In particular, if no query parameters have been involved in processing the request, the query part of the URL MAY be excluded.
Example: `/structures?filter=nelements=2`""",
- )
+ ),
+ ]
class Provider(BaseModel):
"""Information on the database provider of the implementation."""
- name: str = StrictField(..., description="a short name for the database provider")
+ name: Annotated[
+ str, StrictField(description="a short name for the database provider")
+ ]
- description: str = StrictField(
- ..., description="a longer description of the database provider"
- )
+ description: Annotated[
+ str, StrictField(description="a longer description of the database provider")
+ ]
- prefix: str = StrictField(
- ...,
- regex=r"^[a-z]([a-z]|[0-9]|_)*$",
- description="database-provider-specific prefix as found in section Database-Provider-Specific Namespace Prefixes.",
- )
+ prefix: Annotated[
+ str,
+ StrictField(
+ pattern=r"^[a-z]([a-z]|[0-9]|_)*$",
+ description="database-provider-specific prefix as found in section Database-Provider-Specific Namespace Prefixes.",
+ ),
+ ]
- homepage: Optional[Union[AnyHttpUrl, jsonapi.Link]] = StrictField(
- None,
- description="a [JSON API links object](http://jsonapi.org/format/1.0#document-links) "
- "pointing to homepage of the database provider, either "
- "directly as a string, or as a link object.",
- )
+ homepage: Annotated[
+ Optional[jsonapi.JsonLinkType],
+ StrictField(
+ description="a [JSON API links object](http://jsonapi.org/format/1.0#document-links) "
+ "pointing to homepage of the database provider, either "
+ "directly as a string, or as a link object.",
+ ),
+ ] = None
class ImplementationMaintainer(BaseModel):
"""Details about the maintainer of the implementation"""
- email: EmailStr = StrictField(..., description="the maintainer's email address")
+ email: Annotated[
+ EmailStr, StrictField(description="the maintainer's email address")
+ ]
class Implementation(BaseModel):
"""Information on the server implementation"""
- name: Optional[str] = StrictField(None, description="name of the implementation")
-
- version: Optional[str] = StrictField(
- None, description="version string of the current implementation"
- )
-
- homepage: Optional[Union[AnyHttpUrl, jsonapi.Link]] = StrictField(
- None,
- description="A [JSON API links object](http://jsonapi.org/format/1.0/#document-links) pointing to the homepage of the implementation.",
- )
-
- source_url: Optional[Union[AnyUrl, jsonapi.Link]] = StrictField(
- None,
- description="A [JSON API links object](http://jsonapi.org/format/1.0/#document-links) pointing to the implementation source, either downloadable archive or version control system.",
- )
-
- maintainer: Optional[ImplementationMaintainer] = StrictField(
- None,
- description="A dictionary providing details about the maintainer of the implementation.",
- )
-
- issue_tracker: Optional[Union[AnyUrl, jsonapi.Link]] = StrictField(
- None,
- description="A [JSON API links object](http://jsonapi.org/format/1.0/#document-links) pointing to the implementation's issue tracker.",
- )
+ name: Annotated[
+ Optional[str], StrictField(description="name of the implementation")
+ ] = None
+
+ version: Annotated[
+ Optional[str],
+ StrictField(description="version string of the current implementation"),
+ ] = None
+
+ homepage: Annotated[
+ Optional[jsonapi.JsonLinkType],
+ StrictField(
+ description="A [JSON API links object](http://jsonapi.org/format/1.0/#document-links) pointing to the homepage of the implementation.",
+ ),
+ ] = None
+
+ source_url: Annotated[
+ Optional[jsonapi.JsonLinkType],
+ StrictField(
+ description="A [JSON API links object](http://jsonapi.org/format/1.0/#document-links) pointing to the implementation source, either downloadable archive or version control system.",
+ ),
+ ] = None
+
+ maintainer: Annotated[
+ Optional[ImplementationMaintainer],
+ StrictField(
+ description="A dictionary providing details about the maintainer of the implementation.",
+ ),
+ ] = None
+
+ issue_tracker: Annotated[
+ Optional[jsonapi.JsonLinkType],
+ StrictField(
+ description="A [JSON API links object](http://jsonapi.org/format/1.0/#document-links) pointing to the implementation's issue tracker.",
+ ),
+ ] = None
class ResponseMeta(jsonapi.Meta):
@@ -258,119 +287,146 @@ class ResponseMeta(jsonapi.Meta):
database-provider-specific prefix.
"""
- query: ResponseMetaQuery = StrictField(
- ..., description="Information on the Query that was requested"
- )
+ query: Annotated[
+ ResponseMetaQuery,
+ StrictField(description="Information on the Query that was requested"),
+ ]
- api_version: SemanticVersion = StrictField(
- ...,
- description="""Presently used full version of the OPTIMADE API.
+ api_version: Annotated[
+ SemanticVersion,
+ StrictField(
+ description="""Presently used full version of the OPTIMADE API.
The version number string MUST NOT be prefixed by, e.g., "v".
Examples: `1.0.0`, `1.0.0-rc.2`.""",
- )
+ ),
+ ]
- more_data_available: bool = StrictField(
- ...,
- description="`false` if the response contains all data for the request (e.g., a request issued to a single entry endpoint, or a `filter` query at the last page of a paginated response) and `true` if the response is incomplete in the sense that multiple objects match the request, and not all of them have been included in the response (e.g., a query with multiple pages that is not at the last page).",
- )
+ more_data_available: Annotated[
+ bool,
+ StrictField(
+ description="`false` if the response contains all data for the request (e.g., a request issued to a single entry endpoint, or a `filter` query at the last page of a paginated response) and `true` if the response is incomplete in the sense that multiple objects match the request, and not all of them have been included in the response (e.g., a query with multiple pages that is not at the last page).",
+ ),
+ ]
# start of "SHOULD" fields for meta response
- optimade_schema: Optional[Union[AnyHttpUrl, jsonapi.Link]] = StrictField(
- None,
- alias="schema",
- description="""A [JSON API links object](http://jsonapi.org/format/1.0/#document-links) that points to a schema for the response.
+ optimade_schema: Annotated[
+ Optional[jsonapi.JsonLinkType],
+ StrictField(
+ alias="schema",
+ description="""A [JSON API links object](http://jsonapi.org/format/1.0/#document-links) that points to a schema for the response.
If it is a string, or a dictionary containing no `meta` field, the provided URL MUST point at an [OpenAPI](https://swagger.io/specification/) schema.
It is possible that future versions of this specification allows for alternative schema types.
Hence, if the `meta` field of the JSON API links object is provided and contains a field `schema_type` that is not equal to the string `OpenAPI` the client MUST not handle failures to parse the schema or to validate the response against the schema as errors.""",
- )
-
- time_stamp: Optional[datetime] = StrictField(
- None,
- description="A timestamp containing the date and time at which the query was executed.",
- )
-
- data_returned: Optional[int] = StrictField(
- None,
- description="An integer containing the total number of data resource objects returned for the current `filter` query, independent of pagination.",
- ge=0,
- )
-
- provider: Optional[Provider] = StrictField(
- None, description="information on the database provider of the implementation."
- )
+ ),
+ ] = None
+
+ time_stamp: Annotated[
+ Optional[datetime],
+ StrictField(
+ description="A timestamp containing the date and time at which the query was executed.",
+ ),
+ ] = None
+
+ data_returned: Annotated[
+ Optional[int],
+ StrictField(
+ description="An integer containing the total number of data resource objects returned for the current `filter` query, independent of pagination.",
+ ge=0,
+ ),
+ ] = None
+
+ provider: Annotated[
+ Optional[Provider],
+ StrictField(
+ description="information on the database provider of the implementation."
+ ),
+ ] = None
# start of "MAY" fields for meta response
- data_available: Optional[int] = StrictField(
- None,
- description="An integer containing the total number of data resource objects available in the database for the endpoint.",
- )
-
- last_id: Optional[str] = StrictField(
- None, description="a string containing the last ID returned"
- )
-
- response_message: Optional[str] = StrictField(
- None, description="response string from the server"
- )
-
- implementation: Optional[Implementation] = StrictField(
- None, description="a dictionary describing the server implementation"
- )
-
- warnings: Optional[List[Warnings]] = StrictField(
- None,
- description="""A list of warning resource objects representing non-critical errors or warnings.
+ data_available: Annotated[
+ Optional[int],
+ StrictField(
+ description="An integer containing the total number of data resource objects available in the database for the endpoint.",
+ ),
+ ] = None
+
+ last_id: Annotated[
+ Optional[str],
+ StrictField(description="a string containing the last ID returned"),
+ ] = None
+
+ response_message: Annotated[
+ Optional[str], StrictField(description="response string from the server")
+ ] = None
+
+ implementation: Annotated[
+ Optional[Implementation],
+ StrictField(description="a dictionary describing the server implementation"),
+ ] = None
+
+ warnings: Annotated[
+ Optional[list[Warnings]],
+ StrictField(
+ description="""A list of warning resource objects representing non-critical errors or warnings.
A warning resource object is defined similarly to a [JSON API error object](http://jsonapi.org/format/1.0/#error-objects), but MUST also include the field `type`, which MUST have the value `"warning"`.
The field `detail` MUST be present and SHOULD contain a non-critical message, e.g., reporting unrecognized search attributes or deprecated features.
The field `status`, representing a HTTP response status code, MUST NOT be present for a warning resource object.
This is an exclusive field for error resource objects.""",
- uniqueItems=True,
- )
+ uniqueItems=True,
+ ),
+ ] = None
class Success(jsonapi.Response):
"""errors are not allowed"""
- meta: ResponseMeta = StrictField(
- ..., description="A meta object containing non-standard information"
- )
+ meta: Annotated[
+ ResponseMeta,
+ StrictField(description="A meta object containing non-standard information"),
+ ]
- @root_validator(pre=True)
- def either_data_meta_or_errors_must_be_set(cls, values):
+ @model_validator(mode="after")
+ def either_data_meta_or_errors_must_be_set(self) -> "Success":
"""Overwriting the existing validation function, since 'errors' MUST NOT be set."""
required_fields = ("data", "meta")
- if not any(field in values for field in required_fields):
+ if not any(field in self.model_fields_set for field in required_fields):
raise ValueError(
f"At least one of {required_fields} MUST be specified in the top-level response."
)
# errors MUST be skipped
- if "errors" in values:
+ if self.errors or "errors" in self.model_fields_set:
raise ValueError("'errors' MUST be skipped for a successful response.")
- return values
+ return self
class BaseRelationshipMeta(jsonapi.Meta):
"""Specific meta field for base relationship resource"""
- description: str = StrictField(
- ..., description="OPTIONAL human-readable description of the relationship."
- )
+ description: Annotated[
+ str,
+ StrictField(
+ description="OPTIONAL human-readable description of the relationship."
+ ),
+ ]
class BaseRelationshipResource(jsonapi.BaseResource):
"""Minimum requirements to represent a relationship resource"""
- meta: Optional[BaseRelationshipMeta] = StrictField(
- None,
- description="Relationship meta field. MUST contain 'description' if supplied.",
- )
+ meta: Annotated[
+ Optional[BaseRelationshipMeta],
+ StrictField(
+ description="Relationship meta field. MUST contain 'description' if supplied.",
+ ),
+ ] = None
class Relationship(jsonapi.Relationship):
"""Similar to normal JSON API relationship, but with addition of OPTIONAL meta field for a resource."""
- data: Optional[
- Union[BaseRelationshipResource, List[BaseRelationshipResource]]
- ] = StrictField(None, description="Resource linkage", uniqueItems=True)
+ data: Annotated[
+ Optional[Union[BaseRelationshipResource, list[BaseRelationshipResource]]],
+ StrictField(description="Resource linkage", uniqueItems=True),
+ ] = None
diff --git a/optimade/models/references.py b/optimade/models/references.py
index afdd2f48f..e238b5dd6 100644
--- a/optimade/models/references.py
+++ b/optimade/models/references.py
@@ -1,7 +1,6 @@
-# pylint: disable=line-too-long,no-self-argument
-from typing import List, Optional
+from typing import Annotated, Any, Literal, Optional
-from pydantic import AnyUrl, BaseModel, validator # pylint: disable=no-name-in-module
+from pydantic import AnyUrl, BaseModel, field_validator
from optimade.models.entries import EntryResource, EntryResourceAttributes
from optimade.models.utils import OptimadeField, SupportLevel
@@ -12,26 +11,32 @@
class Person(BaseModel):
"""A person, i.e., an author, editor or other."""
- name: str = OptimadeField(
- ...,
- description="""Full name of the person, REQUIRED.""",
- support=SupportLevel.MUST,
- queryable=SupportLevel.OPTIONAL,
- )
-
- firstname: Optional[str] = OptimadeField(
- None,
- description="""First name of the person.""",
- support=SupportLevel.OPTIONAL,
- queryable=SupportLevel.OPTIONAL,
- )
-
- lastname: Optional[str] = OptimadeField(
- None,
- description="""Last name of the person.""",
- support=SupportLevel.OPTIONAL,
- queryable=SupportLevel.OPTIONAL,
- )
+ name: Annotated[
+ str,
+ OptimadeField(
+ description="""Full name of the person, REQUIRED.""",
+ support=SupportLevel.MUST,
+ queryable=SupportLevel.OPTIONAL,
+ ),
+ ]
+
+ firstname: Annotated[
+ Optional[str],
+ OptimadeField(
+ description="""First name of the person.""",
+ support=SupportLevel.OPTIONAL,
+ queryable=SupportLevel.OPTIONAL,
+ ),
+ ] = None
+
+ lastname: Annotated[
+ Optional[str],
+ OptimadeField(
+ description="""Last name of the person.""",
+ support=SupportLevel.OPTIONAL,
+ queryable=SupportLevel.OPTIONAL,
+ ),
+ ] = None
class ReferenceResourceAttributes(EntryResourceAttributes):
@@ -42,187 +47,239 @@ class ReferenceResourceAttributes(EntryResourceAttributes):
"""
- authors: Optional[List[Person]] = OptimadeField(
- None,
- description="List of person objects containing the authors of the reference.",
- support=SupportLevel.OPTIONAL,
- queryable=SupportLevel.OPTIONAL,
- )
-
- editors: Optional[List[Person]] = OptimadeField(
- None,
- description="List of person objects containing the editors of the reference.",
- support=SupportLevel.OPTIONAL,
- queryable=SupportLevel.OPTIONAL,
- )
-
- doi: Optional[str] = OptimadeField(
- None,
- description="The digital object identifier of the reference.",
- support=SupportLevel.OPTIONAL,
- queryable=SupportLevel.OPTIONAL,
- )
-
- url: Optional[AnyUrl] = OptimadeField(
- None,
- description="The URL of the reference.",
- support=SupportLevel.OPTIONAL,
- queryable=SupportLevel.OPTIONAL,
- )
-
- address: Optional[str] = OptimadeField(
- None,
- description="Meaning of property matches the BiBTeX specification.",
- support=SupportLevel.OPTIONAL,
- queryable=SupportLevel.OPTIONAL,
- )
-
- annote: Optional[str] = OptimadeField(
- None,
- description="Meaning of property matches the BiBTeX specification.",
- support=SupportLevel.OPTIONAL,
- queryable=SupportLevel.OPTIONAL,
- )
-
- booktitle: Optional[str] = OptimadeField(
- None,
- description="Meaning of property matches the BiBTeX specification.",
- support=SupportLevel.OPTIONAL,
- queryable=SupportLevel.OPTIONAL,
- )
-
- chapter: Optional[str] = OptimadeField(
- None,
- description="Meaning of property matches the BiBTeX specification.",
- support=SupportLevel.OPTIONAL,
- queryable=SupportLevel.OPTIONAL,
- )
-
- crossref: Optional[str] = OptimadeField(
- None,
- description="Meaning of property matches the BiBTeX specification.",
- support=SupportLevel.OPTIONAL,
- queryable=SupportLevel.OPTIONAL,
- )
-
- edition: Optional[str] = OptimadeField(
- None,
- description="Meaning of property matches the BiBTeX specification.",
- support=SupportLevel.OPTIONAL,
- queryable=SupportLevel.OPTIONAL,
- )
-
- howpublished: Optional[str] = OptimadeField(
- None,
- description="Meaning of property matches the BiBTeX specification.",
- support=SupportLevel.OPTIONAL,
- queryable=SupportLevel.OPTIONAL,
- )
-
- institution: Optional[str] = OptimadeField(
- None,
- description="Meaning of property matches the BiBTeX specification.",
- support=SupportLevel.OPTIONAL,
- queryable=SupportLevel.OPTIONAL,
- )
-
- journal: Optional[str] = OptimadeField(
- None,
- description="Meaning of property matches the BiBTeX specification.",
- support=SupportLevel.OPTIONAL,
- queryable=SupportLevel.OPTIONAL,
- )
-
- key: Optional[str] = OptimadeField(
- None,
- description="Meaning of property matches the BiBTeX specification.",
- support=SupportLevel.OPTIONAL,
- queryable=SupportLevel.OPTIONAL,
- )
-
- month: Optional[str] = OptimadeField(
- None,
- description="Meaning of property matches the BiBTeX specification.",
- support=SupportLevel.OPTIONAL,
- queryable=SupportLevel.OPTIONAL,
- )
-
- note: Optional[str] = OptimadeField(
- None,
- description="Meaning of property matches the BiBTeX specification.",
- support=SupportLevel.OPTIONAL,
- queryable=SupportLevel.OPTIONAL,
- )
-
- number: Optional[str] = OptimadeField(
- None,
- description="Meaning of property matches the BiBTeX specification.",
- support=SupportLevel.OPTIONAL,
- queryable=SupportLevel.OPTIONAL,
- )
-
- organization: Optional[str] = OptimadeField(
- None,
- description="Meaning of property matches the BiBTeX specification.",
- support=SupportLevel.OPTIONAL,
- queryable=SupportLevel.OPTIONAL,
- )
-
- pages: Optional[str] = OptimadeField(
- None,
- description="Meaning of property matches the BiBTeX specification.",
- support=SupportLevel.OPTIONAL,
- queryable=SupportLevel.OPTIONAL,
- )
-
- publisher: Optional[str] = OptimadeField(
- None,
- description="Meaning of property matches the BiBTeX specification.",
- support=SupportLevel.OPTIONAL,
- queryable=SupportLevel.OPTIONAL,
- )
-
- school: Optional[str] = OptimadeField(
- None,
- description="Meaning of property matches the BiBTeX specification.",
- support=SupportLevel.OPTIONAL,
- queryable=SupportLevel.OPTIONAL,
- )
-
- series: Optional[str] = OptimadeField(
- None,
- description="Meaning of property matches the BiBTeX specification.",
- support=SupportLevel.OPTIONAL,
- queryable=SupportLevel.OPTIONAL,
- )
-
- title: Optional[str] = OptimadeField(
- None,
- description="Meaning of property matches the BiBTeX specification.",
- support=SupportLevel.OPTIONAL,
- queryable=SupportLevel.OPTIONAL,
- )
-
- bib_type: Optional[str] = OptimadeField(
- None,
- description="Type of the reference, corresponding to the **type** property in the BiBTeX specification.",
- support=SupportLevel.OPTIONAL,
- queryable=SupportLevel.OPTIONAL,
- )
-
- volume: Optional[str] = OptimadeField(
- None,
- description="Meaning of property matches the BiBTeX specification.",
- support=SupportLevel.OPTIONAL,
- queryable=SupportLevel.OPTIONAL,
- )
-
- year: Optional[str] = OptimadeField(
- None,
- description="Meaning of property matches the BiBTeX specification.",
- support=SupportLevel.OPTIONAL,
- queryable=SupportLevel.OPTIONAL,
- )
+ authors: Annotated[
+ Optional[list[Person]],
+ OptimadeField(
+ description="List of person objects containing the authors of the reference.",
+ support=SupportLevel.OPTIONAL,
+ queryable=SupportLevel.OPTIONAL,
+ ),
+ ] = None
+
+ editors: Annotated[
+ Optional[list[Person]],
+ OptimadeField(
+ description="List of person objects containing the editors of the reference.",
+ support=SupportLevel.OPTIONAL,
+ queryable=SupportLevel.OPTIONAL,
+ ),
+ ] = None
+
+ doi: Annotated[
+ Optional[str],
+ OptimadeField(
+ description="The digital object identifier of the reference.",
+ support=SupportLevel.OPTIONAL,
+ queryable=SupportLevel.OPTIONAL,
+ ),
+ ] = None
+
+ url: Annotated[
+ Optional[AnyUrl],
+ OptimadeField(
+ description="The URL of the reference.",
+ support=SupportLevel.OPTIONAL,
+ queryable=SupportLevel.OPTIONAL,
+ ),
+ ] = None
+
+ address: Annotated[
+ Optional[str],
+ OptimadeField(
+ description="Meaning of property matches the BiBTeX specification.",
+ support=SupportLevel.OPTIONAL,
+ queryable=SupportLevel.OPTIONAL,
+ ),
+ ] = None
+
+ annote: Annotated[
+ Optional[str],
+ OptimadeField(
+ description="Meaning of property matches the BiBTeX specification.",
+ support=SupportLevel.OPTIONAL,
+ queryable=SupportLevel.OPTIONAL,
+ ),
+ ] = None
+
+ booktitle: Annotated[
+ Optional[str],
+ OptimadeField(
+ description="Meaning of property matches the BiBTeX specification.",
+ support=SupportLevel.OPTIONAL,
+ queryable=SupportLevel.OPTIONAL,
+ ),
+ ] = None
+
+ chapter: Annotated[
+ Optional[str],
+ OptimadeField(
+ description="Meaning of property matches the BiBTeX specification.",
+ support=SupportLevel.OPTIONAL,
+ queryable=SupportLevel.OPTIONAL,
+ ),
+ ] = None
+
+ crossref: Annotated[
+ Optional[str],
+ OptimadeField(
+ description="Meaning of property matches the BiBTeX specification.",
+ support=SupportLevel.OPTIONAL,
+ queryable=SupportLevel.OPTIONAL,
+ ),
+ ] = None
+
+ edition: Annotated[
+ Optional[str],
+ OptimadeField(
+ description="Meaning of property matches the BiBTeX specification.",
+ support=SupportLevel.OPTIONAL,
+ queryable=SupportLevel.OPTIONAL,
+ ),
+ ] = None
+
+ howpublished: Annotated[
+ Optional[str],
+ OptimadeField(
+ description="Meaning of property matches the BiBTeX specification.",
+ support=SupportLevel.OPTIONAL,
+ queryable=SupportLevel.OPTIONAL,
+ ),
+ ] = None
+
+ institution: Annotated[
+ Optional[str],
+ OptimadeField(
+ description="Meaning of property matches the BiBTeX specification.",
+ support=SupportLevel.OPTIONAL,
+ queryable=SupportLevel.OPTIONAL,
+ ),
+ ] = None
+
+ journal: Annotated[
+ Optional[str],
+ OptimadeField(
+ description="Meaning of property matches the BiBTeX specification.",
+ support=SupportLevel.OPTIONAL,
+ queryable=SupportLevel.OPTIONAL,
+ ),
+ ] = None
+
+ key: Annotated[
+ Optional[str],
+ OptimadeField(
+ description="Meaning of property matches the BiBTeX specification.",
+ support=SupportLevel.OPTIONAL,
+ queryable=SupportLevel.OPTIONAL,
+ ),
+ ] = None
+
+ month: Annotated[
+ Optional[str],
+ OptimadeField(
+ description="Meaning of property matches the BiBTeX specification.",
+ support=SupportLevel.OPTIONAL,
+ queryable=SupportLevel.OPTIONAL,
+ ),
+ ] = None
+
+ note: Annotated[
+ Optional[str],
+ OptimadeField(
+ description="Meaning of property matches the BiBTeX specification.",
+ support=SupportLevel.OPTIONAL,
+ queryable=SupportLevel.OPTIONAL,
+ ),
+ ] = None
+
+ number: Annotated[
+ Optional[str],
+ OptimadeField(
+ description="Meaning of property matches the BiBTeX specification.",
+ support=SupportLevel.OPTIONAL,
+ queryable=SupportLevel.OPTIONAL,
+ ),
+ ] = None
+
+ organization: Annotated[
+ Optional[str],
+ OptimadeField(
+ description="Meaning of property matches the BiBTeX specification.",
+ support=SupportLevel.OPTIONAL,
+ queryable=SupportLevel.OPTIONAL,
+ ),
+ ] = None
+
+ pages: Annotated[
+ Optional[str],
+ OptimadeField(
+ description="Meaning of property matches the BiBTeX specification.",
+ support=SupportLevel.OPTIONAL,
+ queryable=SupportLevel.OPTIONAL,
+ ),
+ ] = None
+
+ publisher: Annotated[
+ Optional[str],
+ OptimadeField(
+ description="Meaning of property matches the BiBTeX specification.",
+ support=SupportLevel.OPTIONAL,
+ queryable=SupportLevel.OPTIONAL,
+ ),
+ ] = None
+
+ school: Annotated[
+ Optional[str],
+ OptimadeField(
+ description="Meaning of property matches the BiBTeX specification.",
+ support=SupportLevel.OPTIONAL,
+ queryable=SupportLevel.OPTIONAL,
+ ),
+ ] = None
+
+ series: Annotated[
+ Optional[str],
+ OptimadeField(
+ description="Meaning of property matches the BiBTeX specification.",
+ support=SupportLevel.OPTIONAL,
+ queryable=SupportLevel.OPTIONAL,
+ ),
+ ] = None
+
+ title: Annotated[
+ Optional[str],
+ OptimadeField(
+ description="Meaning of property matches the BiBTeX specification.",
+ support=SupportLevel.OPTIONAL,
+ queryable=SupportLevel.OPTIONAL,
+ ),
+ ] = None
+
+ bib_type: Annotated[
+ Optional[str],
+ OptimadeField(
+ description="Type of the reference, corresponding to the **type** property in the BiBTeX specification.",
+ support=SupportLevel.OPTIONAL,
+ queryable=SupportLevel.OPTIONAL,
+ ),
+ ] = None
+
+ volume: Annotated[
+ Optional[str],
+ OptimadeField(
+ description="Meaning of property matches the BiBTeX specification.",
+ support=SupportLevel.OPTIONAL,
+ queryable=SupportLevel.OPTIONAL,
+ ),
+ ] = None
+
+ year: Annotated[
+ Optional[str],
+ OptimadeField(
+ description="Meaning of property matches the BiBTeX specification.",
+ support=SupportLevel.OPTIONAL,
+ queryable=SupportLevel.OPTIONAL,
+ ),
+ ] = None
class ReferenceResource(EntryResource):
@@ -244,9 +301,10 @@ class ReferenceResource(EntryResource):
"""
- type: str = OptimadeField(
- "references",
- description="""The name of the type of an entry.
+ type: Annotated[
+ Literal["references"],
+ OptimadeField(
+ description="""The name of the type of an entry.
- **Type**: string.
- **Requirements/Conventions**:
- **Support**: MUST be supported by all implementations, MUST NOT be `null`.
@@ -255,14 +313,21 @@ class ReferenceResource(EntryResource):
- MUST be an existing entry type.
- The entry of type and ID MUST be returned in response to a request for `//` under the versioned base URL.
- **Example**: `"structures"`""",
- regex="^references$",
- support=SupportLevel.MUST,
- queryable=SupportLevel.MUST,
- )
+ pattern="^references$",
+ support=SupportLevel.MUST,
+ queryable=SupportLevel.MUST,
+ ),
+ ] = "references"
attributes: ReferenceResourceAttributes
- @validator("attributes")
- def validate_attributes(cls, v):
- if not any(prop[1] is not None for prop in v):
+ @field_validator("attributes", mode="before")
+ @classmethod
+ def validate_attributes(cls, value: Any) -> dict[str, Any]:
+ if not isinstance(value, dict):
+ if isinstance(value, BaseModel):
+ value = value.model_dump()
+ else:
+ raise TypeError("attributes field must be a mapping")
+ if not any(prop[1] is not None for prop in value):
raise ValueError("reference object must have at least one field defined")
- return v
+ return value
diff --git a/optimade/models/responses.py b/optimade/models/responses.py
index 01845dc82..c7cf72855 100644
--- a/optimade/models/responses.py
+++ b/optimade/models/responses.py
@@ -1,7 +1,6 @@
-# pylint: disable=no-self-argument
-from typing import Any, Dict, List, Optional, Union
+from typing import Annotated, Any, Optional, Union
-from pydantic import Field, root_validator
+from pydantic import model_validator
from optimade.models.baseinfo import BaseInfoResource
from optimade.models.entries import EntryInfoResource, EntryResource
@@ -31,87 +30,129 @@
class ErrorResponse(Response):
"""errors MUST be present and data MUST be skipped"""
- meta: ResponseMeta = StrictField(
- ..., description="A meta object containing non-standard information."
- )
- errors: List[OptimadeError] = StrictField(
- ...,
- description="A list of OPTIMADE-specific JSON API error objects, where the field detail MUST be present.",
- uniqueItems=True,
- )
-
- @root_validator(pre=True)
- def data_must_be_skipped(cls, values):
- if "data" in values:
+ meta: Annotated[
+ ResponseMeta,
+ StrictField(description="A meta object containing non-standard information."),
+ ]
+ errors: Annotated[
+ list[OptimadeError],
+ StrictField(
+ description="A list of OPTIMADE-specific JSON API error objects, where the field detail MUST be present.",
+ uniqueItems=True,
+ ),
+ ]
+
+ @model_validator(mode="after")
+ def data_must_be_skipped(self) -> "ErrorResponse":
+ if self.data or "data" in self.model_fields_set:
raise ValueError("data MUST be skipped for failures reporting errors.")
- return values
+ return self
class IndexInfoResponse(Success):
- data: IndexInfoResource = StrictField(
- ..., description="Index meta-database /info data."
- )
+ data: Annotated[
+ IndexInfoResource, StrictField(description="Index meta-database /info data.")
+ ]
class EntryInfoResponse(Success):
- data: EntryInfoResource = StrictField(
- ..., description="OPTIMADE information for an entry endpoint."
- )
+ data: Annotated[
+ EntryInfoResource,
+ StrictField(description="OPTIMADE information for an entry endpoint."),
+ ]
class InfoResponse(Success):
- data: BaseInfoResource = StrictField(
- ..., description="The implementations /info data."
- )
+ data: Annotated[
+ BaseInfoResource, StrictField(description="The implementations /info data.")
+ ]
class EntryResponseOne(Success):
- data: Union[EntryResource, Dict[str, Any], None] = Field(...) # type: ignore[assignment]
- included: Optional[Union[List[EntryResource], List[Dict[str, Any]]]] = Field( # type: ignore[assignment]
- None, uniqueItems=True
- )
+ data: Annotated[
+ Optional[Union[EntryResource, dict[str, Any]]],
+ StrictField(
+ description="The single entry resource returned by this query.",
+ union_mode="left_to_right",
+ ),
+ ] = None # type: ignore[assignment]
+ included: Annotated[
+ Optional[Union[list[EntryResource], list[dict[str, Any]]]],
+ StrictField(
+ description="A list of unique included OPTIMADE entry resources.",
+ uniqueItems=True,
+ union_mode="left_to_right",
+ ),
+ ] = None # type: ignore[assignment]
class EntryResponseMany(Success):
- data: Union[List[EntryResource], List[Dict[str, Any]]] = Field( # type: ignore[assignment]
- ..., uniqueItems=True
- )
- included: Optional[Union[List[EntryResource], List[Dict[str, Any]]]] = Field( # type: ignore[assignment]
- None, uniqueItems=True
- )
+ data: Annotated[ # type: ignore[assignment]
+ Union[list[EntryResource], list[dict[str, Any]]],
+ StrictField(
+ description="List of unique OPTIMADE entry resource objects.",
+ uniqueItems=True,
+ union_mode="left_to_right",
+ ),
+ ]
+ included: Annotated[
+ Optional[Union[list[EntryResource], list[dict[str, Any]]]],
+ StrictField(
+ description="A list of unique included OPTIMADE entry resources.",
+ uniqueItems=True,
+ union_mode="left_to_right",
+ ),
+ ] = None # type: ignore[assignment]
class LinksResponse(EntryResponseMany):
- data: Union[List[LinksResource], List[Dict[str, Any]]] = StrictField(
- ...,
- description="List of unique OPTIMADE links resource objects.",
- uniqueItems=True,
- )
+ data: Annotated[
+ Union[list[LinksResource], list[dict[str, Any]]],
+ StrictField(
+ description="List of unique OPTIMADE links resource objects.",
+ uniqueItems=True,
+ union_mode="left_to_right",
+ ),
+ ]
class StructureResponseOne(EntryResponseOne):
- data: Union[StructureResource, Dict[str, Any], None] = StrictField(
- ..., description="A single structures entry resource."
- )
+ data: Annotated[
+ Optional[Union[StructureResource, dict[str, Any]]],
+ StrictField(
+ description="A single structures entry resource.",
+ union_mode="left_to_right",
+ ),
+ ]
class StructureResponseMany(EntryResponseMany):
- data: Union[List[StructureResource], List[Dict[str, Any]]] = StrictField(
- ...,
- description="List of unique OPTIMADE structures entry resource objects.",
- uniqueItems=True,
- )
+ data: Annotated[
+ Union[list[StructureResource], list[dict[str, Any]]],
+ StrictField(
+ description="List of unique OPTIMADE structures entry resource objects.",
+ uniqueItems=True,
+ union_mode="left_to_right",
+ ),
+ ]
class ReferenceResponseOne(EntryResponseOne):
- data: Union[ReferenceResource, Dict[str, Any], None] = StrictField(
- ..., description="A single references entry resource."
- )
+ data: Annotated[
+ Optional[Union[ReferenceResource, dict[str, Any]]],
+ StrictField(
+ description="A single references entry resource.",
+ union_mode="left_to_right",
+ ),
+ ]
class ReferenceResponseMany(EntryResponseMany):
- data: Union[List[ReferenceResource], List[Dict[str, Any]]] = StrictField(
- ...,
- description="List of unique OPTIMADE references entry resource objects.",
- uniqueItems=True,
- )
+ data: Annotated[
+ Union[list[ReferenceResource], list[dict[str, Any]]],
+ StrictField(
+ description="List of unique OPTIMADE references entry resource objects.",
+ uniqueItems=True,
+ union_mode="left_to_right",
+ ),
+ ]
diff --git a/optimade/models/structures.py b/optimade/models/structures.py
index aa89afe69..3567c711d 100644
--- a/optimade/models/structures.py
+++ b/optimade/models/structures.py
@@ -1,17 +1,25 @@
-# pylint: disable=no-self-argument,line-too-long,no-name-in-module
import re
import warnings
from enum import Enum, IntEnum
-from typing import List, Optional, Union
-
-from pydantic import BaseModel, conlist, root_validator, validator
+from typing import TYPE_CHECKING, Annotated, Literal, Optional, Union
+
+from pydantic import (
+ BaseModel,
+ BeforeValidator,
+ Field,
+ conint,
+ constr,
+ field_validator,
+ model_validator,
+)
from optimade.models.entries import EntryResource, EntryResourceAttributes
+from optimade.models.types import ChemicalSymbol
from optimade.models.utils import (
ANONYMOUS_ELEMENTS,
CHEMICAL_FORMULA_REGEXP,
CHEMICAL_SYMBOLS,
- EXTRA_SYMBOLS,
+ SPACE_GROUP_SYMMETRY_OPERATION_REGEX,
OptimadeField,
StrictField,
SupportLevel,
@@ -19,8 +27,8 @@
)
from optimade.warnings import MissingExpectedField
-EXTENDED_CHEMICAL_SYMBOLS = set(CHEMICAL_SYMBOLS + EXTRA_SYMBOLS)
-
+if TYPE_CHECKING: # pragma: no cover
+ from pydantic import ValidationInfo
__all__ = (
"Vector3D",
@@ -37,8 +45,14 @@
EPS = 2**-23
-Vector3D = conlist(float, min_items=3, max_items=3)
-Vector3D_unknown = conlist(Union[float, None], min_items=3, max_items=3)
+Vector3D = Annotated[
+ list[Annotated[float, BeforeValidator(float)]], Field(min_length=3, max_length=3)
+]
+Vector3D_unknown = Annotated[
+ list[Optional[Annotated[float, BeforeValidator(float)]]],
+ Field(min_length=3, max_length=3),
+]
+SymOp = constr(pattern=SPACE_GROUP_SYMMETRY_OPERATION_REGEX)
class Periodicity(IntEnum):
@@ -77,125 +91,136 @@ class Species(BaseModel):
"""
- name: str = OptimadeField(
- ...,
- description="""Gives the name of the species; the **name** value MUST be unique in the `species` list.""",
- support=SupportLevel.MUST,
- queryable=SupportLevel.OPTIONAL,
- )
+ name: Annotated[
+ str,
+ OptimadeField(
+ description="""Gives the name of the species; the **name** value MUST be unique in the `species` list.""",
+ support=SupportLevel.MUST,
+ queryable=SupportLevel.OPTIONAL,
+ ),
+ ]
- chemical_symbols: List[str] = OptimadeField(
- ...,
- description="""MUST be a list of strings of all chemical elements composing this species. Each item of the list MUST be one of the following:
+ chemical_symbols: Annotated[
+ list[ChemicalSymbol],
+ OptimadeField(
+ description="""MUST be a list of strings of all chemical elements composing this species. Each item of the list MUST be one of the following:
- a valid chemical-element symbol, or
- the special value `"X"` to represent a non-chemical element, or
- the special value `"vacancy"` to represent that this site has a non-zero probability of having a vacancy (the respective probability is indicated in the `concentration` list, see below).
If any one entry in the `species` list has a `chemical_symbols` list that is longer than 1 element, the correct flag MUST be set in the list `structure_features`.""",
- support=SupportLevel.MUST,
- queryable=SupportLevel.OPTIONAL,
- )
+ support=SupportLevel.MUST,
+ queryable=SupportLevel.OPTIONAL,
+ ),
+ ]
- concentration: List[float] = OptimadeField(
- ...,
- description="""MUST be a list of floats, with same length as `chemical_symbols`. The numbers represent the relative concentration of the corresponding chemical symbol in this species. The numbers SHOULD sum to one. Cases in which the numbers do not sum to one typically fall only in the following two categories:
+ concentration: Annotated[
+ list[float],
+ OptimadeField(
+ description="""MUST be a list of floats, with same length as `chemical_symbols`. The numbers represent the relative concentration of the corresponding chemical symbol in this species. The numbers SHOULD sum to one. Cases in which the numbers do not sum to one typically fall only in the following two categories:
- Numerical errors when representing float numbers in fixed precision, e.g. for two chemical symbols with concentrations `1/3` and `2/3`, the concentration might look something like `[0.33333333333, 0.66666666666]`. If the client is aware that the sum is not one because of numerical precision, it can renormalize the values so that the sum is exactly one.
- Experimental errors in the data present in the database. In this case, it is the responsibility of the client to decide how to process the data.
Note that concentrations are uncorrelated between different site (even of the same species).""",
- support=SupportLevel.MUST,
- queryable=SupportLevel.OPTIONAL,
- )
-
- mass: Optional[List[float]] = OptimadeField(
- None,
- description="""If present MUST be a list of floats expressed in a.m.u.
+ support=SupportLevel.MUST,
+ queryable=SupportLevel.OPTIONAL,
+ ),
+ ]
+
+ mass: Annotated[
+ Optional[list[float]],
+ OptimadeField(
+ description="""If present MUST be a list of floats expressed in a.m.u.
Elements denoting vacancies MUST have masses equal to 0.""",
- unit="a.m.u.",
- support=SupportLevel.OPTIONAL,
- queryable=SupportLevel.OPTIONAL,
- )
+ unit="a.m.u.",
+ support=SupportLevel.OPTIONAL,
+ queryable=SupportLevel.OPTIONAL,
+ ),
+ ] = None
- original_name: Optional[str] = OptimadeField(
- None,
- description="""Can be any valid Unicode string, and SHOULD contain (if specified) the name of the species that is used internally in the source database.
+ original_name: Annotated[
+ Optional[str],
+ OptimadeField(
+ description="""Can be any valid Unicode string, and SHOULD contain (if specified) the name of the species that is used internally in the source database.
Note: With regards to "source database", we refer to the immediate source being queried via the OPTIMADE API implementation.""",
- support=SupportLevel.OPTIONAL,
- queryable=SupportLevel.OPTIONAL,
- )
-
- attached: Optional[List[str]] = OptimadeField(
- None,
- description="""If provided MUST be a list of length 1 or more of strings of chemical symbols for the elements attached to this site, or "X" for a non-chemical element.""",
- support=SupportLevel.OPTIONAL,
- queryable=SupportLevel.OPTIONAL,
- )
-
- nattached: Optional[List[int]] = OptimadeField(
- None,
- description="""If provided MUST be a list of length 1 or more of integers indicating the number of attached atoms of the kind specified in the value of the :field:`attached` key.""",
- support=SupportLevel.OPTIONAL,
- queryable=SupportLevel.OPTIONAL,
- )
+ support=SupportLevel.OPTIONAL,
+ queryable=SupportLevel.OPTIONAL,
+ ),
+ ] = None
+
+ attached: Annotated[
+ Optional[list[str]],
+ OptimadeField(
+ description="""If provided MUST be a list of length 1 or more of strings of chemical symbols for the elements attached to this site, or "X" for a non-chemical element.""",
+ support=SupportLevel.OPTIONAL,
+ queryable=SupportLevel.OPTIONAL,
+ ),
+ ] = None
+
+ nattached: Annotated[
+ Optional[list[int]],
+ OptimadeField(
+ description="""If provided MUST be a list of length 1 or more of integers indicating the number of attached atoms of the kind specified in the value of the :field:`attached` key.""",
+ support=SupportLevel.OPTIONAL,
+ queryable=SupportLevel.OPTIONAL,
+ ),
+ ] = None
+
+ @field_validator("concentration", "mass", mode="after")
+ def validate_concentration_and_mass(
+ cls, value: Optional[list[float]], info: "ValidationInfo"
+ ) -> Optional[list[float]]:
+ if not value:
+ return value
- @validator("chemical_symbols", each_item=True)
- def validate_chemical_symbols(cls, v):
- if v not in EXTENDED_CHEMICAL_SYMBOLS:
- raise ValueError(
- f'{v!r} MUST be an element symbol, e.g., "C", "He", or a special symbol from {EXTRA_SYMBOLS}.'
- )
- return v
-
- @validator("concentration", "mass")
- def validate_concentration_and_mass(cls, v, values, field):
- if not v:
- return v
- if values.get("chemical_symbols"):
- if len(v) != len(values["chemical_symbols"]):
+ if info.data.get("chemical_symbols"):
+ if len(value) != len(info.data["chemical_symbols"]):
raise ValueError(
- f"Length of concentration ({len(v)}) MUST equal length of chemical_symbols "
- f"({len(values.get('chemical_symbols', []))})"
+ f"Length of concentration ({len(value)}) MUST equal length of "
+ f"chemical_symbols ({len(info.data['chemical_symbols'])})"
)
- return v
+ return value
raise ValueError(
- f"Could not validate {field.name!r} as 'chemical_symbols' is missing/invalid."
+ f"Could not validate {info.field_name!r} as 'chemical_symbols' is missing/invalid."
)
- @validator("attached", "nattached")
- def validate_minimum_list_length(cls, v):
- if v is not None and len(v) < 1:
+ @field_validator("attached", "nattached", mode="after")
+ @classmethod
+ def validate_minimum_list_length(
+ cls, value: Optional[Union[list[str], list[int]]]
+ ) -> Optional[Union[list[str], list[int]]]:
+ if value is not None and len(value) < 1:
raise ValueError(
- f"The list's length MUST be 1 or more, instead it was found to be {len(v)}"
+ "The list's length MUST be 1 or more, instead it was found to be "
+ f"{len(value)}"
)
- return v
+ return value
- @root_validator
- def attached_nattached_mutually_exclusive(cls, values):
- attached, nattached = (
- values.get("attached", None),
- values.get("nattached", None),
- )
- if (attached is None and nattached is not None) or (
- attached is not None and nattached is None
+ @model_validator(mode="after")
+ def attached_nattached_mutually_exclusive(self) -> "Species":
+ if (self.attached is None and self.nattached is not None) or (
+ self.attached is not None and self.nattached is None
):
raise ValueError(
- f"Either both or none of attached ({attached}) and nattached ({nattached}) MUST be set."
+ f"Either both or none of attached ({self.attached}) and nattached "
+ f"({self.nattached}) MUST be set."
)
if (
- attached is not None
- and nattached is not None
- and len(attached) != len(nattached)
+ self.attached is not None
+ and self.nattached is not None
+ and len(self.attached) != len(self.nattached)
):
raise ValueError(
- f"attached ({attached}) and nattached ({nattached}) MUST be lists of equal length."
+ f"attached ({self.attached}) and nattached ({self.nattached}) MUST be "
+ "lists of equal length."
)
- return values
+ return self
class Assembly(BaseModel):
@@ -210,46 +235,52 @@ class Assembly(BaseModel):
"""
- sites_in_groups: List[List[int]] = OptimadeField(
- ...,
- description="""Index of the sites (0-based) that belong to each group for each assembly.
+ sites_in_groups: Annotated[
+ list[list[int]],
+ OptimadeField(
+ description="""Index of the sites (0-based) that belong to each group for each assembly.
- **Examples**:
- `[[1], [2]]`: two groups, one with the second site, one with the third.
- `[[1,2], [3]]`: one group with the second and third site, one with the fourth.""",
- support=SupportLevel.MUST,
- queryable=SupportLevel.OPTIONAL,
- )
-
- group_probabilities: List[float] = OptimadeField(
- ...,
- description="""Statistical probability of each group. It MUST have the same length as `sites_in_groups`.
+ support=SupportLevel.MUST,
+ queryable=SupportLevel.OPTIONAL,
+ ),
+ ]
+
+ group_probabilities: Annotated[
+ list[float],
+ OptimadeField(
+ description="""Statistical probability of each group. It MUST have the same length as `sites_in_groups`.
It SHOULD sum to one.
See below for examples of how to specify the probability of the occurrence of a vacancy.
The possible reasons for the values not to sum to one are the same as already specified above for the `concentration` of each `species`.""",
- support=SupportLevel.MUST,
- queryable=SupportLevel.OPTIONAL,
- )
-
- @validator("sites_in_groups")
- def validate_sites_in_groups(cls, v):
+ support=SupportLevel.MUST,
+ queryable=SupportLevel.OPTIONAL,
+ ),
+ ]
+
+ @field_validator("sites_in_groups", mode="after")
+ @classmethod
+ def validate_sites_in_groups(cls, value: list[list[int]]) -> list[list[int]]:
sites = []
- for group in v:
+ for group in value:
sites.extend(group)
if len(set(sites)) != len(sites):
raise ValueError(
- f"A site MUST NOT appear in more than one group. Given value: {v}"
+ f"A site MUST NOT appear in more than one group. Given value: {value}"
)
- return v
+ return value
- @validator("group_probabilities")
- def check_self_consistency(cls, v, values):
- if len(v) != len(values.get("sites_in_groups", [])):
+ @model_validator(mode="after")
+ def check_self_consistency(self) -> "Assembly":
+ if len(self.group_probabilities) != len(self.sites_in_groups):
raise ValueError(
f"sites_in_groups and group_probabilities MUST be of same length, "
- f"but are {len(values.get('sites_in_groups', []))} and {len(v)}, respectively"
+ f"but are {len(self.sites_in_groups)} and {len(self.group_probabilities)}, "
+ "respectively"
)
- return v
+ return self
CORRELATED_STRUCTURE_FIELDS = (
@@ -263,9 +294,10 @@ def check_self_consistency(cls, v, values):
class StructureResourceAttributes(EntryResourceAttributes):
"""This class contains the Field for the attributes used to represent a structure, e.g. unit cell, atoms, positions."""
- elements: Optional[List[str]] = OptimadeField(
- ...,
- description="""The chemical symbols of the different elements present in the structure.
+ elements: Annotated[
+ Optional[list[str]],
+ OptimadeField(
+ description="""The chemical symbols of the different elements present in the structure.
- **Type**: list of strings.
@@ -285,13 +317,15 @@ class StructureResourceAttributes(EntryResourceAttributes):
- A filter that matches all records of structures that contain Si, Al **and** O, and possibly other elements: `elements HAS ALL "Si", "Al", "O"`.
- To match structures with exactly these three elements, use `elements HAS ALL "Si", "Al", "O" AND elements LENGTH 3`.
- Note: length queries on this property can be equivalently formulated by filtering on the `nelements`_ property directly.""",
- support=SupportLevel.SHOULD,
- queryable=SupportLevel.MUST,
- )
+ support=SupportLevel.SHOULD,
+ queryable=SupportLevel.MUST,
+ ),
+ ] = None
- nelements: Optional[int] = OptimadeField(
- ...,
- description="""Number of different elements in the structure as an integer.
+ nelements: Annotated[
+ Optional[int],
+ OptimadeField(
+ description="""Number of different elements in the structure as an integer.
- **Type**: integer
@@ -307,13 +341,15 @@ class StructureResourceAttributes(EntryResourceAttributes):
- Note: queries on this property can equivalently be formulated using `elements LENGTH`.
- A filter that matches structures that have exactly 4 elements: `nelements=4`.
- A filter that matches structures that have between 2 and 7 elements: `nelements>=2 AND nelements<=7`.""",
- support=SupportLevel.SHOULD,
- queryable=SupportLevel.MUST,
- )
+ support=SupportLevel.SHOULD,
+ queryable=SupportLevel.MUST,
+ ),
+ ] = None
- elements_ratios: Optional[List[float]] = OptimadeField(
- ...,
- description="""Relative proportions of different elements in the structure.
+ elements_ratios: Annotated[
+ Optional[list[float]],
+ OptimadeField(
+ description="""Relative proportions of different elements in the structure.
- **Type**: list of floats
@@ -332,13 +368,15 @@ class StructureResourceAttributes(EntryResourceAttributes):
- Note: Useful filters can be formulated using the set operator syntax for correlated values.
However, since the values are floating point values, the use of equality comparisons is generally inadvisable.
- OPTIONAL: a filter that matches structures where approximately 1/3 of the atoms in the structure are the element Al is: `elements:elements_ratios HAS ALL "Al":>0.3333, "Al":<0.3334`.""",
- support=SupportLevel.SHOULD,
- queryable=SupportLevel.MUST,
- )
+ support=SupportLevel.SHOULD,
+ queryable=SupportLevel.MUST,
+ ),
+ ] = None
- chemical_formula_descriptive: Optional[str] = OptimadeField(
- ...,
- description="""The chemical formula for a structure as a string in a form chosen by the API implementation.
+ chemical_formula_descriptive: Annotated[
+ Optional[str],
+ OptimadeField(
+ description="""The chemical formula for a structure as a string in a form chosen by the API implementation.
- **Type**: string
@@ -360,13 +398,15 @@ class StructureResourceAttributes(EntryResourceAttributes):
- Note: the free-form nature of this property is likely to make queries on it across different databases inconsistent.
- A filter that matches an exactly given formula: `chemical_formula_descriptive="(H2O)2 Na"`.
- A filter that does a partial match: `chemical_formula_descriptive CONTAINS "H2O"`.""",
- support=SupportLevel.SHOULD,
- queryable=SupportLevel.MUST,
- )
-
- chemical_formula_reduced: Optional[str] = OptimadeField(
- ...,
- description="""The reduced chemical formula for a structure as a string with element symbols and integer chemical proportion numbers.
+ support=SupportLevel.SHOULD,
+ queryable=SupportLevel.MUST,
+ ),
+ ] = None
+
+ chemical_formula_reduced: Annotated[
+ Optional[str],
+ OptimadeField(
+ description="""The reduced chemical formula for a structure as a string with element symbols and integer chemical proportion numbers.
The proportion number MUST be omitted if it is 1.
- **Type**: string
@@ -389,14 +429,16 @@ class StructureResourceAttributes(EntryResourceAttributes):
- **Query examples**:
- A filter that matches an exactly given formula is `chemical_formula_reduced="H2NaO"`.""",
- support=SupportLevel.SHOULD,
- queryable=SupportLevel.MUST,
- regex=CHEMICAL_FORMULA_REGEXP,
- )
+ support=SupportLevel.SHOULD,
+ queryable=SupportLevel.MUST,
+ pattern=CHEMICAL_FORMULA_REGEXP,
+ ),
+ ] = None
- chemical_formula_hill: Optional[str] = OptimadeField(
- None,
- description="""The chemical formula for a structure in [Hill form](https://dx.doi.org/10.1021/ja02046a005) with element symbols followed by integer chemical proportion numbers. The proportion number MUST be omitted if it is 1.
+ chemical_formula_hill: Annotated[
+ Optional[str],
+ OptimadeField(
+ description="""The chemical formula for a structure in [Hill form](https://dx.doi.org/10.1021/ja02046a005) with element symbols followed by integer chemical proportion numbers. The proportion number MUST be omitted if it is 1.
- **Type**: string
@@ -420,14 +462,16 @@ class StructureResourceAttributes(EntryResourceAttributes):
- **Query examples**:
- A filter that matches an exactly given formula is `chemical_formula_hill="H2O2"`.""",
- support=SupportLevel.OPTIONAL,
- queryable=SupportLevel.OPTIONAL,
- regex=CHEMICAL_FORMULA_REGEXP,
- )
+ support=SupportLevel.OPTIONAL,
+ queryable=SupportLevel.OPTIONAL,
+ pattern=CHEMICAL_FORMULA_REGEXP,
+ ),
+ ] = None
- chemical_formula_anonymous: Optional[str] = OptimadeField(
- ...,
- description="""The anonymous formula is the `chemical_formula_reduced`, but where the elements are instead first ordered by their chemical proportion number, and then, in order left to right, replaced by anonymous symbols A, B, C, ..., Z, Aa, Ba, ..., Za, Ab, Bb, ... and so on.
+ chemical_formula_anonymous: Annotated[
+ Optional[str],
+ OptimadeField(
+ description="""The anonymous formula is the `chemical_formula_reduced`, but where the elements are instead first ordered by their chemical proportion number, and then, in order left to right, replaced by anonymous symbols A, B, C, ..., Z, Aa, Ba, ..., Za, Ab, Bb, ... and so on.
- **Type**: string
@@ -442,17 +486,19 @@ class StructureResourceAttributes(EntryResourceAttributes):
- **Querying**:
- A filter that matches an exactly given formula is `chemical_formula_anonymous="A2B"`.""",
- support=SupportLevel.SHOULD,
- queryable=SupportLevel.MUST,
- regex=CHEMICAL_FORMULA_REGEXP,
- )
-
- dimension_types: Optional[ # type: ignore[valid-type]
- conlist(Periodicity, min_items=3, max_items=3)
- ] = OptimadeField(
- None,
- title="Dimension Types",
- description="""List of three integers.
+ support=SupportLevel.SHOULD,
+ queryable=SupportLevel.MUST,
+ pattern=CHEMICAL_FORMULA_REGEXP,
+ ),
+ ] = None
+
+ dimension_types: Annotated[
+ Optional[list[Periodicity]],
+ OptimadeField(
+ min_length=3,
+ max_length=3,
+ title="Dimension Types",
+ description="""List of three integers.
For each of the three directions indicated by the three lattice vectors (see property `lattice_vectors`), this list indicates if the direction is periodic (value `1`) or non-periodic (value `0`).
Note: the elements in this list each refer to the direction of the corresponding entry in `lattice_vectors` and *not* the Cartesian x, y, z directions.
@@ -469,13 +515,15 @@ class StructureResourceAttributes(EntryResourceAttributes):
- For a wire along the direction specified by the third lattice vector: `[0, 0, 1]`
- For a 2D surface/slab, periodic on the plane defined by the first and third lattice vectors: `[1, 0, 1]`
- For a bulk 3D system: `[1, 1, 1]`""",
- support=SupportLevel.SHOULD,
- queryable=SupportLevel.OPTIONAL,
- )
+ support=SupportLevel.SHOULD,
+ queryable=SupportLevel.OPTIONAL,
+ ),
+ ] = None
- nperiodic_dimensions: Optional[int] = OptimadeField(
- ...,
- description="""An integer specifying the number of periodic dimensions in the structure, equivalent to the number of non-zero entries in `dimension_types`.
+ nperiodic_dimensions: Annotated[
+ Optional[int],
+ OptimadeField(
+ description="""An integer specifying the number of periodic dimensions in the structure, equivalent to the number of non-zero entries in `dimension_types`.
- **Type**: integer
@@ -491,15 +539,17 @@ class StructureResourceAttributes(EntryResourceAttributes):
- **Query examples**:
- Match only structures with exactly 3 periodic dimensions: `nperiodic_dimensions=3`
- Match all structures with 2 or fewer periodic dimensions: `nperiodic_dimensions<=2`""",
- support=SupportLevel.SHOULD,
- queryable=SupportLevel.MUST,
- )
-
- lattice_vectors: Optional[ # type: ignore[valid-type]
- conlist(Vector3D_unknown, min_items=3, max_items=3)
- ] = OptimadeField(
- None,
- description="""The three lattice vectors in Cartesian coordinates, in ångström (Å).
+ support=SupportLevel.SHOULD,
+ queryable=SupportLevel.MUST,
+ ),
+ ] = None
+
+ lattice_vectors: Annotated[
+ Optional[list[Vector3D_unknown]],
+ OptimadeField(
+ min_length=3,
+ max_length=3,
+ description="""The three lattice vectors in Cartesian coordinates, in ångström (Å).
- **Type**: list of list of floats or unknown values.
@@ -518,14 +568,16 @@ class StructureResourceAttributes(EntryResourceAttributes):
- **Examples**:
- `[[4.0,0.0,0.0],[0.0,4.0,0.0],[0.0,1.0,4.0]]` represents a cell, where the first vector is `(4, 0, 0)`, i.e., a vector aligned along the `x` axis of length 4 Ã…; the second vector is `(0, 4, 0)`; and the third vector is `(0, 1, 4)`.""",
- unit="Ã…",
- support=SupportLevel.SHOULD,
- queryable=SupportLevel.OPTIONAL,
- )
-
- cartesian_site_positions: Optional[List[Vector3D]] = OptimadeField( # type: ignore[valid-type]
- ...,
- description="""Cartesian positions of each site in the structure.
+ unit="Ã…",
+ support=SupportLevel.SHOULD,
+ queryable=SupportLevel.OPTIONAL,
+ ),
+ ] = None
+
+ cartesian_site_positions: Annotated[
+ Optional[list[Vector3D]],
+ OptimadeField(
+ description="""Cartesian positions of each site in the structure.
A site is usually used to describe positions of atoms; what atoms can be encountered at a given site is conveyed by the `species_at_sites` property, and the species themselves are described in the `species` property.
- **Type**: list of list of floats
@@ -539,14 +591,164 @@ class StructureResourceAttributes(EntryResourceAttributes):
- **Examples**:
- `[[0,0,0],[0,0,2]]` indicates a structure with two sites, one sitting at the origin and one along the (positive) *z*-axis, 2 Ã… away from the origin.""",
- unit="Ã…",
- support=SupportLevel.SHOULD,
+ unit="Ã…",
+ support=SupportLevel.SHOULD,
+ queryable=SupportLevel.OPTIONAL,
+ ),
+ ] = None
+
+ space_group_symmetry_operations_xyz: Optional[list[SymOp]] = OptimadeField( # type: ignore[valid-type]
+ None,
+ description="""A list of symmetry operations given as general position x, y and z coordinates in algebraic form.
+
+Each symmetry operation is described by a string that gives that symmetry operation in Jones' faithful representation (Bradley & Cracknell, 1972: pp. 35-37), adapted for computer string notation.
+The letters x, y and z that are typesetted with overbars in printed text represent coordinate values multiplied by -1 and are encoded as -x, -y and -z, respectively.
+The syntax of the strings representing symmetry operations MUST conform to regular expressions given in appendix The Symmetry Operation String Regular Expressions.
+The interpretation of the strings MUST follow the conventions of the IUCr CIF core dictionary (IUCr, 2023).
+In particular, this property MUST explicitly provide all symmetry operations needed to generate all the atoms in the unit cell from the atoms in the asymmetric unit, for the setting used.
+This symmetry operation set MUST always include the `"x,y,z"` identity operation.
+The symmetry operations are to be applied to fractional atom coordinates.
+In case only Cartesian coordinates are available, these Cartesian coordinates must be converted to fractional coordinates before the application of the provided symmetry operations.
+If the symmetry operation list is present, it MUST be compatible with other space group specifications (e.g. the ITC space group number, the Hall symbol, the Hermann-Mauguin symbol) if these are present.
+
+- **Type**: list of strings
+
+- **Requirements/Conventions**:
+ - **Support**: OPTIONAL support in implementations, i.e., MAY be `null`.
+
+ - The property is RECOMMENDED if coordinates are returned in a form to which these operations can or must be applied (e.g. fractional atom coordinates of an asymmetric unit).
+ - The property is REQUIRED if symmetry operations are necessary to reconstruct the full model of the material and no other symmetry information (e.g., the Hall symbol) is provided that would allow the user to derive symmetry operations unambiguously.
+ - MUST be null if `nperiodic_dimensions` is equal to 0.
+
+- **Examples**:
+
+ - Space group operations for the space group with ITC number 3 (H-M symbol `P 2`, extended H-M symbol `P 1 2 1`, Hall symbol `P 2y`): `["x,y,z", "-x,y,-z"]`
+ - Space group operations for the space group with ITC number 5 (H-M symbol `C 2`, extended H-M symbol `C 1 2 1`, Hall symbol `C 2y`): `["x,y,z", "-x,y,-z", "x+1/2,y+1/2,z", "-x+1/2,y+1/2,-z"]`
+
+- **Notes**:
+ The list of space group symmetry operations applies to the whole periodic array of atoms and together with the lattice translations given in the `lattice_vectors` property provides the necessary information to reconstruct all atom site positions of the periodic material.
+ Thus, the symmetry operations described in this property are only applicable to material models with at least one periodic dimension.
+ This property is not meant to represent arbitrary symmetries of molecules, non-periodic (finite) collections of atoms or non-crystallographic symmetry.
+
+- **Bibliographic References**:
+
+ Bradley, C. J. and Cracknell, A. P. (1972) The Mathematical Theory of Symmetry in Solids. Oxford, Clarendon Press (paperback edition 2010) 745 p. ISBN 978-0-19-958258-7.
+
+ IUCr (2023) Core dictionary (coreCIF) version 2.4.5; data name _space_group_symop_operation_xyz. Available from: https://www.iucr.org/__data/iucr/cifdic_html/1/cif_core.dic/Ispace_group_symop_operation_xyz.html [Accessed 2023-06-18T16:46+03:00].""",
+ support=SupportLevel.OPTIONAL,
+ queryable=SupportLevel.OPTIONAL,
+ )
+
+ space_group_symbol_hall: Optional[str] = OptimadeField(
+ None,
+ description="""A Hall space group symbol representing the symmetry of the structure as defined in (Hall, 1981, 1981a).
+
+- **Type**: string
+
+- **Requirements/Conventions**:
+ - **Support**: OPTIONAL support in implementations, i.e., MAY be `null`.
+ - **Query**: Support for queries on this property is OPTIONAL.
+ - The change-of-basis operations are used as defined in the International Tables of Crystallography (ITC) Vol. B, Sect. 1.4, Appendix A1.4.2 (IUCr, 2001).
+ - Each component of the Hall symbol MUST be separated by a single space symbol.
+ - If there exists a standard Hall symbol which represents the symmetry it SHOULD be used.
+ - MUST be null if `nperiodic_dimensions` is not equal to 3.
+
+- **Examples**:
+
+ - Space group symbols with explicit origin (the Hall symbols):
+
+ - `P 2c -2ac`
+ - `-I 4bd 2ab 3`
+
+ - Space group symbols with change-of-basis operations:
+
+ - `P 2yb (-1/2*x+z,1/2*x,y)`
+ - `-I 4 2 (1/2*x+1/2*y,-1/2*x+1/2*y,z)`
+
+- **Bibliographic References**:
+
+ Hall, S. R. (1981) Space-group notation with an explicit origin. Acta Crystallographica Section A, 37, 517-525, International Union of Crystallography (IUCr), DOI: https://doi.org/10.1107/s0567739481001228
+
+ Hall, S. R. (1981a) Space-group notation with an explicit origin; erratum. Acta Crystallographica Section A, 37, 921-921, International Union of Crystallography (IUCr), DOI: https://doi.org/10.1107/s0567739481001976
+
+ IUCr (2001). International Tables for Crystallography vol. B. Reciprocal Space. Ed. U. Shmueli. 2-nd edition. Dordrecht/Boston/London, Kluwer Academic Publishers.""",
+ support=SupportLevel.OPTIONAL,
+ queryable=SupportLevel.OPTIONAL,
+ )
+
+ space_group_symbol_hermann_mauguin: Optional[str] = OptimadeField(
+ None,
+ description="""A human- and machine-readable string containing the short Hermann-Mauguin (H-M) symbol which specifies the space group of the structure in the response.
+- **Type**: string
+
+- **Requirements/Conventions**:
+ - **Support**: OPTIONAL support in implementations, i.e., MAY be `null`.
+ - **Query**: Support for queries on this property is OPTIONAL.
+ - The H-M symbol SHOULD aim to convey the closest representation of the symmetry information that can be specified using the short format used in the International Tables for Crystallography vol. A (IUCr, 2005), Table 4.3.2.1 as described in the accompanying text.
+ - The symbol MAY be a non-standard short H-M symbol.
+ - The H-M symbol does not unambiguously communicate the axis, cell, and origin choice, and the given symbol SHOULD NOT be amended to convey this information.
+ - To encode as character strings, the following adaptations MUST be made when representing H-M symbols given in their typesetted form:
+
+ - the overbar above the numbers MUST be changed to the minus sign in front of the digit (e.g. '-2');
+ - subscripts that denote screw axes are written as digits immediately after the axis designator without a space (e.g. 'P 32')
+ - the space group generators MUST be separated by a single space (e.g. 'P 21 21 2');
+ - there MUST be no spaces in the space group generator designation (i.e. use 'P 21/m', not the 'P 21 / m');
+
+- **Examples**:
+ - `C 2`
+ - `P 21 21 21`
+
+- **Bibliographic References**:
+
+ IUCr (2005). International Tables for Crystallography vol. A. Space-Group Symmetry. Ed. Theo Hahn. 5-th edition. Dordrecht, Springer.""",
+ support=SupportLevel.OPTIONAL,
+ queryable=SupportLevel.OPTIONAL,
+ )
+
+ space_group_symbol_hermann_mauguin_extended: Optional[str] = OptimadeField(
+ None,
+ description="""A human- and machine-readable string containing the extended Hermann-Mauguin (H-M) symbol which specifies the space group of the structure in the response.
+
+- **Type**: string
+- **Requirements/Conventions**:
+
+ - **Support**: OPTIONAL support in implementations, i.e., MAY be `null`.
+ - **Query**: Support for queries on this property is OPTIONAL.
+ - The H-M symbols SHOULD be given as specified in the International Tables for Crystallography vol. A (IUCr, 2005), Table 4.3.2.1.
+ - The change-of-basis operation SHOULD be provided for the non-standard axis and cell choices.
+ - The extended H-M symbol does not unambiguously communicate the origin choice, and the given symbol SHOULD NOT be amended to convey this information.
+ - The description of the change-of-basis SHOULD follow conventions of the ITC Vol. B, Sect. 1.4, Appendix A1.4.2 (IUCr, 2001).
+ - The same character string encoding conventions MUST be used as for the specification of the `space_group_symbol_hermann_mauguin` property.
+
+- **Examples**:
+
+ - `C 1 2 1`
+
+- **Bibliographic References**:
+
+ IUCr (2001). International Tables for Crystallography vol. B. Reciprocal Space. Ed. U. Shmueli. 2-nd edition. Dordrecht/Boston/London, Kluwer Academic Publishers.""",
+ support=SupportLevel.OPTIONAL,
+ queryable=SupportLevel.OPTIONAL,
+ )
+
+ space_group_it_number: Optional[conint(ge=1, le=230)] = OptimadeField( # type: ignore[valid-type]
+ None,
+ description="""Space group number for the structure assigned by the International Tables for Crystallography Vol. A.
+- **Type**: integer
+
+- **Requirements/Conventions**:
+ - **Support**: OPTIONAL support in implementations, i.e., MAY be `null`.
+ - **Query**: Support for queries on this property is OPTIONAL.
+ - The integer value MUST be between 1 and 230.
+ - MUST be null if `nperiodic_dimensions` is not equal to 3.""",
+ support=SupportLevel.OPTIONAL,
queryable=SupportLevel.OPTIONAL,
)
- nsites: Optional[int] = OptimadeField(
- ...,
- description="""An integer specifying the length of the `cartesian_site_positions` property.
+ nsites: Annotated[
+ Optional[int],
+ OptimadeField(
+ description="""An integer specifying the length of the `cartesian_site_positions` property.
- **Type**: integer
@@ -560,13 +762,15 @@ class StructureResourceAttributes(EntryResourceAttributes):
- **Query examples**:
- Match only structures with exactly 4 sites: `nsites=4`
- Match structures that have between 2 and 7 sites: `nsites>=2 AND nsites<=7`""",
- queryable=SupportLevel.MUST,
- support=SupportLevel.SHOULD,
- )
-
- species: Optional[List[Species]] = OptimadeField(
- ...,
- description="""A list describing the species of the sites of this structure.
+ queryable=SupportLevel.MUST,
+ support=SupportLevel.SHOULD,
+ ),
+ ] = None
+
+ species: Annotated[
+ Optional[list[Species]],
+ OptimadeField(
+ description="""A list describing the species of the sites of this structure.
Species can represent pure chemical elements, virtual-crystal atoms representing a statistical occupation of a given site by multiple chemical elements, and/or a location to which there are attached atoms, i.e., atoms whose precise location are unknown beyond that they are attached to that position (frequently used to indicate hydrogen atoms attached to another element, e.g., a carbon with three attached hydrogens might represent a methyl group, -CH3).
- **Type**: list of dictionary with keys:
@@ -629,13 +833,15 @@ class StructureResourceAttributes(EntryResourceAttributes):
- `[ {"name": "C12", "chemical_symbols": ["C"], "concentration": [1.0], "mass": [12.0]} ]`: any site with this species is occupied by a carbon isotope with mass 12.
- `[ {"name": "C13", "chemical_symbols": ["C"], "concentration": [1.0], "mass": [13.0]} ]`: any site with this species is occupied by a carbon isotope with mass 13.
- `[ {"name": "CH3", "chemical_symbols": ["C"], "concentration": [1.0], "attached": ["H"], "nattached": [3]} ]`: any site with this species is occupied by a methyl group, -CH3, which is represented without specifying precise positions of the hydrogen atoms.""",
- support=SupportLevel.SHOULD,
- queryable=SupportLevel.OPTIONAL,
- )
-
- species_at_sites: Optional[List[str]] = OptimadeField(
- ...,
- description="""Name of the species at each site (where values for sites are specified with the same order of the property `cartesian_site_positions`).
+ support=SupportLevel.SHOULD,
+ queryable=SupportLevel.OPTIONAL,
+ ),
+ ] = None
+
+ species_at_sites: Annotated[
+ Optional[list[str]],
+ OptimadeField(
+ description="""Name of the species at each site (where values for sites are specified with the same order of the property `cartesian_site_positions`).
The properties of the species are found in the property `species`.
- **Type**: list of strings.
@@ -653,13 +859,15 @@ class StructureResourceAttributes(EntryResourceAttributes):
- **Examples**:
- `["Ti","O2"]` indicates that the first site is hosting a species labeled `"Ti"` and the second a species labeled `"O2"`.
- `["Ac", "Ac", "Ag", "Ir"]` indicating the first two sites contains the `"Ac"` species, while the third and fourth sites contain the `"Ag"` and `"Ir"` species, respectively.""",
- support=SupportLevel.SHOULD,
- queryable=SupportLevel.OPTIONAL,
- )
+ support=SupportLevel.SHOULD,
+ queryable=SupportLevel.OPTIONAL,
+ ),
+ ] = None
- assemblies: Optional[List[Assembly]] = OptimadeField(
- None,
- description="""A description of groups of sites that are statistically correlated.
+ assemblies: Annotated[
+ Optional[list[Assembly]],
+ OptimadeField(
+ description="""A description of groups of sites that are statistically correlated.
- **Type**: list of dictionary with keys:
- `sites_in_groups`: list of list of integers (REQUIRED)
@@ -761,14 +969,16 @@ class StructureResourceAttributes(EntryResourceAttributes):
Site 0 is present with a probability of 20 % and site 1 with a probability of 80 %. These two sites are correlated (either site 0 or 1 is present). Similarly, site 2 is present with a probability of 30 % and site 3 with a probability of 70 %.
These two sites are correlated (either site 2 or 3 is present).
However, the presence or absence of sites 0 and 1 is not correlated with the presence or absence of sites 2 and 3 (in the specific example, the pair of sites (0, 2) can occur with 0.2*0.3 = 6 % probability; the pair (0, 3) with 0.2*0.7 = 14 % probability; the pair (1, 2) with 0.8*0.3 = 24 % probability; and the pair (1, 3) with 0.8*0.7 = 56 % probability).""",
- support=SupportLevel.OPTIONAL,
- queryable=SupportLevel.OPTIONAL,
- )
+ support=SupportLevel.OPTIONAL,
+ queryable=SupportLevel.OPTIONAL,
+ ),
+ ] = None
- structure_features: List[StructureFeatures] = OptimadeField(
- ...,
- title="Structure Features",
- description="""A list of strings that flag which special features are used by the structure.
+ structure_features: Annotated[
+ list[StructureFeatures],
+ OptimadeField(
+ title="Structure Features",
+ description="""A list of strings that flag which special features are used by the structure.
- **Type**: list of strings
@@ -789,62 +999,45 @@ class StructureResourceAttributes(EntryResourceAttributes):
- `assemblies`: this flag MUST be present if the property `assemblies` is present.
- **Examples**: A structure having implicit atoms and using assemblies: `["assemblies", "implicit_atoms"]`""",
- support=SupportLevel.MUST,
- queryable=SupportLevel.MUST,
- )
-
- class Config:
- def schema_extra(schema, model):
- """Two things need to be added to the schema:
-
- 1. Constrained types in pydantic do not currently play nicely with
- "Required Optional" fields, i.e. fields must be specified but can be null.
- The two contrained list fields, `dimension_types` and `lattice_vectors`,
- are OPTIMADE 'SHOULD' fields, which means that they are allowed to be null.
-
- 2. All OPTIMADE 'SHOULD' fields are allowed to be null, so we manually set them
- to be `nullable` according to the OpenAPI definition.
-
- """
- schema["required"].insert(7, "dimension_types")
- schema["required"].insert(9, "lattice_vectors")
-
- nullable_props = (
- prop
- for prop in schema["required"]
- if schema["properties"][prop].get("x-optimade-support")
- == SupportLevel.SHOULD
- )
- for prop in nullable_props:
- schema["properties"][prop]["nullable"] = True
+ support=SupportLevel.MUST,
+ queryable=SupportLevel.MUST,
+ ),
+ ]
- @root_validator(pre=True)
- def warn_on_missing_correlated_fields(cls, values):
+ @model_validator(mode="after")
+ def warn_on_missing_correlated_fields(self) -> "StructureResourceAttributes":
"""Emit warnings if a field takes a null value when a value
was expected based on the value/nullity of another field.
"""
accumulated_warnings = []
for field_set in CORRELATED_STRUCTURE_FIELDS:
- missing_fields = {f for f in field_set if values.get(f) is None}
+ missing_fields = {
+ field for field in field_set if getattr(self, field, None) is None
+ }
if missing_fields and len(missing_fields) != len(field_set):
accumulated_warnings += [
- f"Structure with values {values} is missing fields {missing_fields} which are required if {field_set - missing_fields} are present."
+ f"Structure with attributes {self} is missing fields "
+ f"{missing_fields} which are required if "
+ f"{field_set - missing_fields} are present."
]
for warn in accumulated_warnings:
warnings.warn(warn, MissingExpectedField)
- return values
+ return self
- @validator("chemical_formula_reduced", "chemical_formula_hill")
- def check_ordered_formula(cls, v, field):
- if v is None:
- return v
+ @field_validator("chemical_formula_reduced", "chemical_formula_hill", mode="after")
+ @classmethod
+ def check_ordered_formula(
+ cls, value: Optional[str], info: "ValidationInfo"
+ ) -> Optional[str]:
+ if value is None:
+ return value
- elements = re.findall(r"[A-Z][a-z]?", v)
+ elements = re.findall(r"[A-Z][a-z]?", value)
expected_elements = sorted(elements)
- if field.name == "chemical_formula_hill":
+ if info.field_name == "chemical_formula_hill":
# Make sure C is first (and H is second, if present along with C).
if "C" in expected_elements:
expected_elements = sorted(
@@ -854,24 +1047,27 @@ def check_ordered_formula(cls, v, field):
if any(elem not in CHEMICAL_SYMBOLS for elem in elements):
raise ValueError(
- f"Cannot use unknown chemical symbols {[elem for elem in elements if elem not in CHEMICAL_SYMBOLS]} in {field.name!r}"
+ f"Cannot use unknown chemical symbols {[elem for elem in elements if elem not in CHEMICAL_SYMBOLS]} in {info.field_name!r}"
)
if expected_elements != elements:
- order = "Hill" if field.name == "chemical_formula_hill" else "alphabetical"
+ order = (
+ "Hill" if info.field_name == "chemical_formula_hill" else "alphabetical"
+ )
raise ValueError(
- f"Elements in {field.name!r} must appear in {order} order: {expected_elements} not {elements}."
+ f"Elements in {info.field_name!r} must appear in {order} order: {expected_elements} not {elements}."
)
- return v
+ return value
- @validator("chemical_formula_anonymous")
- def check_anonymous_formula(cls, v):
- if v is None:
- return v
+ @field_validator("chemical_formula_anonymous", mode="after")
+ @classmethod
+ def check_anonymous_formula(cls, value: Optional[str]) -> Optional[str]:
+ if value is None:
+ return value
- elements = tuple(re.findall(r"[A-Z][a-z]*", v))
- numbers = re.split(r"[A-Z][a-z]*", v)[1:]
+ elements = tuple(re.findall(r"[A-Z][a-z]*", value))
+ numbers = re.split(r"[A-Z][a-z]*", value)[1:]
numbers = [int(i) if i else 1 for i in numbers]
expected_labels = ANONYMOUS_ELEMENTS[: len(elements)]
@@ -879,225 +1075,268 @@ def check_anonymous_formula(cls, v):
if expected_numbers != numbers:
raise ValueError(
- f"'chemical_formula_anonymous' {v} has wrong order: elements with highest proportion should appear first: {numbers} vs expected {expected_numbers}"
+ f"'chemical_formula_anonymous' {value} has wrong order: elements with "
+ f"highest proportion should appear first: {numbers} vs expected "
+ f"{expected_numbers}"
)
if elements != expected_labels:
raise ValueError(
- f"'chemical_formula_anonymous' {v} has wrong labels: {elements} vs expected {expected_labels}."
+ f"'chemical_formula_anonymous' {value} has wrong labels: {elements} vs"
+ f" expected {expected_labels}."
)
- return v
+ return value
- @validator("chemical_formula_anonymous", "chemical_formula_reduced")
- def check_reduced_formulae(cls, value, field):
+ @field_validator(
+ "chemical_formula_anonymous", "chemical_formula_reduced", mode="after"
+ )
+ @classmethod
+ def check_reduced_formulae(
+ cls, value: Optional[str], info: "ValidationInfo"
+ ) -> Optional[str]:
if value is None:
return value
reduced_formula = reduce_formula(value)
if reduced_formula != value:
raise ValueError(
- f"{field.name} {value!r} is not properly reduced: expected {reduced_formula!r}."
+ f"{info.field_name} {value!r} is not properly reduced: expected "
+ f"{reduced_formula!r}."
)
return value
- @validator("elements", each_item=True)
- def element_must_be_chemical_symbol(cls, v):
- if v not in CHEMICAL_SYMBOLS:
- raise ValueError(f"Only chemical symbols are allowed, you passed: {v}")
- return v
-
- @validator("elements")
- def elements_must_be_alphabetical(cls, v):
- if v is None:
- return v
-
- if sorted(v) != v:
- raise ValueError(f"elements must be sorted alphabetically, but is: {v}")
- return v
+ @field_validator("elements", mode="after")
+ @classmethod
+ def elements_must_be_alphabetical(
+ cls, value: Optional[list[str]]
+ ) -> Optional[list[str]]:
+ if value is None:
+ return value
- @validator("elements_ratios")
- def ratios_must_sum_to_one(cls, v):
- if v is None:
- return v
+ if sorted(value) != value:
+ raise ValueError(f"elements must be sorted alphabetically, but is: {value}")
+ return value
- if abs(sum(v) - 1) > EPS:
- raise ValueError(
- f"elements_ratios MUST sum to 1 within (at least single precision) floating point accuracy. It sums to: {sum(v)}"
- )
- return v
+ @field_validator("space_group_symbol_hall", "space_group_it_number", mode="after")
+ @classmethod
+ def check_space_group_vs_nperiodic_dimensions(cls, value):
+ if sorted(value) != value:
+ raise ValueError(f"elements must be sorted alphabetically, but is: {value}")
+ return value
- @validator("nperiodic_dimensions")
- def check_periodic_dimensions(cls, v, values):
- if v is None:
- return v
+ @field_validator("elements_ratios", mode="after")
+ @classmethod
+ def ratios_must_sum_to_one(
+ cls, value: Optional[list[float]]
+ ) -> Optional[list[float]]:
+ if value is None:
+ return value
- if values.get("dimension_types") and v != sum(values.get("dimension_types")):
+ if abs(sum(value) - 1) > EPS:
raise ValueError(
- f"nperiodic_dimensions ({v}) does not match expected value of {sum(values['dimension_types'])} "
- f"from dimension_types ({values['dimension_types']})"
+ "elements_ratios MUST sum to 1 within (at least single precision) "
+ f"floating point accuracy. It sums to: {sum(value)}"
)
+ return value
- return v
-
- @validator("lattice_vectors", always=True)
- def required_if_dimension_types_has_one(cls, v, values):
- if v is None:
- return v
+ @model_validator(mode="after")
+ def check_dimensions_types_dependencies(self) -> "StructureResourceAttributes":
+ if self.nperiodic_dimensions is not None:
+ if self.dimension_types and self.nperiodic_dimensions != sum(
+ self.dimension_types
+ ):
+ raise ValueError(
+ f"nperiodic_dimensions ({self.nperiodic_dimensions}) does not match "
+ f"expected value of {sum(self.dimension_types)} from dimension_types "
+ f"({self.dimension_types})"
+ )
- if values.get("dimension_types"):
- for dim_type, vector in zip(values.get("dimension_types", (None,) * 3), v):
- if None in vector and dim_type == Periodicity.PERIODIC.value:
- raise ValueError(
- f"Null entries in lattice vectors are only permitted when the corresponding dimension type is {Periodicity.APERIODIC.value}. "
- f"Here: dimension_types = {tuple(getattr(_, 'value', None) for _ in values.get('dimension_types', []))}, lattice_vectors = {v}"
- )
+ if self.lattice_vectors is not None:
+ if self.dimension_types:
+ for dim_type, vector in zip(self.dimension_types, self.lattice_vectors):
+ if None in vector and dim_type == Periodicity.PERIODIC.value:
+ raise ValueError(
+ f"Null entries in lattice vectors are only permitted when the "
+ "corresponding dimension type is "
+ f"{Periodicity.APERIODIC.value}. Here: dimension_types = "
+ f"{tuple(getattr(_, 'value', None) for _ in self.dimension_types)},"
+ f" lattice_vectors = {self.lattice_vectors}"
+ )
- return v
+ return self
- @validator("lattice_vectors")
- def null_values_for_whole_vector(cls, v):
- if v is None:
- return v
+ @field_validator("lattice_vectors", mode="after")
+ @classmethod
+ def null_values_for_whole_vector(
+ cls,
+ value: Optional[
+ Annotated[list[Vector3D_unknown], Field(min_length=3, max_length=3)]
+ ],
+ ) -> Optional[Annotated[list[Vector3D_unknown], Field(min_length=3, max_length=3)]]:
+ if value is None:
+ return value
- for vector in v:
- if None in vector and any((isinstance(_, float) for _ in vector)):
+ for vector in value:
+ if None in vector and any(isinstance(_, float) for _ in vector):
raise ValueError(
- f"A lattice vector MUST be either all `null` or all numbers (vector: {vector}, all vectors: {v})"
+ "A lattice vector MUST be either all `null` or all numbers "
+ f"(vector: {vector}, all vectors: {value})"
)
- return v
+ return value
- @validator("nsites")
- def validate_nsites(cls, v, values):
- if v is None:
- return v
+ @model_validator(mode="after")
+ def validate_nsites(self) -> "StructureResourceAttributes":
+ if self.nsites is None:
+ return self
- if values.get("cartesian_site_positions") and v != len(
- values.get("cartesian_site_positions", [])
+ if self.cartesian_site_positions and self.nsites != len(
+ self.cartesian_site_positions
):
raise ValueError(
- f"nsites (value: {v}) MUST equal length of cartesian_site_positions "
- f"(value: {len(values.get('cartesian_site_positions', []))})"
+ f"nsites (value: {self.nsites}) MUST equal length of "
+ "cartesian_site_positions (value: "
+ f"{len(self.cartesian_site_positions)})"
)
- return v
+ return self
- @validator("species_at_sites")
- def validate_species_at_sites(cls, v, values):
- if v is None:
- return v
+ @model_validator(mode="after")
+ def validate_species_at_sites(self) -> "StructureResourceAttributes":
+ if self.species_at_sites is None:
+ return self
- if values.get("nsites") and len(v) != values.get("nsites"):
+ if self.nsites and len(self.species_at_sites) != self.nsites:
raise ValueError(
- f"Number of species_at_sites (value: {len(v)}) MUST equal number of sites "
- f"(value: {values.get('nsites', 'Not specified')})"
+ f"Number of species_at_sites (value: {len(self.species_at_sites)}) "
+ f"MUST equal number of sites (value: {self.nsites})"
)
- if values.get("species"):
- all_species_names = {
- getattr(_, "name", None) for _ in values.get("species", [{}])
- }
- all_species_names -= {None}
- for value in v:
- if value not in all_species_names:
+
+ if self.species:
+ all_species_names = {_.name for _ in self.species}
+
+ for species_at_site in self.species_at_sites:
+ if species_at_site not in all_species_names:
raise ValueError(
"species_at_sites MUST be represented by a species' name, "
- f"but {value} was not found in the list of species names: {all_species_names}"
+ f"but {species_at_site} was not found in the list of species "
+ f"names: {all_species_names}"
)
- return v
- @validator("species")
- def validate_species(cls, v):
- if v is None:
- return v
+ return self
- all_species = [_.name for _ in v]
+ @field_validator("species", mode="after")
+ @classmethod
+ def validate_species(
+ cls, value: Optional[list[Species]]
+ ) -> Optional[list[Species]]:
+ if value is None:
+ return value
+
+ all_species = [_.name for _ in value]
unique_species = set(all_species)
if len(all_species) != len(unique_species):
raise ValueError(
f"Species MUST be unique based on their 'name'. Found species names: {all_species}"
)
- return v
+ return value
- @validator("structure_features", always=True)
- def validate_structure_features(cls, v, values):
- if [StructureFeatures(value) for value in sorted((_.value for _ in v))] != v:
+ @model_validator(mode="after")
+ def validate_structure_features(self) -> "StructureResourceAttributes":
+ if [
+ StructureFeatures(value)
+ for value in sorted(_.value for _ in self.structure_features)
+ ] != self.structure_features:
raise ValueError(
- f"structure_features MUST be sorted alphabetically, given value: {v}"
+ "structure_features MUST be sorted alphabetically, structure_features: "
+ f"{self.structure_features}"
)
# assemblies
- if values.get("assemblies") is not None:
- if StructureFeatures.ASSEMBLIES not in v:
+ if self.assemblies is not None:
+ if StructureFeatures.ASSEMBLIES not in self.structure_features:
raise ValueError(
- f"{StructureFeatures.ASSEMBLIES.value} MUST be present, since the property of the same name is present"
+ f"{StructureFeatures.ASSEMBLIES.value} MUST be present, since the "
+ "property of the same name is present"
)
- elif StructureFeatures.ASSEMBLIES in v:
+ elif StructureFeatures.ASSEMBLIES in self.structure_features:
raise ValueError(
f"{StructureFeatures.ASSEMBLIES.value} MUST NOT be present, "
"since the property of the same name is not present"
)
- if values.get("species"):
+ if self.species:
# disorder
- for species in values.get("species", []):
+ for species in self.species:
if len(species.chemical_symbols) > 1:
- if StructureFeatures.DISORDER not in v:
+ if StructureFeatures.DISORDER not in self.structure_features:
raise ValueError(
- f"{StructureFeatures.DISORDER.value} MUST be present when any one entry in species "
- "has a chemical_symbols list greater than one element"
+ f"{StructureFeatures.DISORDER.value} MUST be present when "
+ "any one entry in species has a chemical_symbols list "
+ "greater than one element"
)
break
else:
- if StructureFeatures.DISORDER in v:
+ if StructureFeatures.DISORDER in self.structure_features:
raise ValueError(
- f"{StructureFeatures.DISORDER.value} MUST NOT be present, since all species' chemical_symbols "
- "lists are equal to or less than one element"
+ f"{StructureFeatures.DISORDER.value} MUST NOT be present, "
+ "since all species' chemical_symbols lists are equal to or "
+ "less than one element"
)
+
# site_attachments
- for species in values.get("species", []):
+ for species in self.species:
# There is no need to also test "nattached",
# since a Species validator makes sure either both are present or both are None.
- if getattr(species, "attached", None) is not None:
- if StructureFeatures.SITE_ATTACHMENTS not in v:
+ if species.attached is not None:
+ if (
+ StructureFeatures.SITE_ATTACHMENTS
+ not in self.structure_features
+ ):
raise ValueError(
- f"{StructureFeatures.SITE_ATTACHMENTS.value} MUST be present when any one entry "
- "in species includes attached and nattached"
+ f"{StructureFeatures.SITE_ATTACHMENTS.value} MUST be "
+ "present when any one entry in species includes attached "
+ "and nattached"
)
break
else:
- if StructureFeatures.SITE_ATTACHMENTS in v:
+ if StructureFeatures.SITE_ATTACHMENTS in self.structure_features:
raise ValueError(
- f"{StructureFeatures.SITE_ATTACHMENTS.value} MUST NOT be present, since no species includes "
- "the attached and nattached fields"
+ f"{StructureFeatures.SITE_ATTACHMENTS.value} MUST NOT be "
+ "present, since no species includes the attached and nattached"
+ " fields"
)
+
# implicit_atoms
- species_names = [_.name for _ in values.get("species", [])]
- for name in species_names:
- if values.get(
- "species_at_sites"
- ) is not None and name not in values.get("species_at_sites", []):
- if StructureFeatures.IMPLICIT_ATOMS not in v:
+ for name in [_.name for _ in self.species]:
+ if (
+ self.species_at_sites is not None
+ and name not in self.species_at_sites
+ ):
+ if StructureFeatures.IMPLICIT_ATOMS not in self.structure_features:
raise ValueError(
- f"{StructureFeatures.IMPLICIT_ATOMS.value} MUST be present when any one entry in species "
- "is not represented in species_at_sites"
+ f"{StructureFeatures.IMPLICIT_ATOMS.value} MUST be present"
+ " when any one entry in species is not represented in "
+ "species_at_sites"
)
break
else:
- if StructureFeatures.IMPLICIT_ATOMS in v:
+ if StructureFeatures.IMPLICIT_ATOMS in self.structure_features:
raise ValueError(
- f"{StructureFeatures.IMPLICIT_ATOMS.value} MUST NOT be present, since all species are "
- "represented in species_at_sites"
+ f"{StructureFeatures.IMPLICIT_ATOMS.value} MUST NOT be "
+ "present, since all species are represented in species_at_sites"
)
- return v
+ return self
class StructureResource(EntryResource):
"""Representing a structure."""
- type: str = StrictField(
- "structures",
- description="""The name of the type of an entry.
+ type: Annotated[
+ Literal["structures"],
+ StrictField(
+ description="""The name of the type of an entry.
- **Type**: string.
@@ -1110,9 +1349,10 @@ class StructureResource(EntryResource):
- **Examples**:
- `"structures"`""",
- regex="^structures$",
- support=SupportLevel.MUST,
- queryable=SupportLevel.MUST,
- )
+ pattern="^structures$",
+ support=SupportLevel.MUST,
+ queryable=SupportLevel.MUST,
+ ),
+ ] = "structures"
attributes: StructureResourceAttributes
diff --git a/optimade/models/types.py b/optimade/models/types.py
new file mode 100644
index 000000000..b230a0fde
--- /dev/null
+++ b/optimade/models/types.py
@@ -0,0 +1,64 @@
+from typing import Annotated, Optional, Union, get_args
+
+from pydantic import Field
+
+from optimade.models.utils import (
+ ELEMENT_SYMBOLS_PATTERN,
+ EXTENDED_CHEMICAL_SYMBOLS_PATTERN,
+ SEMVER_PATTERN,
+)
+
+__all__ = ("ChemicalSymbol", "SemanticVersion")
+
+ChemicalSymbol = Annotated[str, Field(pattern=EXTENDED_CHEMICAL_SYMBOLS_PATTERN)]
+
+ElementSymbol = Annotated[str, Field(pattern=ELEMENT_SYMBOLS_PATTERN)]
+
+SemanticVersion = Annotated[
+ str,
+ Field(
+ pattern=SEMVER_PATTERN, examples=["0.10.1", "1.0.0-rc.2", "1.2.3-rc.5+develop"]
+ ),
+]
+
+AnnotatedType = type(ChemicalSymbol)
+OptionalType = type(Optional[str])
+UnionType = type(Union[str, int])
+NoneType = type(None)
+
+
+def _get_origin_type(annotation: type) -> type:
+ """Get the origin type of a type annotation.
+
+ Parameters:
+ annotation: The type annotation.
+
+ Returns:
+ The origin type.
+
+ """
+ # If the annotation is a Union, get the first non-None type (this includes
+ # Optional[T])
+ if isinstance(annotation, (OptionalType, UnionType)):
+ for arg in get_args(annotation):
+ if arg not in (None, NoneType):
+ annotation = arg
+ break
+
+ # If the annotation is an Annotated type, get the first type
+ if isinstance(annotation, AnnotatedType):
+ annotation = get_args(annotation)[0]
+
+ # Recursively unpack annotation, if it is a Union, Optional, or Annotated type
+ while isinstance(annotation, (OptionalType, UnionType, AnnotatedType)):
+ annotation = _get_origin_type(annotation)
+
+ # Special case for Literal
+ # NOTE: Expecting Literal arguments to all be of a single type
+ arg = get_args(annotation)
+ if arg and not isinstance(arg, type):
+ # Expect arg to be a Literal type argument
+ annotation = type(arg)
+
+ # Ensure that the annotation is a builtin type
+ return getattr(annotation, "__origin__", annotation)
diff --git a/optimade/models/utils.py b/optimade/models/utils.py
index 7e04bac07..a811d4a1e 100644
--- a/optimade/models/utils.py
+++ b/optimade/models/utils.py
@@ -5,14 +5,13 @@
import warnings
from enum import Enum
from functools import reduce
-from typing import TYPE_CHECKING, List, Optional, Union
+from typing import TYPE_CHECKING, Any, Optional, Union
from pydantic import Field
-from pydantic.fields import FieldInfo
+from pydantic_core import PydanticUndefined
if TYPE_CHECKING: # pragma: no cover
- from typing import Any
-
+ from collections.abc import Generator
_PYDANTIC_FIELD_KWARGS = list(inspect.signature(Field).parameters.keys())
@@ -20,13 +19,14 @@
"CHEMICAL_SYMBOLS",
"EXTRA_SYMBOLS",
"ATOMIC_NUMBERS",
- "SemanticVersion",
"SupportLevel",
)
OPTIMADE_SCHEMA_EXTENSION_KEYS = ["support", "queryable", "unit", "sortable"]
OPTIMADE_SCHEMA_EXTENSION_PREFIX = "x-optimade-"
+SEMVER_PATTERN = r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"
+
class SupportLevel(Enum):
"""OPTIMADE property/field support levels"""
@@ -36,35 +36,12 @@ class SupportLevel(Enum):
OPTIONAL = "optional"
-class StrictFieldInfo(FieldInfo):
- """Wraps the standard pydantic `FieldInfo` in order
- to prefix any custom keys from `StrictField`.
-
- """
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- for key in OPTIMADE_SCHEMA_EXTENSION_KEYS:
- if key in self.extra:
- self.extra[f"{OPTIMADE_SCHEMA_EXTENSION_PREFIX}{key}"] = self.extra.pop(
- key
- )
-
-
-def StrictPydanticField(*args, **kwargs):
- """Wrapper for `Field` that uses `StrictFieldInfo` instead of
- the pydantic `FieldInfo`.
- """
- field_info = StrictFieldInfo(*args, **kwargs)
- field_info._validate()
- return field_info
-
-
def StrictField(
- *args: "Any",
+ default: "Any" = PydanticUndefined,
+ *,
description: Optional[str] = None,
**kwargs: "Any",
-) -> StrictFieldInfo:
+) -> Any:
"""A wrapper around `pydantic.Field` that does the following:
- Forbids any "extra" keys that would be passed to `pydantic.Field`,
@@ -74,7 +51,7 @@ def StrictField(
- Emits a warning when no description is provided.
Arguments:
- *args: Positional arguments passed through to `Field`.
+ default: The only non-keyword argument allowed for Field.
description: The description of the `Field`; if this is not
specified then a `UserWarning` will be emitted.
**kwargs: Extra keyword arguments to be passed to `Field`.
@@ -88,37 +65,68 @@ def StrictField(
The pydantic `Field`.
"""
+ allowed_schema_and_field_keys = ["pattern"]
allowed_keys = [
"pattern",
"uniqueItems",
- "nullable",
] + OPTIMADE_SCHEMA_EXTENSION_KEYS
_banned = [k for k in kwargs if k not in set(_PYDANTIC_FIELD_KWARGS + allowed_keys)]
if _banned:
raise RuntimeError(
- f"Not creating StrictField({args}, {kwargs}) with forbidden keywords {_banned}."
+ f"Not creating StrictField({default!r}, **{kwargs!r}) with "
+ f"forbidden keywords {_banned}."
)
- if description is not None:
- kwargs["description"] = description
-
+ # Handle description
if description is None:
warnings.warn(
- f"No description provided for StrictField specified by {args}, {kwargs}."
+ f"No description provided for StrictField specified by {default!r}, "
+ f"**{kwargs!r}."
)
+ else:
+ kwargs["description"] = description
- return StrictPydanticField(*args, **kwargs)
+ # OPTIMADE schema extensions
+ json_schema_extra: dict[str, Any] = kwargs.pop("json_schema_extra", {})
+
+ # Go through all JSON Schema keys and add them to the json_schema_extra.
+ for key in allowed_keys:
+ if key not in kwargs:
+ continue
+
+ # If they are OPTIMADE schema extensions, add them with the OPTIMADE prefix.
+ schema_key = (
+ f"{OPTIMADE_SCHEMA_EXTENSION_PREFIX}{key}"
+ if key in OPTIMADE_SCHEMA_EXTENSION_KEYS
+ else key
+ )
+
+ for key_variant in (key, schema_key):
+ if key_variant in json_schema_extra:
+ if json_schema_extra.pop(key_variant) != kwargs[key]:
+ raise RuntimeError(
+ f"Conflicting values for {key} in json_schema_extra and kwargs."
+ )
+
+ json_schema_extra[schema_key] = (
+ kwargs[key] if key in allowed_schema_and_field_keys else kwargs.pop(key)
+ )
+
+ kwargs["json_schema_extra"] = json_schema_extra
+
+ return Field(default, **kwargs)
def OptimadeField(
- *args,
+ default: "Any" = PydanticUndefined,
+ *,
support: Optional[Union[str, SupportLevel]] = None,
queryable: Optional[Union[str, SupportLevel]] = None,
unit: Optional[str] = None,
**kwargs,
-) -> Field:
+) -> Any:
"""A wrapper around `pydantic.Field` that adds OPTIMADE-specific
field paramters `queryable`, `support` and `unit`, indicating
the corresponding support level in the specification and the
@@ -139,110 +147,43 @@ def OptimadeField(
# Collect non-null keyword arguments to add to the Field schema
if unit is not None:
kwargs["unit"] = unit
+
if queryable is not None:
if isinstance(queryable, str):
queryable = SupportLevel(queryable.lower())
kwargs["queryable"] = queryable
+
if support is not None:
if isinstance(support, str):
support = SupportLevel(support.lower())
kwargs["support"] = support
- return StrictField(*args, **kwargs)
-
+ return StrictField(default, **kwargs)
-class SemanticVersion(str):
- """A custom type for a semantic version, using the recommended
- semver regexp from
- https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string.
-
- """
- regex = re.compile(
- r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"
- )
-
- @classmethod
- def __get_validators__(cls):
- yield cls.validate
-
- @classmethod
- def __modify_schema__(cls, field_schema):
- field_schema.update(
- pattern=cls.regex.pattern,
- example=["0.10.1", "1.0.0-rc.2", "1.2.3-rc.5+develop"],
- )
-
- @classmethod
- def validate(cls, v: str):
- if not cls.regex.match(v):
- raise ValueError(
- f"Unable to validate the version string {v!r} as a semantic version (expected ..)."
- "See https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string for more information."
- )
-
- return v
-
- @property
- def _match(self):
- """The result of the regex match."""
- return self.regex.match(self)
-
- @property
- def major(self) -> int:
- """The major version number."""
- return int(self._match.group(1))
-
- @property
- def minor(self) -> int:
- """The minor version number."""
- return int(self._match.group(2))
-
- @property
- def patch(self) -> int:
- """The patch version number."""
- return int(self._match.group(3))
-
- @property
- def prerelease(self) -> str:
- """The pre-release tag."""
- return self._match.group(4)
-
- @property
- def build_metadata(self) -> str:
- """The build metadata."""
- return self._match.group(5)
-
- @property
- def base_version(self) -> str:
- """The base version string without patch and metadata info."""
- return f"{self.major}.{self.minor}.{self.patch}"
-
-
-def anonymous_element_generator():
+def anonymous_element_generator() -> "Generator[str, None, None]":
"""Generator that yields the next symbol in the A, B, Aa, ... Az naming scheme."""
from string import ascii_lowercase
for size in itertools.count(1):
- for s in itertools.product(ascii_lowercase, repeat=size):
- s = list(s)
- s[0] = s[0].upper()
- yield "".join(s)
+ for tuple_strings in itertools.product(ascii_lowercase, repeat=size):
+ list_strings = list(tuple_strings)
+ list_strings[0] = list_strings[0].upper()
+ yield "".join(list_strings)
def _reduce_or_anonymize_formula(
formula: str, alphabetize: bool = True, anonymize: bool = False
) -> str:
"""Takes an input formula, reduces it and either alphabetizes or anonymizes it."""
- import re
import sys
- numbers: List[int] = [
+ numbers: list[int] = [
int(n.strip() or 1) for n in re.split(r"[A-Z][a-z]*", formula)[1:]
]
# Need to remove leading 1 from split and convert to ints
- species = re.findall("[A-Z][a-z]*", formula)
+ species: list[str] = re.findall("[A-Z][a-z]*", formula)
if sys.version_info[1] >= 9:
gcd = math.gcd(*numbers)
@@ -259,7 +200,7 @@ def _reduce_or_anonymize_formula(
species = [s for _, s in zip(numbers, anonymous_element_generator())]
elif alphabetize:
- species, numbers = zip(*sorted(zip(species, numbers)))
+ species, numbers = zip(*sorted(zip(species, numbers))) # type: ignore[assignment]
return "".join(f"{s}{n if n != 1 else ''}" for n, s in zip(numbers, species))
@@ -418,3 +359,15 @@ def reduce_formula(formula: str) -> str:
ATOMIC_NUMBERS = {}
for Z, symbol in enumerate(CHEMICAL_SYMBOLS):
ATOMIC_NUMBERS[symbol] = Z + 1
+
+EXTENDED_CHEMICAL_SYMBOLS_PATTERN = (
+ "(" + "|".join(CHEMICAL_SYMBOLS + EXTRA_SYMBOLS) + ")"
+)
+
+ELEMENT_SYMBOLS_PATTERN = "(" + "|".join(CHEMICAL_SYMBOLS) + ")"
+
+translation = "1/2|[12]/3|[1-3]/4|[1-5]/6"
+translation_appended = f"[-+]? [xyz] ([-+][xyz])? ([-+] ({translation}) )?"
+translation_prepended = f"[-+]? ({translation}) ([-+] [xyz] ([-+][xyz])? )?"
+symop = f"({translation_appended}|{translation_prepended})".replace(" ", "")
+SPACE_GROUP_SYMMETRY_OPERATION_REGEX = f"^{symop},{symop},{symop}$"
diff --git a/optimade/server/config.py b/optimade/server/config.py
index 3a3e7cea7..0ea08099e 100644
--- a/optimade/server/config.py
+++ b/optimade/server/config.py
@@ -1,17 +1,18 @@
-# pylint: disable=no-self-argument
+import json
+import os
import warnings
from enum import Enum
from pathlib import Path
-from typing import Any, Dict, List, Literal, Optional, Tuple, Union
+from typing import Annotated, Any, Literal, Optional, Union
-from pydantic import ( # pylint: disable=no-name-in-module
- AnyHttpUrl,
+import yaml
+from pydantic import AnyHttpUrl, Field, field_validator, model_validator
+from pydantic.fields import FieldInfo
+from pydantic_settings import (
BaseSettings,
- Field,
- root_validator,
- validator,
+ PydanticBaseSettingsSource,
+ SettingsConfigDict,
)
-from pydantic.env_settings import SettingsSourceCallable
from optimade import __api_version__, __version__
from optimade.models import Implementation, Provider
@@ -19,8 +20,8 @@
DEFAULT_CONFIG_FILE_PATH: str = str(Path.home().joinpath(".optimade.json"))
"""Default configuration file path.
-This variable is used as the fallback value if the environment variable `OPTIMADE_CONFIG_FILE` is
-not set.
+This variable is used as the fallback value if the environment variable
+`OPTIMADE_CONFIG_FILE` is not set.
!!! note
It is set to: `pathlib.Path.home()/.optimade.json`
@@ -56,9 +57,10 @@ class SupportedBackend(Enum):
- `elastic`: [Elasticsearch](https://www.elastic.co/).
- `mongodb`: [MongoDB](https://www.mongodb.com/).
- `mongomock`: Also MongoDB, but instead of using the
- [`pymongo`](https://pymongo.readthedocs.io/) driver to connect to a live Mongo database
- instance, this will use the [`mongomock`](https://github.com/mongomock/mongomock) driver,
- creating an in-memory database, which is mainly used for testing.
+ [`pymongo`](https://pymongo.readthedocs.io/) driver to connect to a live Mongo
+ database instance, this will use the
+ [`mongomock`](https://github.com/mongomock/mongomock) driver, creating an
+ in-memory database, which is mainly used for testing.
"""
@@ -67,262 +69,372 @@ class SupportedBackend(Enum):
MONGOMOCK = "mongomock"
-def config_file_settings(settings: BaseSettings) -> Dict[str, Any]:
+class ConfigFileSettingsSource(PydanticBaseSettingsSource):
"""Configuration file settings source.
Based on the example in the
- [pydantic documentation](https://pydantic-docs.helpmanual.io/usage/settings/#adding-sources),
- this function loads ServerConfig settings from a configuration file.
+ [pydantic documentation](https://docs.pydantic.dev/latest/concepts/pydantic_settings/#customise-settings-sources),
+ this class defines loading ServerConfig settings from a configuration file.
The file must be of either type JSON or YML/YAML.
+ """
- Parameters:
- settings: The `pydantic.BaseSettings` class using this function as a
- `pydantic.SettingsSourceCallable`.
+ def get_field_value(
+ self, field: FieldInfo, field_name: str
+ ) -> tuple[Any, str, bool]:
+ """Must be defined according to parent abstract class.
- Returns:
- Dictionary of settings as read from a file.
+ It does not make sense to use it for this class, since 'extra' is set to
+ 'allow'. We will instead just parse and take every key/field specified in the
+ config file.
+ """
+ raise NotImplementedError
- """
- import json
- import os
+ def parse_config_file(self) -> dict[str, Any]:
+ """Parse the config file and return a dictionary of its content."""
+ encoding = self.config.get("env_file_encoding")
+ config_file = Path(os.getenv("OPTIMADE_CONFIG_FILE", DEFAULT_CONFIG_FILE_PATH))
- import yaml
+ parsed_config_file = {}
+ if config_file.is_file():
+ config_file_content = config_file.read_text(encoding=encoding)
- encoding = settings.__config__.env_file_encoding
- config_file = Path(os.getenv("OPTIMADE_CONFIG_FILE", DEFAULT_CONFIG_FILE_PATH))
+ try:
+ parsed_config_file = json.loads(config_file_content)
+ except json.JSONDecodeError as json_exc:
+ try:
+ # This can essentially also load JSON files, as JSON is a subset of
+ # YAML v1, but I suspect it is not as rigorous
+ parsed_config_file = yaml.safe_load(config_file_content)
+ except yaml.YAMLError as yaml_exc:
+ warnings.warn(
+ f"Unable to parse config file {config_file} as JSON or "
+ "YAML, using the default settings instead..\n"
+ f"Errors:\n JSON:\n{json_exc}.\n\n YAML:\n{yaml_exc}"
+ )
+ else:
+ warnings.warn(
+ f"Unable to find config file at {config_file}, using the default "
+ "settings instead."
+ )
- res = {}
- if config_file.is_file():
- config_file_content = config_file.read_text(encoding=encoding)
+ if parsed_config_file is None:
+ # This can happen if the yaml loading doesn't succeed properly, e.g., if
+ # the file is empty.
+ warnings.warn(
+ f"Unable to load any settings from {config_file}, using the default "
+ "settings instead."
+ )
+ parsed_config_file = {}
- try:
- res = json.loads(config_file_content)
- except json.JSONDecodeError as json_exc:
- try:
- # This can essentially also load JSON files, as JSON is a subset of YAML v1,
- # but I suspect it is not as rigorous
- res = yaml.safe_load(config_file_content)
- except yaml.YAMLError as yaml_exc:
- warnings.warn(
- f"Unable to parse config file {config_file} as JSON or YAML, using the "
- "default settings instead..\n"
- f"Errors:\n JSON:\n{json_exc}.\n\n YAML:\n{yaml_exc}"
- )
- else:
- warnings.warn(
- f"Unable to find config file at {config_file}, using the default settings instead."
- )
+ if not isinstance(parsed_config_file, dict):
+ warnings.warn(
+ f"Unable to parse config file {config_file} as a dictionary, using "
+ "the default settings instead."
+ )
+ parsed_config_file = {}
- if res is None:
- # This can happen if the yaml loading doesn't succeed properly, e.g., if the file is empty.
- warnings.warn(
- "Unable to load any settings from {config_file}, using the default settings instead."
- )
- res = {}
+ return parsed_config_file
- return res
+ def __call__(self) -> dict[str, Any]:
+ return self.parse_config_file()
class ServerConfig(BaseSettings):
"""This class stores server config parameters in a way that
can be easily extended for new config file types.
-
"""
- debug: bool = Field(
- False,
- description="Turns on Debug Mode for the OPTIMADE Server implementation",
+ model_config = SettingsConfigDict(
+ env_prefix="optimade_",
+ extra="allow",
+ env_file_encoding="utf-8",
+ case_sensitive=False,
)
- insert_test_data: bool = Field(
- True,
- description=(
- "Insert test data into each collection on server initialisation. If true, the "
- "configured backend will be populated with test data on server start. Should be "
- "disabled for production usage."
+ debug: Annotated[
+ bool,
+ Field(
+ description="Turns on Debug Mode for the OPTIMADE Server implementation",
),
- )
-
- use_real_mongo: Optional[bool] = Field(
- None, description="DEPRECATED: force usage of MongoDB over any other backend."
- )
+ ] = False
+
+ insert_test_data: Annotated[
+ bool,
+ Field(
+ description=(
+ "Insert test data into each collection on server initialisation. If true, "
+ "the configured backend will be populated with test data on server start. "
+ "Should be disabled for production usage."
+ ),
+ ),
+ ] = True
- database_backend: SupportedBackend = Field(
- SupportedBackend.MONGOMOCK,
- description="Which database backend to use out of the supported backends.",
- )
+ use_real_mongo: Annotated[
+ Optional[bool],
+ Field(description="DEPRECATED: force usage of MongoDB over any other backend."),
+ ] = None
- elastic_hosts: Optional[Union[str, List[str], Dict, List[Dict]]] = Field(
- None, description="Host settings to pass through to the `Elasticsearch` class."
- )
+ database_backend: Annotated[
+ SupportedBackend,
+ Field(
+ description="Which database backend to use out of the supported backends.",
+ ),
+ ] = SupportedBackend.MONGOMOCK
- mongo_count_timeout: int = Field(
- 5,
- description="""Number of seconds to allow MongoDB to perform a full database count before falling back to `null`.
-This operation can require a full COLLSCAN for empty queries which can be prohibitively slow if the database does not fit into the active set, hence a timeout can drastically speed-up response times.""",
- )
+ elastic_hosts: Annotated[
+ Optional[Union[str, list[str], dict[str, Any], list[dict[str, Any]]]],
+ Field(
+ description="Host settings to pass through to the `Elasticsearch` class."
+ ),
+ ] = None
+
+ mongo_count_timeout: Annotated[
+ int,
+ Field(
+ description=(
+ "Number of seconds to allow MongoDB to perform a full database count "
+ "before falling back to `null`. This operation can require a full COLLSCAN"
+ " for empty queries which can be prohibitively slow if the database does "
+ "not fit into the active set, hence a timeout can drastically speed-up "
+ "response times."
+ ),
+ ),
+ ] = 5
- mongo_database: str = Field(
- "optimade", description="Mongo database for collection data"
- )
- mongo_uri: str = Field("localhost:27017", description="URI for the Mongo server")
- links_collection: str = Field(
- "links", description="Mongo collection name for /links endpoint resources"
- )
- references_collection: str = Field(
- "references",
- description="Mongo collection name for /references endpoint resources",
- )
- structures_collection: str = Field(
- "structures",
- description="Mongo collection name for /structures endpoint resources",
- )
- page_limit: int = Field(20, description="Default number of resources per page")
- page_limit_max: int = Field(
- 500, description="Max allowed number of resources per page"
+ mongo_database: Annotated[
+ str, Field(description="Mongo database for collection data")
+ ] = "optimade"
+ mongo_uri: Annotated[str, Field(description="URI for the Mongo server")] = (
+ "localhost:27017"
)
- default_db: str = Field(
- "test_server",
- description=(
- "ID of /links endpoint resource for the chosen default OPTIMADE implementation (only "
- "relevant for the index meta-database)"
+ links_collection: Annotated[
+ str, Field(description="Mongo collection name for /links endpoint resources")
+ ] = "links"
+ references_collection: Annotated[
+ str,
+ Field(
+ description="Mongo collection name for /references endpoint resources",
),
- )
- root_path: Optional[str] = Field(
- None,
- description=(
- "Sets the FastAPI app `root_path` parameter. This can be used to serve the API under a"
- " path prefix behind a proxy or as a sub-application of another FastAPI app. See "
- "https://fastapi.tiangolo.com/advanced/sub-applications/#technical-details-root_path "
- "for details."
+ ] = "references"
+ structures_collection: Annotated[
+ str,
+ Field(
+ description="Mongo collection name for /structures endpoint resources",
),
- )
- base_url: Optional[str] = Field(
- None, description="Base URL for this implementation"
- )
- implementation: Implementation = Field(
- Implementation(
- name="OPTIMADE Python Tools",
- version=__version__,
- source_url="https://github.com/Materials-Consortia/optimade-python-tools",
- maintainer={"email": "dev@optimade.org"},
- issue_tracker="https://github.com/Materials-Consortia/optimade-python-tools/issues",
+ ] = "structures"
+ page_limit: Annotated[
+ int, Field(description="Default number of resources per page")
+ ] = 20
+ page_limit_max: Annotated[
+ int, Field(description="Max allowed number of resources per page")
+ ] = 500
+ default_db: Annotated[
+ str,
+ Field(
+ description=(
+ "ID of /links endpoint resource for the chosen default OPTIMADE "
+ "implementation (only relevant for the index meta-database)"
+ ),
),
- description="Introspective information about this OPTIMADE implementation",
- )
- index_base_url: Optional[AnyHttpUrl] = Field(
- None,
- description="An optional link to the base URL for the index meta-database of the provider.",
- )
- provider: Provider = Field(
- Provider(
- prefix="exmpl",
- name="Example provider",
- description="Provider used for examples, not to be assigned to a real database",
- homepage="https://example.com",
+ ] = "test_server"
+ root_path: Annotated[
+ Optional[str],
+ Field(
+ description=(
+ "Sets the FastAPI app `root_path` parameter. This can be used to serve the"
+ " API under a path prefix behind a proxy or as a sub-application of "
+ "another FastAPI app. See "
+ "https://fastapi.tiangolo.com/advanced/sub-applications/#technical-details-root_path"
+ " for details."
+ ),
),
- description="General information about the provider of this OPTIMADE implementation",
- )
- provider_fields: Dict[
- Literal["links", "references", "structures"],
- List[Union[str, Dict[Literal["name", "type", "unit", "description"], str]]],
- ] = Field(
- {},
- description=(
- "A list of additional fields to be served with the provider's prefix attached, "
- "broken down by endpoint."
+ ] = None
+ base_url: Annotated[
+ Optional[str], Field(description="Base URL for this implementation")
+ ] = None
+ implementation: Annotated[
+ Implementation,
+ Field(
+ description="Introspective information about this OPTIMADE implementation",
),
- )
- aliases: Dict[Literal["links", "references", "structures"], Dict[str, str]] = Field(
- {},
- description=(
- "A mapping between field names in the database with their corresponding OPTIMADE field"
- " names, broken down by endpoint."
+ ] = Implementation(
+ name="OPTIMADE Python Tools",
+ version=__version__,
+ source_url="https://github.com/Materials-Consortia/optimade-python-tools",
+ maintainer={"email": "dev@optimade.org"},
+ issue_tracker=(
+ "https://github.com/Materials-Consortia/optimade-python-tools/issues"
),
+ homepage="https://optimade.org/optimade-python-tools",
)
- length_aliases: Dict[
- Literal["links", "references", "structures"], Dict[str, str]
- ] = Field(
- {},
- description=(
- "A mapping between a list property (or otherwise) and an integer property that defines"
- " the length of that list, for example elements -> nelements. The standard aliases are"
- " applied first, so this dictionary must refer to the API fields, not the database "
- "fields."
+ index_base_url: Annotated[
+ Optional[AnyHttpUrl],
+ Field(
+ description=(
+ "An optional link to the base URL for the index meta-database of the "
+ "provider."
+ ),
),
- )
- index_links_path: Path = Field(
- Path(__file__).parent.joinpath("index_links.json"),
- description=(
- "Absolute path to a JSON file containing the MongoDB collecton of links entries "
- "(documents) to serve under the /links endpoint of the index meta-database. "
- "NB! As suggested in the previous sentence, these will only be served when using a "
- "MongoDB-based backend."
+ ] = None
+ provider: Annotated[
+ Provider,
+ Field(
+ description=(
+ "General information about the provider of this OPTIMADE implementation"
+ ),
),
- )
-
- is_index: Optional[bool] = Field(
- False,
+ ] = Provider(
+ prefix="exmpl",
+ name="Example provider",
description=(
- "A runtime setting to dynamically switch between index meta-database and "
- "normal OPTIMADE servers. Used for switching behaviour of e.g., `meta->optimade_schema` "
- "values in the response. Any provided value may be overridden when used with the reference "
- "server implementation."
+ "Provider used for examples, not to be assigned to a real database"
),
+ homepage="https://example.com",
)
-
- schema_url: Optional[Union[str, AnyHttpUrl]] = Field(
- f"https://schemas.optimade.org/openapi/v{__api_version__}/optimade.json",
- description=(
- "A URL that will be provided in the `meta->schema` field for every response"
+ provider_fields: Annotated[
+ dict[
+ Literal["links", "references", "structures"],
+ list[Union[str, dict[Literal["name", "type", "unit", "description"], str]]],
+ ],
+ Field(
+ description=(
+ "A list of additional fields to be served with the provider's prefix "
+ "attached, broken down by endpoint."
+ ),
),
- )
-
- custom_landing_page: Optional[Union[str, Path]] = Field(
- None,
- description="The location of a custom landing page (Jinja template) to use for the API.",
- )
-
- index_schema_url: Optional[Union[str, AnyHttpUrl]] = Field(
- f"https://schemas.optimade.org/openapi/v{__api_version__}/optimade_index.json",
- description=(
- "A URL that will be provided in the `meta->schema` field for every response from the index meta-database."
+ ] = {}
+ aliases: Annotated[
+ dict[Literal["links", "references", "structures"], dict[str, str]],
+ Field(
+ description=(
+ "A mapping between field names in the database with their corresponding "
+ "OPTIMADE field names, broken down by endpoint."
+ ),
),
- )
-
- log_level: LogLevel = Field(
- LogLevel.INFO, description="Logging level for the OPTIMADE server."
- )
- log_dir: Path = Field(
- Path("/var/log/optimade/"),
- description="Folder in which log files will be saved.",
- )
- validate_query_parameters: Optional[bool] = Field(
- True,
- description="If True, the server will check whether the query parameters given in the request are correct.",
- )
-
- validate_api_response: Optional[bool] = Field(
- True,
- description="""If False, data from the database will not undergo validation before being emitted by the API, and
- only the mapping of aliases will occur.""",
- )
+ ] = {}
+ length_aliases: Annotated[
+ dict[Literal["links", "references", "structures"], dict[str, str]],
+ Field(
+ description=(
+ "A mapping between a list property (or otherwise) and an integer property "
+ "that defines the length of that list, for example elements -> nelements. "
+ "The standard aliases are applied first, so this dictionary must refer to "
+ "the API fields, not the database fields."
+ ),
+ ),
+ ] = {}
+ index_links_path: Annotated[
+ Path,
+ Field(
+ description=(
+ "Absolute path to a JSON file containing the MongoDB collecton of links "
+ "entries (documents) to serve under the /links endpoint of the index "
+ "meta-database. NB! As suggested in the previous sentence, these will "
+ "only be served when using a MongoDB-based backend."
+ ),
+ ),
+ ] = Path(__file__).parent.joinpath("index_links.json")
+
+ is_index: Annotated[
+ Optional[bool],
+ Field(
+ description=(
+ "A runtime setting to dynamically switch between index meta-database and "
+ "normal OPTIMADE servers. Used for switching behaviour of e.g., "
+ "`meta->optimade_schema` values in the response. Any provided value may "
+ "be overridden when used with the reference server implementation."
+ ),
+ ),
+ ] = False
+
+ schema_url: Annotated[
+ Optional[Union[str, AnyHttpUrl]],
+ Field(
+ description=(
+ "A URL that will be provided in the `meta->schema` field for every response"
+ ),
+ ),
+ ] = f"https://schemas.optimade.org/openapi/v{__api_version__}/optimade.json"
+
+ custom_landing_page: Annotated[
+ Optional[Union[str, Path]],
+ Field(
+ description=(
+ "The location of a custom landing page (Jinja template) to use for the API."
+ ),
+ ),
+ ] = None
+
+ index_schema_url: Annotated[
+ Optional[Union[str, AnyHttpUrl]],
+ Field(
+ description=(
+ "A URL that will be provided in the `meta->schema` field for every "
+ "response from the index meta-database."
+ ),
+ ),
+ ] = f"https://schemas.optimade.org/openapi/v{__api_version__}/optimade_index.json"
+
+ log_level: Annotated[
+ LogLevel, Field(description="Logging level for the OPTIMADE server.")
+ ] = LogLevel.INFO
+ log_dir: Annotated[
+ Path,
+ Field(
+ description="Folder in which log files will be saved.",
+ ),
+ ] = Path("/var/log/optimade/")
+ validate_query_parameters: Annotated[
+ Optional[bool],
+ Field(
+ description=(
+ "If True, the server will check whether the query parameters given in the "
+ "request are correct."
+ ),
+ ),
+ ] = True
+
+ validate_api_response: Annotated[
+ Optional[bool],
+ Field(
+ description=(
+ "If False, data from the database will not undergo validation before being"
+ " emitted by the API, and only the mapping of aliases will occur."
+ ),
+ ),
+ ] = True
- @validator("implementation", pre=True)
- def set_implementation_version(cls, v):
+ @field_validator("implementation", mode="before")
+ @classmethod
+ def set_implementation_version(cls, value: Any) -> dict[str, Any]:
"""Set defaults and modify bypassed value(s)"""
+ if not isinstance(value, dict):
+ if isinstance(value, Implementation):
+ value = value.model_dump()
+ else:
+ raise TypeError(
+ f"Expected a dict or Implementation instance, got {type(value)}"
+ )
+
res = {"version": __version__}
- res.update(v)
+ res.update(value)
return res
- @root_validator(pre=True)
- def use_real_mongo_override(cls, values):
+ @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 = values.pop("use_real_mongo", None)
+ 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' "
@@ -331,57 +443,46 @@ def use_real_mongo_override(cls, values):
)
if use_real_mongo:
- values["database_backend"] = SupportedBackend.MONGODB
-
- return values
-
- class Config:
+ self.database_backend = SupportedBackend.MONGODB
+
+ return self
+
+ @classmethod
+ def settings_customise_sources(
+ cls,
+ settings_cls: type[BaseSettings],
+ init_settings: PydanticBaseSettingsSource,
+ env_settings: PydanticBaseSettingsSource,
+ dotenv_settings: PydanticBaseSettingsSource,
+ file_secret_settings: PydanticBaseSettingsSource,
+ ) -> tuple[PydanticBaseSettingsSource, ...]:
"""
- This is a pydantic model Config object that modifies the behaviour of
- ServerConfig by adding a prefix to the environment variables that
- override config file values. It has nothing to do with the OPTIMADE
- config.
+ **Priority of config settings sources**:
+
+ 1. Passed arguments upon initialization of
+ [`ServerConfig`][optimade.server.config.ServerConfig].
+ 2. Environment variables, matching the syntax: `"OPTIMADE_"` or `"optimade_"` +
+ ``, e.g., `OPTIMADE_LOG_LEVEL=debug` or
+ `optimade_log_dir=~/logs_dir/optimade/`.
+ 3. Configuration file (JSON/YAML) taken from:
+ 1. Environment variable `OPTIMADE_CONFIG_FILE`.
+ 2. Default location (see
+ [DEFAULT_CONFIG_FILE_PATH][optimade.server.config.DEFAULT_CONFIG_FILE_PATH]).
+ 4. Settings from secret file (see
+ [pydantic documentation](https://pydantic-docs.helpmanual.io/usage/settings/#secret-support)
+ for more information).
"""
-
- env_prefix = "optimade_"
- extra = "allow"
- env_file_encoding = "utf-8"
-
- @classmethod
- def customise_sources(
- cls,
- init_settings: SettingsSourceCallable,
- env_settings: SettingsSourceCallable,
- file_secret_settings: SettingsSourceCallable,
- ) -> Tuple[SettingsSourceCallable, ...]:
- """
- **Priority of config settings sources**:
-
- 1. Passed arguments upon initialization of
- [`ServerConfig`][optimade.server.config.ServerConfig].
- 2. Environment variables, matching the syntax: `"OPTIMADE_"` or `"optimade_"` +
- ``, e.g., `OPTIMADE_LOG_LEVEL=debug` or
- `optimade_log_dir=~/logs_dir/optimade/`.
- 3. Configuration file (JSON/YAML) taken from:
- 1. Environment variable `OPTIMADE_CONFIG_FILE`.
- 2. Default location (see
- [DEFAULT_CONFIG_FILE_PATH][optimade.server.config.DEFAULT_CONFIG_FILE_PATH]).
- 4. Settings from secret file (see
- [pydantic documentation](https://pydantic-docs.helpmanual.io/usage/settings/#secret-support)
- for more information).
-
- """
- return (
- init_settings,
- env_settings,
- config_file_settings,
- file_secret_settings,
- )
+ return (
+ init_settings,
+ env_settings,
+ 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.Config.customise_sources])
+[`customise_sources`][optimade.server.config.ServerConfig.settings_customise_sources])
and makes it importable in the server code.
"""
diff --git a/optimade/server/data/__init__.py b/optimade/server/data/__init__.py
index 87060d387..c659845b9 100644
--- a/optimade/server/data/__init__.py
+++ b/optimade/server/data/__init__.py
@@ -1,4 +1,5 @@
-""" Test Data to be used with the OPTIMADE server """
+"""Test Data to be used with the OPTIMADE server"""
+
from pathlib import Path
import bson.json_util
diff --git a/optimade/server/entry_collections/elasticsearch.py b/optimade/server/entry_collections/elasticsearch.py
index d14ad6499..7b0fffc7e 100644
--- a/optimade/server/entry_collections/elasticsearch.py
+++ b/optimade/server/entry_collections/elasticsearch.py
@@ -1,6 +1,7 @@
import json
+from collections.abc import Iterable
from pathlib import Path
-from typing import Any, Dict, Iterable, List, Optional, Tuple, Type
+from typing import Any, Optional
from optimade.filtertransformers.elasticsearch import ElasticTransformer
from optimade.models import EntryResource
@@ -24,8 +25,8 @@ class ElasticCollection(EntryCollection):
def __init__(
self,
name: str,
- resource_cls: Type[EntryResource],
- resource_mapper: Type[BaseResourceMapper],
+ resource_cls: type[EntryResource],
+ resource_mapper: type[BaseResourceMapper],
client: Optional["Elasticsearch"] = None,
):
"""Initialize the ElasticCollection for the given parameters.
@@ -78,7 +79,7 @@ def create_optimade_index(self) -> None:
LOGGER.debug(f"Created Elastic index for {self.name!r} with parameters {body}")
@property
- def predefined_index(self) -> Dict[str, Any]:
+ def predefined_index(self) -> dict[str, Any]:
"""Loads and returns the default pre-defined index."""
with open(Path(__file__).parent.joinpath("elastic_indexes.json")) as f:
index = json.load(f)
@@ -86,8 +87,8 @@ def predefined_index(self) -> Dict[str, Any]:
@staticmethod
def create_elastic_index_from_mapper(
- resource_mapper: Type[BaseResourceMapper], fields: Iterable[str]
- ) -> Dict[str, Any]:
+ resource_mapper: type[BaseResourceMapper], fields: Iterable[str]
+ ) -> dict[str, Any]:
"""Create a fallback elastic index based on a resource mapper.
Arguments:
@@ -110,7 +111,7 @@ def __len__(self):
"""Returns the total number of entries in the collection."""
return Search(using=self.client, index=self.name).execute().hits.total.value
- def insert(self, data: List[EntryResource]) -> None:
+ def insert(self, data: list[EntryResource]) -> None:
"""Add the given entries to the underlying database.
Warning:
@@ -123,7 +124,7 @@ def insert(self, data: List[EntryResource]) -> None:
def get_id(item):
if self.name == "links":
- id_ = "%s-%s" % (item["id"], item["type"])
+ id_ = f"{item['id']}-{item['type']}"
elif "id" in item:
id_ = item["id"]
elif "_id" in item:
@@ -148,8 +149,8 @@ def get_id(item):
)
def _run_db_query(
- self, criteria: Dict[str, Any], single_entry=False
- ) -> Tuple[List[Dict[str, Any]], int, bool]:
+ self, criteria: dict[str, Any], single_entry=False
+ ) -> tuple[list[dict[str, Any]], int, bool]:
"""Run the query on the backend and collect the results.
Arguments:
diff --git a/optimade/server/entry_collections/entry_collections.py b/optimade/server/entry_collections/entry_collections.py
index d59bb8713..342fa03a3 100644
--- a/optimade/server/entry_collections/entry_collections.py
+++ b/optimade/server/entry_collections/entry_collections.py
@@ -2,13 +2,15 @@
import re
import warnings
from abc import ABC, abstractmethod
-from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Type, Union
+from collections.abc import Iterable
+from typing import Any, Optional, Union
from lark import Transformer
from optimade.exceptions import BadRequest, Forbidden, NotFound
from optimade.filterparser import LarkParser
-from optimade.models.entries import EntryResource
+from optimade.models import Attributes, EntryResource
+from optimade.models.types import NoneType, _get_origin_type
from optimade.server.config import CONFIG, SupportedBackend
from optimade.server.mappers import BaseResourceMapper
from optimade.server.query_params import EntryListingQueryParams, SingleEntryQueryParams
@@ -21,8 +23,8 @@
def create_collection(
name: str,
- resource_cls: Type[EntryResource],
- resource_mapper: Type[BaseResourceMapper],
+ resource_cls: type[EntryResource],
+ resource_mapper: type[BaseResourceMapper],
) -> "EntryCollection":
"""Create an `EntryCollection` of the configured type, depending on the value of
`CONFIG.database_backend`.
@@ -83,8 +85,8 @@ class EntryCollection(ABC):
def __init__(
self,
- resource_cls: Type[EntryResource],
- resource_mapper: Type[BaseResourceMapper],
+ resource_cls: type[EntryResource],
+ resource_mapper: type[BaseResourceMapper],
transformer: Transformer,
):
"""Initialize the collection for the given parameters.
@@ -110,14 +112,14 @@ def __init__(
for field in CONFIG.provider_fields.get(resource_mapper.ENDPOINT, [])
]
- self._all_fields: Set[str] = set()
+ self._all_fields: set[str] = set()
@abstractmethod
def __len__(self) -> int:
"""Returns the total number of entries in the collection."""
@abstractmethod
- def insert(self, data: List[EntryResource]) -> None:
+ def insert(self, data: list[EntryResource]) -> None:
"""Add the given entries to the underlying database.
Arguments:
@@ -126,7 +128,7 @@ def insert(self, data: List[EntryResource]) -> None:
"""
@abstractmethod
- def count(self, **kwargs: Any) -> Union[int, None]:
+ def count(self, **kwargs: Any) -> Optional[int]:
"""Returns the number of entries matching the query specified
by the keyword arguments.
@@ -137,7 +139,13 @@ def count(self, **kwargs: Any) -> Union[int, None]:
def find(
self, params: Union[EntryListingQueryParams, SingleEntryQueryParams]
- ) -> Tuple[Union[None, Dict, List[Dict]], Optional[int], bool, Set[str], Set[str],]:
+ ) -> tuple[
+ Optional[Union[dict[str, Any], list[dict[str, Any]]]],
+ Optional[int],
+ bool,
+ set[str],
+ set[str],
+ ]:
"""
Fetches results and indicates if more data is available.
@@ -159,7 +167,7 @@ def find(
"""
criteria = self.handle_query_params(params)
single_entry = isinstance(params, SingleEntryQueryParams)
- response_fields = criteria.pop("fields")
+ response_fields: set[str] = criteria.pop("fields")
raw_results, data_returned, more_data_available = self._run_db_query(
criteria, single_entry
@@ -170,10 +178,10 @@ def find(
response_fields - self.resource_mapper.TOP_LEVEL_NON_ATTRIBUTES_FIELDS
)
- bad_optimade_fields = set()
- bad_provider_fields = set()
+ bad_optimade_fields: set[str] = set()
+ bad_provider_fields: set[str] = set()
supported_prefixes = self.resource_mapper.SUPPORTED_PREFIXES
- all_attributes = self.resource_mapper.ALL_ATTRIBUTES
+ all_attributes: set[str] = self.resource_mapper.ALL_ATTRIBUTES
for field in include_fields:
if field not in all_attributes:
if field.startswith("_"):
@@ -195,13 +203,13 @@ def find(
detail=f"Unrecognised OPTIMADE field(s) in requested `response_fields`: {bad_optimade_fields}."
)
- results: Union[None, List[Dict], Dict] = None
+ results: Optional[Union[list[dict[str, Any]], dict[str, Any]]] = None
if raw_results:
results = [self.resource_mapper.map_back(doc) for doc in raw_results]
if single_entry:
- results = results[0] # type: ignore[assignment]
+ results = results[0]
if (
CONFIG.validate_api_response
@@ -224,8 +232,8 @@ def find(
@abstractmethod
def _run_db_query(
- self, criteria: Dict[str, Any], single_entry: bool = False
- ) -> Tuple[List[Dict[str, Any]], Optional[int], bool]:
+ self, criteria: dict[str, Any], single_entry: bool = False
+ ) -> tuple[list[dict[str, Any]], Optional[int], bool]:
"""Run the query on the backend and collect the results.
Arguments:
@@ -239,7 +247,7 @@ def _run_db_query(
"""
@property
- def all_fields(self) -> Set[str]:
+ def all_fields(self) -> set[str]:
"""Get the set of all fields handled in this collection,
from attribute fields in the schema, provider fields and top-level OPTIMADE fields.
@@ -266,7 +274,7 @@ def all_fields(self) -> Set[str]:
return self._all_fields
- def get_attribute_fields(self) -> Set[str]:
+ def get_attribute_fields(self) -> set[str]:
"""Get the set of attribute fields
Return only the _first-level_ attribute fields from the schema of the resource class,
@@ -281,24 +289,20 @@ def get_attribute_fields(self) -> Set[str]:
Property names.
"""
+ annotation = _get_origin_type(
+ self.resource_cls.model_fields["attributes"].annotation
+ )
+
+ if annotation in (None, NoneType) or not issubclass(annotation, Attributes):
+ raise TypeError(
+ "resource class 'attributes' field must be a subclass of 'EntryResourceAttributes'"
+ )
- schema = self.resource_cls.schema()
- attributes = schema["properties"]["attributes"]
- if "allOf" in attributes:
- allOf = attributes.pop("allOf")
- for dict_ in allOf:
- attributes.update(dict_)
- if "$ref" in attributes:
- path = attributes["$ref"].split("/")[1:]
- attributes = schema.copy()
- while path:
- next_key = path.pop(0)
- attributes = attributes[next_key]
- return set(attributes["properties"].keys())
+ return set(annotation.model_fields) # type: ignore[attr-defined]
def handle_query_params(
self, params: Union[EntryListingQueryParams, SingleEntryQueryParams]
- ) -> Dict[str, Any]:
+ ) -> dict[str, Any]:
"""Parse and interpret the backend-agnostic query parameter models into a dictionary
that can be used by the specific backend.
@@ -404,7 +408,7 @@ def handle_query_params(
return cursor_kwargs
- def parse_sort_params(self, sort_params: str) -> Iterable[Tuple[str, int]]:
+ def parse_sort_params(self, sort_params: str) -> Iterable[tuple[str, int]]:
"""Handles any sort parameters passed to the collection,
resolving aliases and dealing with any invalid fields.
@@ -416,7 +420,7 @@ def parse_sort_params(self, sort_params: str) -> Iterable[Tuple[str, int]]:
sort direction encoded as 1 (ascending) or -1 (descending).
"""
- sort_spec: List[Tuple[str, int]] = []
+ sort_spec: list[tuple[str, int]] = []
for field in sort_params.split(","):
sort_dir = 1
if field.startswith("-"):
@@ -464,8 +468,8 @@ def parse_sort_params(self, sort_params: str) -> Iterable[Tuple[str, int]]:
def get_next_query_params(
self,
params: EntryListingQueryParams,
- results: Union[None, Dict, List[Dict]],
- ) -> Dict[str, List[str]]:
+ results: Optional[Union[dict[str, Any], list[dict[str, Any]]]],
+ ) -> dict[str, list[str]]:
"""Provides url query pagination parameters that will be used in the next
link.
@@ -477,7 +481,7 @@ def get_next_query_params(
A dictionary with the necessary query parameters.
"""
- query: Dict[str, List[str]] = dict()
+ query: dict[str, list[str]] = dict()
if isinstance(results, list) and results:
# If a user passed a particular pagination mechanism, keep using it
# Otherwise, use the default pagination mechanism of the collection
diff --git a/optimade/server/entry_collections/mongo.py b/optimade/server/entry_collections/mongo.py
index b7bd20f9b..3036aa729 100644
--- a/optimade/server/entry_collections/mongo.py
+++ b/optimade/server/entry_collections/mongo.py
@@ -1,4 +1,4 @@
-from typing import Any, Dict, List, Optional, Tuple, Type, Union
+from typing import Any, Optional, Union
from optimade.filtertransformers.mongo import MongoTransformer
from optimade.models import EntryResource
@@ -38,8 +38,8 @@ class MongoCollection(EntryCollection):
def __init__(
self,
name: str,
- resource_cls: Type[EntryResource],
- resource_mapper: Type[BaseResourceMapper],
+ resource_cls: type[EntryResource],
+ resource_mapper: type[BaseResourceMapper],
database: str = CONFIG.mongo_database,
):
"""Initialize the MongoCollection for the given parameters.
@@ -91,7 +91,7 @@ def count(self, **kwargs: Any) -> Union[int, None]:
except ExecutionTimeout:
return None
- def insert(self, data: List[EntryResource]) -> None:
+ def insert(self, data: list[EntryResource]) -> None:
"""Add the given entries to the underlying database.
Warning:
@@ -105,7 +105,7 @@ def insert(self, data: List[EntryResource]) -> None:
def handle_query_params(
self, params: Union[EntryListingQueryParams, SingleEntryQueryParams]
- ) -> Dict[str, Any]:
+ ) -> dict[str, Any]:
"""Parse and interpret the backend-agnostic query parameter models into a dictionary
that can be used by MongoDB.
@@ -142,8 +142,8 @@ def handle_query_params(
return criteria
def _run_db_query(
- self, criteria: Dict[str, Any], single_entry: bool = False
- ) -> Tuple[List[Dict[str, Any]], Optional[int], bool]:
+ self, criteria: dict[str, Any], single_entry: bool = False
+ ) -> tuple[list[dict[str, Any]], Optional[int], bool]:
"""Run the query on the backend and collect the results.
Arguments:
diff --git a/optimade/server/exception_handlers.py b/optimade/server/exception_handlers.py
index 06fc083a8..c79eaa637 100644
--- a/optimade/server/exception_handlers.py
+++ b/optimade/server/exception_handlers.py
@@ -1,5 +1,6 @@
import traceback
-from typing import Callable, Iterable, List, Optional, Tuple, Type, Union
+from collections.abc import Iterable
+from typing import Callable, Optional, Union
from fastapi import Request
from fastapi.encoders import jsonable_encoder
@@ -18,7 +19,7 @@ def general_exception(
request: Request,
exc: Exception,
status_code: int = 500, # A status_code in `exc` will take precedence
- errors: Optional[List[OptimadeError]] = None,
+ errors: Optional[list[OptimadeError]] = None,
) -> JSONAPIResponse:
"""Handle an exception
@@ -221,8 +222,8 @@ def general_exception_handler(request: Request, exc: Exception) -> JSONAPIRespon
OPTIMADE_EXCEPTIONS: Iterable[
- Tuple[
- Type[Exception],
+ tuple[
+ type[Exception],
Callable[[Request, Exception], JSONAPIResponse],
]
] = [
diff --git a/optimade/server/logger.py b/optimade/server/logger.py
index cae1ec35c..0f574aab4 100644
--- a/optimade/server/logger.py
+++ b/optimade/server/logger.py
@@ -1,4 +1,5 @@
"""Logging to both file and terminal"""
+
import logging
import logging.handlers
import os
diff --git a/optimade/server/main.py b/optimade/server/main.py
index 4f95dcccb..80acac2a8 100644
--- a/optimade/server/main.py
+++ b/optimade/server/main.py
@@ -5,8 +5,10 @@
This is an example implementation with example data.
To implement your own server see the documentation at https://optimade.org/optimade-python-tools.
"""
+
import os
import warnings
+from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
@@ -44,6 +46,19 @@
if CONFIG.debug: # pragma: no cover
LOGGER.info("DEBUG MODE")
+
+@asynccontextmanager # type: ignore[arg-type]
+async def lifespan(app: FastAPI):
+ """Add dynamic endpoints on startup."""
+ # Add API endpoints for MANDATORY base URL `/vMAJOR`
+ add_major_version_base_url(app)
+ # Add API endpoints for OPTIONAL base URLs `/vMAJOR.MINOR` and `/vMAJOR.MINOR.PATCH`
+ add_optional_versioned_base_urls(app)
+
+ # Yield so that the app can start
+ yield
+
+
app = FastAPI(
root_path=CONFIG.root_path,
title="OPTIMADE API",
@@ -57,6 +72,8 @@
redoc_url=f"{BASE_URL_PREFIXES['major']}/extensions/redoc",
openapi_url=f"{BASE_URL_PREFIXES['major']}/extensions/openapi.json",
default_response_class=JSONAPIResponse,
+ separate_input_output_schemas=False,
+ lifespan=lifespan,
)
@@ -123,11 +140,3 @@ def add_optional_versioned_base_urls(app: FastAPI):
for version in ("minor", "patch"):
for endpoint in (info, links, references, structures, landing):
app.include_router(endpoint.router, prefix=BASE_URL_PREFIXES[version])
-
-
-@app.on_event("startup")
-async def startup_event():
- # Add API endpoints for MANDATORY base URL `/vMAJOR`
- add_major_version_base_url(app)
- # Add API endpoints for OPTIONAL base URLs `/vMAJOR.MINOR` and `/vMAJOR.MINOR.PATCH`
- add_optional_versioned_base_urls(app)
diff --git a/optimade/server/main_index.py b/optimade/server/main_index.py
index d57b71877..eccbcfb1d 100644
--- a/optimade/server/main_index.py
+++ b/optimade/server/main_index.py
@@ -5,9 +5,11 @@
This is an example implementation with example data.
To implement your own index meta-database server see the documentation at https://optimade.org/optimade-python-tools.
"""
+
import json
import os
import warnings
+from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
@@ -39,6 +41,19 @@
LOGGER.info("DEBUG MODE")
+@asynccontextmanager # type: ignore[arg-type]
+async def lifespan(app: FastAPI):
+ """Add dynamic endpoints and adjust config on startup."""
+ CONFIG.is_index = True
+ # Add API endpoints for MANDATORY base URL `/vMAJOR`
+ add_major_version_base_url(app)
+ # Add API endpoints for OPTIONAL base URLs `/vMAJOR.MINOR` and `/vMAJOR.MINOR.PATCH`
+ add_optional_versioned_base_urls(app)
+
+ # Yield so that the app can start
+ yield
+
+
app = FastAPI(
root_path=CONFIG.root_path,
title="OPTIMADE API - Index meta-database",
@@ -53,6 +68,8 @@
redoc_url=f"{BASE_URL_PREFIXES['major']}/extensions/redoc",
openapi_url=f"{BASE_URL_PREFIXES['major']}/extensions/openapi.json",
default_response_class=JSONAPIResponse,
+ separate_input_output_schemas=False,
+ lifespan=lifespan,
)
@@ -130,12 +147,3 @@ def add_optional_versioned_base_urls(app: FastAPI):
for version in ("minor", "patch"):
app.include_router(index_info.router, prefix=BASE_URL_PREFIXES[version])
app.include_router(links.router, prefix=BASE_URL_PREFIXES[version])
-
-
-@app.on_event("startup")
-async def startup_event():
- CONFIG.is_index = True
- # Add API endpoints for MANDATORY base URL `/vMAJOR`
- add_major_version_base_url(app)
- # Add API endpoints for OPTIONAL base URLs `/vMAJOR.MINOR` and `/vMAJOR.MINOR.PATCH`
- add_optional_versioned_base_urls(app)
diff --git a/optimade/server/mappers/__init__.py b/optimade/server/mappers/__init__.py
index c38e6ccd0..9fd710fd1 100644
--- a/optimade/server/mappers/__init__.py
+++ b/optimade/server/mappers/__init__.py
@@ -1,4 +1,3 @@
-# pylint: disable=undefined-variable
from .entries import * # noqa: F403
from .links import * # noqa: F403
from .references import * # noqa: F403
diff --git a/optimade/server/mappers/entries.py b/optimade/server/mappers/entries.py
index 103671d3a..fef267c7e 100644
--- a/optimade/server/mappers/entries.py
+++ b/optimade/server/mappers/entries.py
@@ -1,6 +1,7 @@
import warnings
+from collections.abc import Iterable
from functools import lru_cache
-from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Type, Union
+from typing import Any, Optional, Union
from optimade.models.entries import EntryResource
@@ -65,19 +66,19 @@ class BaseResourceMapper:
except (ImportError, ModuleNotFoundError):
PROVIDERS = {}
- KNOWN_PROVIDER_PREFIXES: Set[str] = set(
+ KNOWN_PROVIDER_PREFIXES: set[str] = {
prov["id"] for prov in PROVIDERS.get("data", [])
- )
- ALIASES: Tuple[Tuple[str, str], ...] = ()
- LENGTH_ALIASES: Tuple[Tuple[str, str], ...] = ()
- PROVIDER_FIELDS: Tuple[str, ...] = ()
- ENTRY_RESOURCE_CLASS: Type[EntryResource] = EntryResource
- RELATIONSHIP_ENTRY_TYPES: Set[str] = {"references", "structures"}
- TOP_LEVEL_NON_ATTRIBUTES_FIELDS: Set[str] = {"id", "type", "relationships", "links"}
+ }
+ ALIASES: tuple[tuple[str, str], ...] = ()
+ LENGTH_ALIASES: tuple[tuple[str, str], ...] = ()
+ PROVIDER_FIELDS: tuple[str, ...] = ()
+ ENTRY_RESOURCE_CLASS: type[EntryResource] = EntryResource
+ RELATIONSHIP_ENTRY_TYPES: set[str] = {"references", "structures"}
+ TOP_LEVEL_NON_ATTRIBUTES_FIELDS: set[str] = {"id", "type", "relationships", "links"}
@classmethod
@lru_cache(maxsize=NUM_ENTRY_TYPES)
- def all_aliases(cls) -> Iterable[Tuple[str, str]]:
+ def all_aliases(cls) -> Iterable[tuple[str, str]]:
"""Returns all of the associated aliases for this entry type,
including those defined by the server config. The first member
of each tuple is the OPTIMADE-compliant field name, the second
@@ -116,7 +117,7 @@ def all_aliases(cls) -> Iterable[Tuple[str, str]]:
@classproperty
@lru_cache(maxsize=1)
- def SUPPORTED_PREFIXES(cls) -> Set[str]:
+ def SUPPORTED_PREFIXES(cls) -> set[str]:
"""A set of prefixes handled by this entry type.
!!! note
@@ -131,7 +132,7 @@ def SUPPORTED_PREFIXES(cls) -> Set[str]:
return {CONFIG.provider.prefix}
@classproperty
- def ALL_ATTRIBUTES(cls) -> Set[str]:
+ def ALL_ATTRIBUTES(cls) -> set[str]:
"""Returns all attributes served by this entry."""
from optimade.server.config import CONFIG
@@ -147,15 +148,15 @@ def ALL_ATTRIBUTES(cls) -> Set[str]:
for field in CONFIG.provider_fields.get(cls.ENDPOINT, ())
if isinstance(field, dict)
)
- .union(set(cls.get_optimade_field(field) for field in cls.PROVIDER_FIELDS))
+ .union({cls.get_optimade_field(field) for field in cls.PROVIDER_FIELDS})
)
@classproperty
- def ENTRY_RESOURCE_ATTRIBUTES(cls) -> Dict[str, Any]:
+ def ENTRY_RESOURCE_ATTRIBUTES(cls) -> dict[str, Any]:
"""Returns the dictionary of attributes defined by the underlying entry resource class."""
from optimade.server.schemas import retrieve_queryable_properties
- return retrieve_queryable_properties(cls.ENTRY_RESOURCE_CLASS.schema())
+ return retrieve_queryable_properties(cls.ENTRY_RESOURCE_CLASS)
@classproperty
@lru_cache(maxsize=NUM_ENTRY_TYPES)
@@ -164,16 +165,14 @@ def ENDPOINT(cls) -> str:
to the `type` property of the resource class.
"""
- return (
- cls.ENTRY_RESOURCE_CLASS.schema()
- .get("properties", {})
- .get("type", {})
- .get("default", "")
- )
+ endpoint = cls.ENTRY_RESOURCE_CLASS.model_fields["type"].default
+ if not endpoint and not isinstance(endpoint, str):
+ raise ValueError("Type not set for this entry type!")
+ return endpoint
@classmethod
@lru_cache(maxsize=NUM_ENTRY_TYPES)
- def all_length_aliases(cls) -> Tuple[Tuple[str, str], ...]:
+ def all_length_aliases(cls) -> tuple[tuple[str, str], ...]:
"""Returns all of the associated length aliases for this class,
including those defined by the server config.
@@ -340,7 +339,7 @@ def map_back(cls, doc: dict) -> dict:
"""
mapping = ((real, alias) for alias, real in cls.all_aliases())
newdoc = {}
- reals = {real for alias, real in cls.all_aliases()}
+ reals = {real for _, real in cls.all_aliases()}
for key in doc:
if key not in reals:
newdoc[key] = doc[key]
@@ -368,7 +367,7 @@ def map_back(cls, doc: dict) -> dict:
@classmethod
def deserialize(
cls, results: Union[dict, Iterable[dict]]
- ) -> Union[List[EntryResource], EntryResource]:
+ ) -> Union[list[EntryResource], EntryResource]:
"""Converts the raw database entries for this class into serialized models,
mapping the data along the way.
diff --git a/optimade/server/middleware.py b/optimade/server/middleware.py
index 64cb44f90..dab1e8390 100644
--- a/optimade/server/middleware.py
+++ b/optimade/server/middleware.py
@@ -5,11 +5,13 @@
See the specific Starlette [documentation page](https://www.starlette.io/middleware/) for more
information on it's middleware implementation.
"""
+
import json
import re
import urllib.parse
import warnings
-from typing import Generator, Iterable, List, Optional, TextIO, Type, Union
+from collections.abc import Generator, Iterable
+from typing import Optional, TextIO, Union
from starlette.datastructures import URL as StarletteURL
from starlette.middleware.base import BaseHTTPMiddleware
@@ -111,7 +113,7 @@ class HandleApiHint(BaseHTTPMiddleware):
"""Handle `api_hint` query parameter."""
@staticmethod
- def handle_api_hint(api_hint: List[str]) -> Union[None, str]:
+ def handle_api_hint(api_hint: list[str]) -> Union[None, str]:
"""Handle `api_hint` parameter value.
There are several scenarios that can play out, when handling the `api_hint`
@@ -308,12 +310,12 @@ class AddWarnings(BaseHTTPMiddleware):
"""
- _warnings: List[Warnings]
+ _warnings: list[Warnings]
def showwarning(
self,
message: Union[Warning, str],
- category: Type[Warning],
+ category: type[Warning],
filename: str,
lineno: int,
file: Optional[TextIO] = None,
@@ -401,7 +403,7 @@ def showwarning(
new_warning = Warnings(title=title, detail=detail)
# Add new warning to self._warnings
- self._warnings.append(new_warning.dict(exclude_unset=True))
+ self._warnings.append(new_warning.model_dump(exclude_unset=True))
# Show warning message as normal in sys.stderr
warnings._showwarnmsg_impl( # type: ignore[attr-defined]
diff --git a/optimade/server/query_params.py b/optimade/server/query_params.py
index a955d8ef7..a12ecbef1 100644
--- a/optimade/server/query_params.py
+++ b/optimade/server/query_params.py
@@ -1,9 +1,10 @@
from abc import ABC
-from typing import Iterable, List
+from collections.abc import Iterable
+from typing import Annotated
from warnings import warn
from fastapi import Query
-from pydantic import EmailStr # pylint: disable=no-name-in-module
+from pydantic import EmailStr
from optimade.exceptions import BadRequest
from optimade.server.config import CONFIG
@@ -21,7 +22,7 @@ class BaseQueryParams(ABC):
"""
- unsupported_params: List[str] = []
+ unsupported_params: list[str] = []
def check_params(self, query_params: Iterable[str]) -> None:
"""This method checks whether all the query parameters that are specified
@@ -173,7 +174,7 @@ class EntryListingQueryParams(BaseQueryParams):
"""
# The reference server implementation only supports offset/number-based pagination
- unsupported_params: List[str] = [
+ unsupported_params: list[str] = [
"page_cursor",
"page_below",
]
@@ -181,65 +182,91 @@ class EntryListingQueryParams(BaseQueryParams):
def __init__(
self,
*,
- filter: str = Query( # pylint: disable=redefined-builtin
- "",
- description="A filter string, in the format described in section API Filtering Format Specification of the specification.",
- ),
- response_format: str = Query(
- "json",
- description="The output format requested (see section Response Format).\nDefaults to the format string 'json', which specifies the standard output format described in this specification.\nExample: `http://example.com/v1/structures?response_format=xml`",
- ),
- email_address: EmailStr = Query(
- "",
- description="An email address of the user making the request.\nThe email SHOULD be that of a person and not an automatic system.\nExample: `http://example.com/v1/structures?email_address=user@example.com`",
- ),
- response_fields: str = Query(
- "",
- description="A comma-delimited set of fields to be provided in the output.\nIf provided, these fields MUST be returned along with the REQUIRED fields.\nOther OPTIONAL fields MUST NOT be returned when this parameter is present.\nExample: `http://example.com/v1/structures?response_fields=last_modified,nsites`",
- pattern=r"([a-z_][a-z_0-9]*(,[a-z_][a-z_0-9]*)*)?",
- ),
- sort: str = Query(
- "",
- description='If supporting sortable queries, an implementation MUST use the `sort` query parameter with format as specified by [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-sorting).\n\nAn implementation MAY support multiple sort fields for a single query.\nIf it does, it again MUST conform to the JSON API 1.0 specification.\n\nIf an implementation supports sorting for an entry listing endpoint, then the `/info/` endpoint MUST include, for each field name `` in its `data.properties.` response value that can be used for sorting, the key `sortable` with value `true`.\nIf a field name under an entry listing endpoint supporting sorting cannot be used for sorting, the server MUST either leave out the `sortable` key or set it equal to `false` for the specific field name.\nThe set of field names, with `sortable` equal to `true` are allowed to be used in the "sort fields" list according to its definition in the JSON API 1.0 specification.\nThe field `sortable` is in addition to each property description and other OPTIONAL fields.\nAn example is shown in the section Entry Listing Info Endpoints.',
- pattern=r"([a-z_][a-z_0-9]*(,[a-z_][a-z_0-9]*)*)?",
- ),
- page_limit: int = Query(
- CONFIG.page_limit,
- description="Sets a numerical limit on the number of entries returned.\nSee [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-pagination).\nThe API implementation MUST return no more than the number specified.\nIt MAY return fewer.\nThe database MAY have a maximum limit and not accept larger numbers (in which case an error code -- 403 Forbidden -- MUST be returned).\nThe default limit value is up to the API implementation to decide.\nExample: `http://example.com/optimade/v1/structures?page_limit=100`",
- ge=0,
- ),
- page_offset: int = Query(
- 0,
- description="RECOMMENDED for use with _offset-based_ pagination: using `page_offset` and `page_limit` is RECOMMENDED.\nExample: Skip 50 structures and fetch up to 100: `/structures?page_offset=50&page_limit=100`.",
- ge=0,
- ),
- page_number: int = Query(
- None,
- description="RECOMMENDED for use with _page-based_ pagination: using `page_number` and `page_limit` is RECOMMENDED.\nIt is RECOMMENDED that the first page has number 1, i.e., that `page_number` is 1-based.\nExample: Fetch page 2 of up to 50 structures per page: `/structures?page_number=2&page_limit=50`.",
- # ge=1, # This constraint is only 'RECOMMENDED' in the specification, so should not be included here or in the OpenAPI schema
- ),
- page_cursor: int = Query(
- 0,
- description="RECOMMENDED for use with _cursor-based_ pagination: using `page_cursor` and `page_limit` is RECOMMENDED.",
- ge=0,
- ),
- page_above: str = Query(
- None,
- description="RECOMMENDED for use with _value-based_ pagination: using `page_above`/`page_below` and `page_limit` is RECOMMENDED.\nExample: Fetch up to 100 structures above sort-field value 4000 (in this example, server chooses to fetch results sorted by increasing `id`, so `page_above` value refers to an `id` value): `/structures?page_above=4000&page_limit=100`.",
- ),
- page_below: str = Query(
- None,
- description="RECOMMENDED for use with _value-based_ pagination: using `page_above`/`page_below` and `page_limit` is RECOMMENDED.",
- ),
- include: str = Query(
- "references",
- description='A server MAY implement the JSON API concept of returning [compound documents](https://jsonapi.org/format/1.0/#document-compound-documents) by utilizing the `include` query parameter as specified by [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-includes).\n\nAll related resource objects MUST be returned as part of an array value for the top-level `included` field, see the section JSON Response Schema: Common Fields.\n\nThe value of `include` MUST be a comma-separated list of "relationship paths", as defined in the [JSON API](https://jsonapi.org/format/1.0/#fetching-includes).\nIf relationship paths are not supported, or a server is unable to identify a relationship path a `400 Bad Request` response MUST be made.\n\nThe **default value** for `include` is `references`.\nThis means `references` entries MUST always be included under the top-level field `included` as default, since a server assumes if `include` is not specified by a client in the request, it is still specified as `include=references`.\nNote, if a client explicitly specifies `include` and leaves out `references`, `references` resource objects MUST NOT be included under the top-level field `included`, as per the definition of `included`, see section JSON Response Schema: Common Fields.\n\n> **Note**: A query with the parameter `include` set to the empty string means no related resource objects are to be returned under the top-level field `included`.',
- ),
- api_hint: str = Query(
- "",
- description="If the client provides the parameter, the value SHOULD have the format `vMAJOR` or `vMAJOR.MINOR`, where MAJOR is a major version and MINOR is a minor version of the API. For example, if a client appends `api_hint=v1.0` to the query string, the hint provided is for major version 1 and minor version 0.",
- pattern=r"(v[0-9]+(\.[0-9]+)?)?",
- ),
+ filter: Annotated[
+ str,
+ Query(
+ description="A filter string, in the format described in section API Filtering Format Specification of the specification.",
+ ),
+ ] = "",
+ response_format: Annotated[
+ str,
+ Query(
+ description="The output format requested (see section Response Format).\nDefaults to the format string 'json', which specifies the standard output format described in this specification.\nExample: `http://example.com/v1/structures?response_format=xml`",
+ ),
+ ] = "json",
+ email_address: Annotated[
+ EmailStr,
+ Query(
+ description="An email address of the user making the request.\nThe email SHOULD be that of a person and not an automatic system.\nExample: `http://example.com/v1/structures?email_address=user@example.com`",
+ ),
+ ] = "",
+ response_fields: Annotated[
+ str,
+ Query(
+ description="A comma-delimited set of fields to be provided in the output.\nIf provided, these fields MUST be returned along with the REQUIRED fields.\nOther OPTIONAL fields MUST NOT be returned when this parameter is present.\nExample: `http://example.com/v1/structures?response_fields=last_modified,nsites`",
+ pattern=r"([a-z_][a-z_0-9]*(,[a-z_][a-z_0-9]*)*)?",
+ ),
+ ] = "",
+ sort: Annotated[
+ str,
+ Query(
+ description='If supporting sortable queries, an implementation MUST use the `sort` query parameter with format as specified by [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-sorting).\n\nAn implementation MAY support multiple sort fields for a single query.\nIf it does, it again MUST conform to the JSON API 1.0 specification.\n\nIf an implementation supports sorting for an entry listing endpoint, then the `/info/` endpoint MUST include, for each field name `` in its `data.properties.` response value that can be used for sorting, the key `sortable` with value `true`.\nIf a field name under an entry listing endpoint supporting sorting cannot be used for sorting, the server MUST either leave out the `sortable` key or set it equal to `false` for the specific field name.\nThe set of field names, with `sortable` equal to `true` are allowed to be used in the "sort fields" list according to its definition in the JSON API 1.0 specification.\nThe field `sortable` is in addition to each property description and other OPTIONAL fields.\nAn example is shown in the section Entry Listing Info Endpoints.',
+ pattern=r"([a-z_][a-z_0-9]*(,[a-z_][a-z_0-9]*)*)?",
+ ),
+ ] = "",
+ page_limit: Annotated[
+ int,
+ Query(
+ description="Sets a numerical limit on the number of entries returned.\nSee [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-pagination).\nThe API implementation MUST return no more than the number specified.\nIt MAY return fewer.\nThe database MAY have a maximum limit and not accept larger numbers (in which case an error code -- 403 Forbidden -- MUST be returned).\nThe default limit value is up to the API implementation to decide.\nExample: `http://example.com/optimade/v1/structures?page_limit=100`",
+ ge=0,
+ ),
+ ] = CONFIG.page_limit,
+ page_offset: Annotated[
+ int,
+ Query(
+ description="RECOMMENDED for use with _offset-based_ pagination: using `page_offset` and `page_limit` is RECOMMENDED.\nExample: Skip 50 structures and fetch up to 100: `/structures?page_offset=50&page_limit=100`.",
+ ge=0,
+ ),
+ ] = 0,
+ page_number: Annotated[
+ int,
+ Query(
+ description="RECOMMENDED for use with _page-based_ pagination: using `page_number` and `page_limit` is RECOMMENDED.\nIt is RECOMMENDED that the first page has number 1, i.e., that `page_number` is 1-based.\nExample: Fetch page 2 of up to 50 structures per page: `/structures?page_number=2&page_limit=50`.",
+ # ge=1, # This constraint is only 'RECOMMENDED' in the specification, so should not be included here or in the OpenAPI schema
+ ),
+ ] = None, # type: ignore[assignment]
+ page_cursor: Annotated[
+ int,
+ Query(
+ description="RECOMMENDED for use with _cursor-based_ pagination: using `page_cursor` and `page_limit` is RECOMMENDED.",
+ ge=0,
+ ),
+ ] = 0,
+ page_above: Annotated[
+ str,
+ Query(
+ description="RECOMMENDED for use with _value-based_ pagination: using `page_above`/`page_below` and `page_limit` is RECOMMENDED.\nExample: Fetch up to 100 structures above sort-field value 4000 (in this example, server chooses to fetch results sorted by increasing `id`, so `page_above` value refers to an `id` value): `/structures?page_above=4000&page_limit=100`.",
+ ),
+ ] = None, # type: ignore[assignment]
+ page_below: Annotated[
+ str,
+ Query(
+ description="RECOMMENDED for use with _value-based_ pagination: using `page_above`/`page_below` and `page_limit` is RECOMMENDED.",
+ ),
+ ] = None, # type: ignore[assignment]
+ include: Annotated[
+ str,
+ Query(
+ description='A server MAY implement the JSON API concept of returning [compound documents](https://jsonapi.org/format/1.0/#document-compound-documents) by utilizing the `include` query parameter as specified by [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-includes).\n\nAll related resource objects MUST be returned as part of an array value for the top-level `included` field, see the section JSON Response Schema: Common Fields.\n\nThe value of `include` MUST be a comma-separated list of "relationship paths", as defined in the [JSON API](https://jsonapi.org/format/1.0/#fetching-includes).\nIf relationship paths are not supported, or a server is unable to identify a relationship path a `400 Bad Request` response MUST be made.\n\nThe **default value** for `include` is `references`.\nThis means `references` entries MUST always be included under the top-level field `included` as default, since a server assumes if `include` is not specified by a client in the request, it is still specified as `include=references`.\nNote, if a client explicitly specifies `include` and leaves out `references`, `references` resource objects MUST NOT be included under the top-level field `included`, as per the definition of `included`, see section JSON Response Schema: Common Fields.\n\n> **Note**: A query with the parameter `include` set to the empty string means no related resource objects are to be returned under the top-level field `included`.',
+ ),
+ ] = "references",
+ api_hint: Annotated[
+ str,
+ Query(
+ description="If the client provides the parameter, the value SHOULD have the format `vMAJOR` or `vMAJOR.MINOR`, where MAJOR is a major version and MINOR is a minor version of the API. For example, if a client appends `api_hint=v1.0` to the query string, the hint provided is for major version 1 and minor version 0.",
+ pattern=r"(v[0-9]+(\.[0-9]+)?)?",
+ ),
+ ] = "",
):
self.filter = filter
self.response_format = response_format
@@ -302,28 +329,38 @@ class SingleEntryQueryParams(BaseQueryParams):
def __init__(
self,
*,
- response_format: str = Query(
- "json",
- description="The output format requested (see section Response Format).\nDefaults to the format string 'json', which specifies the standard output format described in this specification.\nExample: `http://example.com/v1/structures?response_format=xml`",
- ),
- email_address: EmailStr = Query(
- "",
- description="An email address of the user making the request.\nThe email SHOULD be that of a person and not an automatic system.\nExample: `http://example.com/v1/structures?email_address=user@example.com`",
- ),
- response_fields: str = Query(
- "",
- description="A comma-delimited set of fields to be provided in the output.\nIf provided, these fields MUST be returned along with the REQUIRED fields.\nOther OPTIONAL fields MUST NOT be returned when this parameter is present.\nExample: `http://example.com/v1/structures?response_fields=last_modified,nsites`",
- pattern=r"([a-z_][a-z_0-9]*(,[a-z_][a-z_0-9]*)*)?",
- ),
- include: str = Query(
- "references",
- description='A server MAY implement the JSON API concept of returning [compound documents](https://jsonapi.org/format/1.0/#document-compound-documents) by utilizing the `include` query parameter as specified by [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-includes).\n\nAll related resource objects MUST be returned as part of an array value for the top-level `included` field, see the section JSON Response Schema: Common Fields.\n\nThe value of `include` MUST be a comma-separated list of "relationship paths", as defined in the [JSON API](https://jsonapi.org/format/1.0/#fetching-includes).\nIf relationship paths are not supported, or a server is unable to identify a relationship path a `400 Bad Request` response MUST be made.\n\nThe **default value** for `include` is `references`.\nThis means `references` entries MUST always be included under the top-level field `included` as default, since a server assumes if `include` is not specified by a client in the request, it is still specified as `include=references`.\nNote, if a client explicitly specifies `include` and leaves out `references`, `references` resource objects MUST NOT be included under the top-level field `included`, as per the definition of `included`, see section JSON Response Schema: Common Fields.\n\n> **Note**: A query with the parameter `include` set to the empty string means no related resource objects are to be returned under the top-level field `included`.',
- ),
- api_hint: str = Query(
- "",
- description="If the client provides the parameter, the value SHOULD have the format `vMAJOR` or `vMAJOR.MINOR`, where MAJOR is a major version and MINOR is a minor version of the API. For example, if a client appends `api_hint=v1.0` to the query string, the hint provided is for major version 1 and minor version 0.",
- pattern=r"(v[0-9]+(\.[0-9]+)?)?",
- ),
+ response_format: Annotated[
+ str,
+ Query(
+ description="The output format requested (see section Response Format).\nDefaults to the format string 'json', which specifies the standard output format described in this specification.\nExample: `http://example.com/v1/structures?response_format=xml`",
+ ),
+ ] = "json",
+ email_address: Annotated[
+ EmailStr,
+ Query(
+ description="An email address of the user making the request.\nThe email SHOULD be that of a person and not an automatic system.\nExample: `http://example.com/v1/structures?email_address=user@example.com`",
+ ),
+ ] = "",
+ response_fields: Annotated[
+ str,
+ Query(
+ description="A comma-delimited set of fields to be provided in the output.\nIf provided, these fields MUST be returned along with the REQUIRED fields.\nOther OPTIONAL fields MUST NOT be returned when this parameter is present.\nExample: `http://example.com/v1/structures?response_fields=last_modified,nsites`",
+ pattern=r"([a-z_][a-z_0-9]*(,[a-z_][a-z_0-9]*)*)?",
+ ),
+ ] = "",
+ include: Annotated[
+ str,
+ Query(
+ description='A server MAY implement the JSON API concept of returning [compound documents](https://jsonapi.org/format/1.0/#document-compound-documents) by utilizing the `include` query parameter as specified by [JSON API 1.0](https://jsonapi.org/format/1.0/#fetching-includes).\n\nAll related resource objects MUST be returned as part of an array value for the top-level `included` field, see the section JSON Response Schema: Common Fields.\n\nThe value of `include` MUST be a comma-separated list of "relationship paths", as defined in the [JSON API](https://jsonapi.org/format/1.0/#fetching-includes).\nIf relationship paths are not supported, or a server is unable to identify a relationship path a `400 Bad Request` response MUST be made.\n\nThe **default value** for `include` is `references`.\nThis means `references` entries MUST always be included under the top-level field `included` as default, since a server assumes if `include` is not specified by a client in the request, it is still specified as `include=references`.\nNote, if a client explicitly specifies `include` and leaves out `references`, `references` resource objects MUST NOT be included under the top-level field `included`, as per the definition of `included`, see section JSON Response Schema: Common Fields.\n\n> **Note**: A query with the parameter `include` set to the empty string means no related resource objects are to be returned under the top-level field `included`.',
+ ),
+ ] = "references",
+ api_hint: Annotated[
+ str,
+ Query(
+ description="If the client provides the parameter, the value SHOULD have the format `vMAJOR` or `vMAJOR.MINOR`, where MAJOR is a major version and MINOR is a minor version of the API. For example, if a client appends `api_hint=v1.0` to the query string, the hint provided is for major version 1 and minor version 0.",
+ pattern=r"(v[0-9]+(\.[0-9]+)?)?",
+ ),
+ ] = "",
):
self.response_format = response_format
self.email_address = email_address
diff --git a/optimade/server/routers/index_info.py b/optimade/server/routers/index_info.py
index 506a80296..310aa2ea1 100644
--- a/optimade/server/routers/index_info.py
+++ b/optimade/server/routers/index_info.py
@@ -32,8 +32,8 @@ def get_info(request: Request) -> IndexInfoResponse:
schema=CONFIG.index_schema_url,
),
data=IndexInfoResource(
- id=IndexInfoResource.schema()["properties"]["id"]["default"],
- type=IndexInfoResource.schema()["properties"]["type"]["default"],
+ id=IndexInfoResource.model_fields["id"].default,
+ type=IndexInfoResource.model_fields["type"].default,
attributes=IndexInfoAttributes(
api_version=f"{__api_version__}",
available_api_versions=[
@@ -50,9 +50,7 @@ def get_info(request: Request) -> IndexInfoResponse:
relationships={
"default": IndexRelationship(
data={
- "type": RelatedLinksResource.schema()["properties"]["type"][
- "default"
- ],
+ "type": RelatedLinksResource.model_fields["type"].default,
"id": CONFIG.default_db,
}
)
diff --git a/optimade/server/routers/info.py b/optimade/server/routers/info.py
index 4b351f371..0c080b6b9 100644
--- a/optimade/server/routers/info.py
+++ b/optimade/server/routers/info.py
@@ -30,8 +30,8 @@ def _generate_info_response() -> BaseInfoResource:
"""Cached closure that generates the info response for the implementation."""
return BaseInfoResource(
- id=BaseInfoResource.schema()["properties"]["id"]["default"],
- type=BaseInfoResource.schema()["properties"]["type"]["default"],
+ id=BaseInfoResource.model_fields["id"].default,
+ type=BaseInfoResource.model_fields["type"].default,
attributes=BaseInfoAttributes(
api_version=__api_version__,
available_api_versions=[
@@ -68,31 +68,33 @@ def _generate_entry_info_response(entry: str) -> EntryInfoResource:
"""Cached closure that generates the entry info response for the given type.
Parameters:
- entry: The OPTIMADE type to generate the info response for, e.g., `"structures"`.
- Must be a key in `ENTRY_INFO_SCHEMAS`.
+ entry: The OPTIMADE type to generate the info response for, e.g.,
+ `"structures"`. Must be a key in `ENTRY_INFO_SCHEMAS`.
"""
valid_entry_info_endpoints = ENTRY_INFO_SCHEMAS.keys()
if entry not in valid_entry_info_endpoints:
raise StarletteHTTPException(
status_code=404,
- detail=f"Entry info not found for {entry}, valid entry info endpoints are: {', '.join(valid_entry_info_endpoints)}",
+ detail=(
+ f"Entry info not found for {entry}, valid entry info endpoints "
+ f"are: {', '.join(valid_entry_info_endpoints)}"
+ ),
)
- schema = ENTRY_INFO_SCHEMAS[entry]()
+ schema = ENTRY_INFO_SCHEMAS[entry]
queryable_properties = {"id", "type", "attributes"}
properties = retrieve_queryable_properties(
schema, queryable_properties, entry_type=entry
)
- output_fields_by_format = {"json": list(properties.keys())}
+ output_fields_by_format = {"json": list(properties)}
return EntryInfoResource(
- formats=list(output_fields_by_format.keys()),
- description=schema.get("description", "Entry Resources"),
+ formats=list(output_fields_by_format),
+ description=getattr(schema, "__doc__", "Entry Resources"),
properties=properties,
output_fields_by_format=output_fields_by_format,
- schema=CONFIG.schema_url,
)
return EntryInfoResponse(
diff --git a/optimade/server/routers/landing.py b/optimade/server/routers/landing.py
index 3b51f2a1c..e71ddf4e7 100644
--- a/optimade/server/routers/landing.py
+++ b/optimade/server/routers/landing.py
@@ -1,4 +1,4 @@
-""" OPTIMADE landing page router. """
+"""OPTIMADE landing page router."""
from functools import lru_cache
from pathlib import Path
@@ -13,7 +13,7 @@
from optimade.server.routers.utils import get_base_url, meta_values
-@lru_cache()
+@lru_cache
def render_landing_page(url: str) -> HTMLResponse:
"""Render and cache the landing page.
diff --git a/optimade/server/routers/links.py b/optimade/server/routers/links.py
index 025d9f76c..044b2e36f 100644
--- a/optimade/server/routers/links.py
+++ b/optimade/server/routers/links.py
@@ -1,4 +1,4 @@
-from typing import Any, Dict
+from typing import Annotated, Any
from fastapi import APIRouter, Depends, Request
@@ -21,12 +21,12 @@
@router.get(
"/links",
- response_model=LinksResponse if CONFIG.validate_api_response else Dict,
+ response_model=LinksResponse if CONFIG.validate_api_response else dict,
response_model_exclude_unset=True,
tags=["Links"],
responses=ERROR_RESPONSES,
)
-def get_links(request: Request, params: EntryListingQueryParams = Depends()) -> Any:
- return get_entries(
- collection=links_coll, response=LinksResponse, request=request, params=params
- )
+def get_links(
+ request: Request, params: Annotated[EntryListingQueryParams, Depends()]
+) -> dict[str, Any]:
+ return get_entries(collection=links_coll, request=request, params=params)
diff --git a/optimade/server/routers/references.py b/optimade/server/routers/references.py
index 2508c700a..60aa0c62a 100644
--- a/optimade/server/routers/references.py
+++ b/optimade/server/routers/references.py
@@ -1,4 +1,4 @@
-from typing import Any, Dict
+from typing import Annotated, Any
from fastapi import APIRouter, Depends, Request
@@ -25,17 +25,18 @@
@router.get(
"/references",
- response_model=ReferenceResponseMany if CONFIG.validate_api_response else Dict,
+ response_model=ReferenceResponseMany
+ if CONFIG.validate_api_response
+ else dict[str, Any],
response_model_exclude_unset=True,
tags=["References"],
responses=ERROR_RESPONSES,
)
def get_references(
- request: Request, params: EntryListingQueryParams = Depends()
-) -> Any:
+ request: Request, params: Annotated[EntryListingQueryParams, Depends()]
+) -> dict[str, Any]:
return get_entries(
collection=references_coll,
- response=ReferenceResponseMany,
request=request,
params=params,
)
@@ -43,18 +44,21 @@ def get_references(
@router.get(
"/references/{entry_id:path}",
- response_model=ReferenceResponseOne if CONFIG.validate_api_response else Dict,
+ response_model=ReferenceResponseOne
+ if CONFIG.validate_api_response
+ else dict[str, Any],
response_model_exclude_unset=True,
tags=["References"],
responses=ERROR_RESPONSES,
)
def get_single_reference(
- request: Request, entry_id: str, params: SingleEntryQueryParams = Depends()
-) -> Any:
+ request: Request,
+ entry_id: str,
+ params: Annotated[SingleEntryQueryParams, Depends()],
+) -> dict[str, Any]:
return get_single_entry(
collection=references_coll,
entry_id=entry_id,
- response=ReferenceResponseOne,
request=request,
params=params,
)
diff --git a/optimade/server/routers/structures.py b/optimade/server/routers/structures.py
index 00980b246..7b090056e 100644
--- a/optimade/server/routers/structures.py
+++ b/optimade/server/routers/structures.py
@@ -1,4 +1,4 @@
-from typing import Any, Dict
+from typing import Annotated, Any
from fastapi import APIRouter, Depends, Request
@@ -25,17 +25,18 @@
@router.get(
"/structures",
- response_model=StructureResponseMany if CONFIG.validate_api_response else Dict,
+ response_model=StructureResponseMany
+ if CONFIG.validate_api_response
+ else dict[str, Any],
response_model_exclude_unset=True,
tags=["Structures"],
responses=ERROR_RESPONSES,
)
def get_structures(
- request: Request, params: EntryListingQueryParams = Depends()
-) -> Any:
+ request: Request, params: Annotated[EntryListingQueryParams, Depends()]
+) -> dict[str, Any]:
return get_entries(
collection=structures_coll,
- response=StructureResponseMany,
request=request,
params=params,
)
@@ -43,18 +44,21 @@ def get_structures(
@router.get(
"/structures/{entry_id:path}",
- response_model=StructureResponseOne if CONFIG.validate_api_response else Dict,
+ response_model=StructureResponseOne
+ if CONFIG.validate_api_response
+ else dict[str, Any],
response_model_exclude_unset=True,
tags=["Structures"],
responses=ERROR_RESPONSES,
)
def get_single_structure(
- request: Request, entry_id: str, params: SingleEntryQueryParams = Depends()
-) -> Any:
+ request: Request,
+ entry_id: str,
+ params: Annotated[SingleEntryQueryParams, Depends()],
+) -> dict[str, Any]:
return get_single_entry(
collection=structures_coll,
entry_id=entry_id,
- response=StructureResponseOne,
request=request,
params=params,
)
diff --git a/optimade/server/routers/utils.py b/optimade/server/routers/utils.py
index cd85a76ed..760e569f1 100644
--- a/optimade/server/routers/utils.py
+++ b/optimade/server/routers/utils.py
@@ -1,8 +1,7 @@
-# pylint: disable=import-outside-toplevel,too-many-locals
import re
import urllib.parse
from datetime import datetime
-from typing import Any, Dict, List, Optional, Set, Type, Union
+from typing import Any, Optional, Union
from fastapi import Request
from fastapi.responses import JSONResponse
@@ -10,13 +9,7 @@
from optimade import __api_version__
from optimade.exceptions import BadRequest, InternalServerError
-from optimade.models import (
- EntryResource,
- EntryResponseMany,
- EntryResponseOne,
- ResponseMeta,
- ToplevelLinks,
-)
+from optimade.models import EntryResource, ResponseMeta, ToplevelLinks
from optimade.server.config import CONFIG
from optimade.server.entry_collections import EntryCollection
from optimade.server.query_params import EntryListingQueryParams, SingleEntryQueryParams
@@ -91,10 +84,10 @@ def meta_values(
def handle_response_fields(
- results: Union[List[EntryResource], EntryResource, List[Dict], Dict],
- exclude_fields: Set[str],
- include_fields: Set[str],
-) -> List[Dict[str, Any]]:
+ results: Union[list[EntryResource], EntryResource, list[dict], dict],
+ exclude_fields: set[str],
+ include_fields: set[str],
+) -> list[dict[str, Any]]:
"""Handle query parameter `response_fields`.
It is assumed that all fields are under `attributes`.
@@ -117,7 +110,7 @@ def handle_response_fields(
while results:
new_entry = results.pop(0)
try:
- new_entry = new_entry.dict(exclude_unset=True, by_alias=True) # type: ignore[union-attr]
+ new_entry = new_entry.model_dump(exclude_unset=True, by_alias=True) # type: ignore[union-attr]
except AttributeError:
pass
@@ -137,10 +130,10 @@ def handle_response_fields(
def get_included_relationships(
- results: Union[EntryResource, List[EntryResource], Dict, List[Dict]],
- ENTRY_COLLECTIONS: Dict[str, EntryCollection],
- include_param: List[str],
-) -> List[Union[EntryResource, Dict]]:
+ results: Union[EntryResource, list[EntryResource], dict, list[dict]],
+ ENTRY_COLLECTIONS: dict[str, EntryCollection],
+ include_param: list[str],
+) -> list[Union[EntryResource, dict[str, Any]]]:
"""Filters the included relationships and makes the appropriate compound request
to include them in the response.
@@ -168,7 +161,7 @@ def get_included_relationships(
f"Known relationship types: {sorted(ENTRY_COLLECTIONS.keys())}"
)
- endpoint_includes: Dict[Any, Dict] = defaultdict(dict)
+ endpoint_includes: dict[Any, dict] = defaultdict(dict)
for doc in results:
# convert list of references into dict by ID to only included unique IDs
if doc is None:
@@ -183,7 +176,7 @@ def get_included_relationships(
continue
if not isinstance(relationships, dict):
- relationships = relationships.dict()
+ relationships = relationships.model_dump()
for entry_type in ENTRY_COLLECTIONS:
# Skip entry type if it is not in `include_param`
@@ -197,12 +190,13 @@ def get_included_relationships(
if ref["id"] not in endpoint_includes[entry_type]:
endpoint_includes[entry_type][ref["id"]] = ref
- included: Dict[
- str, Union[List[EntryResource], EntryResource, List[Dict], Dict]
+ included: dict[
+ str,
+ Union[list[EntryResource], list[dict[str, Any]]],
] = {}
for entry_type in endpoint_includes:
compound_filter = " OR ".join(
- ['id="{}"'.format(ref_id) for ref_id in endpoint_includes[entry_type]]
+ [f'id="{ref_id}"' for ref_id in endpoint_includes[entry_type]]
)
params = EntryListingQueryParams(
filter=compound_filter,
@@ -217,7 +211,7 @@ def get_included_relationships(
ref_results, _, _, _, _ = ENTRY_COLLECTIONS[entry_type].find(params)
if ref_results is None:
ref_results = []
- included[entry_type] = ref_results
+ included[entry_type] = ref_results # type: ignore[assignment]
# flatten dict by endpoint to list
return [obj for endp in included.values() for obj in endp]
@@ -226,7 +220,7 @@ def get_included_relationships(
def get_base_url(
parsed_url_request: Union[
urllib.parse.ParseResult, urllib.parse.SplitResult, StarletteURL, str
- ]
+ ],
) -> str:
"""Get base URL for current server
@@ -246,10 +240,9 @@ def get_base_url(
def get_entries(
collection: EntryCollection,
- response: Type[EntryResponseMany], # noqa
request: Request,
params: EntryListingQueryParams,
-) -> Dict:
+) -> dict[str, Any]:
"""Generalized /{entry} endpoint getter"""
from optimade.server.routers import ENTRY_COLLECTIONS
@@ -285,10 +278,10 @@ def get_entries(
if results is not None and (fields or include_fields):
results = handle_response_fields(results, fields, include_fields) # type: ignore[assignment]
- return dict(
- links=links,
- data=results if results else [],
- meta=meta_values(
+ return {
+ "links": links,
+ "data": results if results else [],
+ "meta": meta_values(
url=request.url,
data_returned=data_returned,
data_available=len(collection),
@@ -297,17 +290,16 @@ def get_entries(
if not CONFIG.is_index
else CONFIG.index_schema_url,
),
- included=included,
- )
+ "included": included,
+ }
def get_single_entry(
collection: EntryCollection,
entry_id: str,
- response: Type[EntryResponseOne],
request: Request,
params: SingleEntryQueryParams,
-) -> Dict:
+) -> dict[str, Any]:
from optimade.server.routers import ENTRY_COLLECTIONS
params.check_params(request.query_params)
@@ -338,10 +330,10 @@ def get_single_entry(
if results is not None and (fields or include_fields):
results = handle_response_fields(results, fields, include_fields)[0] # type: ignore[assignment]
- return dict(
- links=links,
- data=results if results else None,
- meta=meta_values(
+ return {
+ "links": links,
+ "data": results if results else None,
+ "meta": meta_values(
url=request.url,
data_returned=data_returned,
data_available=len(collection),
@@ -350,5 +342,5 @@ def get_single_entry(
if not CONFIG.is_index
else CONFIG.index_schema_url,
),
- included=included,
- )
+ "included": included,
+ }
diff --git a/optimade/server/routers/versions.py b/optimade/server/routers/versions.py
index cccb1f075..d5cc8c3b6 100644
--- a/optimade/server/routers/versions.py
+++ b/optimade/server/routers/versions.py
@@ -1,4 +1,4 @@
-from fastapi import APIRouter, Request
+from fastapi import APIRouter
from fastapi.responses import Response
from optimade.server.routers.utils import BASE_URL_PREFIXES
@@ -15,7 +15,7 @@ class CsvResponse(Response):
tags=["Versions"],
response_class=CsvResponse,
)
-def get_versions(request: Request):
+def get_versions() -> CsvResponse:
"""Respond with the text/csv representation for the served versions."""
version = BASE_URL_PREFIXES["major"].replace("/v", "")
response = f"version\n{version}"
diff --git a/optimade/server/schemas.py b/optimade/server/schemas.py
index c01cc914e..a5cede3f0 100644
--- a/optimade/server/schemas.py
+++ b/optimade/server/schemas.py
@@ -1,17 +1,35 @@
-from typing import Callable, Dict, Iterable, Optional
+from collections.abc import Iterable
+from typing import TYPE_CHECKING, Any, Optional
+
+from pydantic import BaseModel, TypeAdapter
from optimade.models import (
DataType,
+ EntryResource,
ErrorResponse,
ReferenceResource,
StructureResource,
)
+from optimade.models.types import NoneType, _get_origin_type
+
+if TYPE_CHECKING: # pragma: no cover
+ from typing import Literal, Union
+
+ from optimade.models.utils import SupportLevel
+
+ QueryableProperties = dict[
+ str,
+ dict[
+ Literal["description", "unit", "queryable", "support", "sortable", "type"],
+ Optional[Union[str, SupportLevel, bool, DataType]],
+ ],
+ ]
__all__ = ("ENTRY_INFO_SCHEMAS", "ERROR_RESPONSES", "retrieve_queryable_properties")
-ENTRY_INFO_SCHEMAS: Dict[str, Callable[[], Dict]] = {
- "structures": StructureResource.schema,
- "references": ReferenceResource.schema,
+ENTRY_INFO_SCHEMAS: dict[str, type[EntryResource]] = {
+ "structures": StructureResource,
+ "references": ReferenceResource,
}
"""This dictionary is used to define the `/info/` endpoints."""
@@ -24,7 +42,7 @@
"""
from optimade.exceptions import POSSIBLE_ERRORS
- ERROR_RESPONSES: Optional[Dict[int, Dict]] = {
+ ERROR_RESPONSES: Optional[dict[int, dict[str, Any]]] = {
err.status_code: {"model": ErrorResponse, "description": err.title}
for err in POSSIBLE_ERRORS
}
@@ -33,16 +51,15 @@
def retrieve_queryable_properties(
- schema: dict,
+ schema: type[EntryResource],
queryable_properties: Optional[Iterable[str]] = None,
entry_type: Optional[str] = None,
-) -> dict:
- """Recursively loops through the schema of a pydantic model and
- resolves all references, returning a dictionary of all the
+) -> "QueryableProperties":
+ """Recursively loops through a pydantic model, returning a dictionary of all the
OPTIMADE-queryable properties of that model.
Parameters:
- schema: The schema of the pydantic model.
+ schema: The pydantic model.
queryable_properties: The list of properties to find in the schema.
entry_type: An optional entry type for the model. Will be used to
lookup schemas for any config-defined fields.
@@ -53,36 +70,57 @@ def retrieve_queryable_properties(
and type, where provided.
"""
- properties = {}
- for name, value in schema["properties"].items():
+ properties: "QueryableProperties" = {}
+ for name, value in schema.model_fields.items():
+ # Proceed if the field (name) is given explicitly in the queryable_properties
+ # list or if the queryable_properties list is empty (i.e., all properties are
+ # requested)
if not queryable_properties or name in queryable_properties:
- if "$ref" in value:
- path = value["$ref"].split("/")[1:]
- sub_schema = schema.copy()
- while path:
- next_key = path.pop(0)
- sub_schema = sub_schema[next_key]
- sub_queryable_properties = sub_schema["properties"].keys()
+ if name in properties:
+ continue
+
+ # If the field is another data model, "unpack" it by recursively calling
+ # this function.
+ # But first, we need to "unpack" the annotation, getting in behind any
+ # Optional, Union, or Annotated types.
+ annotation = _get_origin_type(value.annotation)
+
+ if annotation not in (None, NoneType) and issubclass(annotation, BaseModel):
+ sub_queryable_properties = list(annotation.model_fields) # type: ignore[attr-defined]
properties.update(
- retrieve_queryable_properties(sub_schema, sub_queryable_properties)
+ retrieve_queryable_properties(annotation, sub_queryable_properties)
)
- else:
- properties[name] = {"description": value.get("description", "")}
- # Update schema with extension keys provided they are not None
- for key in (
- "x-optimade-unit",
- "x-optimade-queryable",
- "x-optimade-support",
+
+ properties[name] = {"description": value.description or ""}
+
+ # Update schema with extension keys, provided they are not None
+ for key in (
+ "x-optimade-unit",
+ "x-optimade-queryable",
+ "x-optimade-support",
+ ):
+ if (
+ value.json_schema_extra
+ and value.json_schema_extra.get(key) is not None
):
- if value.get(key) is not None:
- properties[name][key.replace("x-optimade-", "")] = value[key]
- # All properties are sortable with the MongoDB backend.
- # While the result for sorting lists may not be as expected, they are still sorted.
- properties[name]["sortable"] = value.get("x-optimade-sortable", True)
- # Try to get OpenAPI-specific "format" if possible, else get "type"; a mandatory OpenAPI key.
- properties[name]["type"] = DataType.from_json_type(
- value.get("format", value.get("type"))
- )
+ properties[name][
+ key.replace("x-optimade-", "") # type: ignore[index]
+ ] = value.json_schema_extra[key]
+
+ # All properties are sortable with the MongoDB backend.
+ # While the result for sorting lists may not be as expected, they are still sorted.
+ properties[name]["sortable"] = (
+ value.json_schema_extra.get("x-optimade-sortable", True)
+ if value.json_schema_extra
+ else True
+ )
+
+ # Try to get OpenAPI-specific "format" if possible, else get "type"; a mandatory OpenAPI key.
+ json_schema = TypeAdapter(annotation).json_schema(mode="validation")
+
+ properties[name]["type"] = DataType.from_json_type(
+ json_schema.get("format", json_schema.get("type"))
+ )
# If specified, check the config for any additional well-described provider fields
if entry_type:
@@ -102,4 +140,10 @@ def retrieve_queryable_properties(
properties[name] = {k: field[k] for k in field if k != "name"}
properties[name]["sortable"] = field.get("sortable", True)
+ # Remove JSON fields mistaken as properties
+ non_property_fields = ["attributes", "relationships"]
+ for non_property_field in non_property_fields:
+ if non_property_field in properties:
+ del properties[non_property_field]
+
return properties
diff --git a/optimade/utils.py b/optimade/utils.py
index cb3039565..befe10dfe 100644
--- a/optimade/utils.py
+++ b/optimade/utils.py
@@ -3,8 +3,15 @@
"""
+import contextlib
import json
-from typing import Container, Iterable, List, Optional
+from collections.abc import Container, Iterable
+from typing import TYPE_CHECKING, Optional
+
+from requests.exceptions import SSLError
+
+if TYPE_CHECKING:
+ import rich
from pydantic import ValidationError
@@ -53,11 +60,12 @@ def get_providers(add_mongo_id: bool = False) -> list:
for provider_list_url in PROVIDER_LIST_URLS:
try:
- providers = requests.get(provider_list_url).json()
+ providers = requests.get(provider_list_url, timeout=10).json()
except (
requests.exceptions.ConnectionError,
requests.exceptions.ConnectTimeout,
json.JSONDecodeError,
+ requests.exceptions.SSLError,
):
pass
else:
@@ -75,9 +83,7 @@ def get_providers(add_mongo_id: bool = False) -> list:
{}
The list of providers will not be included in the `/links`-endpoint.
-""".format(
- "".join([f" * {_}\n" for _ in PROVIDER_LIST_URLS])
- )
+""".format("".join([f" * {_}\n" for _ in PROVIDER_LIST_URLS]))
)
return []
@@ -101,12 +107,18 @@ def get_providers(add_mongo_id: bool = False) -> list:
def get_child_database_links(
- provider: LinksResource, obey_aggregate: bool = True
-) -> List[LinksResource]:
+ provider: LinksResource,
+ obey_aggregate: bool = True,
+ headers: Optional[dict] = None,
+ skip_ssl: bool = False,
+) -> list[LinksResource]:
"""For a provider, return a list of available child databases.
Arguments:
provider: The links entry for the provider.
+ obey_aggregate: Whether to only return links that allow
+ aggregation.
+ headers: Additional HTTP headers to pass to the provider.
Returns:
A list of the valid links entries from this provider that
@@ -120,7 +132,6 @@ def get_child_database_links(
import requests
from optimade.models.links import Aggregate, LinkType
- from optimade.models.responses import LinksResponse
base_url = provider.pop("base_url")
if base_url is None:
@@ -128,7 +139,14 @@ def get_child_database_links(
links_endp = base_url + "/v1/links"
try:
- links = requests.get(links_endp, timeout=10)
+ links = requests.get(links_endp, timeout=10, headers=headers)
+ except SSLError as exc:
+ if skip_ssl:
+ links = requests.get(links_endp, timeout=10, headers=headers, verify=False)
+ else:
+ raise RuntimeError(
+ f"SSL error when connecting to provider {provider['id']}. Use `skip_ssl` to ignore."
+ ) from exc
except (requests.ConnectionError, requests.Timeout) as exc:
raise RuntimeError(f"Unable to connect to provider {provider['id']}") from exc
@@ -138,27 +156,31 @@ def get_child_database_links(
)
try:
- links_resp = LinksResponse(**links.json())
-
- return [
- link
- for link in links_resp.data
- if isinstance(link, LinksResource)
- and link.attributes.link_type == LinkType.CHILD
- and link.attributes.base_url is not None
- and (not obey_aggregate or link.attributes.aggregate == Aggregate.OK)
- ]
+ links_resources = links.json().get("data", [])
+ return_links = []
+ for link in links_resources:
+ link = LinksResource(**link)
+ if (
+ link.attributes.link_type == LinkType.CHILD
+ and link.attributes.base_url is not None
+ and (not obey_aggregate or link.attributes.aggregate == Aggregate.OK)
+ ):
+ return_links.append(link)
+
+ return return_links
except (ValidationError, json.JSONDecodeError) as exc:
raise RuntimeError(
- f"Did not understand response from {provider['id']}: {links.content!r}"
- ) from exc
+ f"Did not understand response from {provider['id']}: {links.content!r}, {exc}"
+ )
def get_all_databases(
include_providers: Optional[Container[str]] = None,
exclude_providers: Optional[Container[str]] = None,
exclude_databases: Optional[Container[str]] = None,
+ progress: "Optional[rich.Progress]" = None,
+ skip_ssl: bool = False,
) -> Iterable[str]:
"""Iterate through all databases reported by registered OPTIMADE providers.
@@ -171,21 +193,42 @@ def get_all_databases(
A generator of child database links that obey the given parameters.
"""
- for provider in get_providers():
- if exclude_providers and provider["id"] in exclude_providers:
- continue
- if include_providers and provider["id"] not in include_providers:
- continue
-
- try:
- links = get_child_database_links(provider)
- for link in links:
- if link.attributes.base_url:
- if (
- exclude_databases
- and link.attributes.base_url in exclude_databases
- ):
- continue
- yield str(link.attributes.base_url)
- except RuntimeError:
- pass
+ if progress is not None:
+ _task = progress.add_task(
+ description="Retrieving all databases from registered OPTIMADE providers...",
+ total=None,
+ )
+ else:
+ progress = contextlib.nullcontext()
+ progress.print = lambda _: None # type: ignore[attr-defined]
+ progress.advance = lambda *_: None # type: ignore[attr-defined]
+ _task = None
+
+ with progress:
+ for provider in get_providers():
+ if exclude_providers and provider["id"] in exclude_providers:
+ continue
+ if include_providers and provider["id"] not in include_providers:
+ continue
+
+ try:
+ links = get_child_database_links(provider, skip_ssl=skip_ssl)
+ for link in links:
+ if link.attributes.base_url:
+ if (
+ exclude_databases
+ and link.attributes.base_url in exclude_databases
+ ):
+ continue
+ yield str(link.attributes.base_url)
+ if links and progress is not None:
+ progress.advance(_task, 1)
+ progress.print(
+ f"Retrieved databases from [bold green]{provider['id']}[/bold green]"
+ )
+ except RuntimeError as exc:
+ if progress is not None:
+ progress.print(
+ f"Unable to retrieve databases from [bold red]{provider['id']}[/bold red]: {exc}",
+ )
+ pass
diff --git a/optimade/validator/__init__.py b/optimade/validator/__init__.py
index 2688f5118..ed6b69f2e 100644
--- a/optimade/validator/__init__.py
+++ b/optimade/validator/__init__.py
@@ -1,5 +1,5 @@
-""" This module contains the ImplementationValidator class and corresponding command line tools. """
-# pylint: disable=import-outside-toplevel
+"""This module contains the ImplementationValidator class and corresponding command line tools."""
+
import warnings
from optimade import __api_version__, __version__
diff --git a/optimade/validator/config.py b/optimade/validator/config.py
index 390a3f7b8..0fc6eda3c 100644
--- a/optimade/validator/config.py
+++ b/optimade/validator/config.py
@@ -1,4 +1,4 @@
-""" This submodule defines constant values and definitions
+"""This submodule defines constant values and definitions
from the OPTIMADE specification for use by the validator.
The `VALIDATOR_CONFIG` object can be imported and modified
@@ -7,9 +7,11 @@
"""
-from typing import Any, Container, Dict, List, Set
+from collections.abc import Container
+from typing import Any
-from pydantic import BaseSettings, Field
+from pydantic import Field
+from pydantic_settings import BaseSettings
from optimade.models import (
DataType,
@@ -32,7 +34,7 @@
_ENTRY_SCHEMAS = {
endp: retrieve_queryable_properties(
- ENTRY_INFO_SCHEMAS[endp](), ("id", "type", "attributes")
+ ENTRY_INFO_SCHEMAS[endp], ("id", "type", "attributes")
)
for endp in ENTRY_INFO_SCHEMAS
}
@@ -122,26 +124,26 @@ class ValidatorConfig(BaseSettings):
"""
- response_classes: Dict[str, Any] = Field(
+ response_classes: dict[str, Any] = Field(
_RESPONSE_CLASSES,
description="Dictionary containing the mapping between endpoints and response classes for the main database",
)
- response_classes_index: Dict[str, Any] = Field(
+ response_classes_index: dict[str, Any] = Field(
_RESPONSE_CLASSES_INDEX,
description="Dictionary containing the mapping between endpoints and response classes for the index meta-database",
)
- entry_schemas: Dict[str, Any] = Field(
+ entry_schemas: dict[str, Any] = Field(
_ENTRY_SCHEMAS, description="The entry listing endpoint schemas"
)
- entry_endpoints: Set[str] = Field(
+ entry_endpoints: set[str] = Field(
_ENTRY_ENDPOINTS,
description="The entry endpoints to validate, if present in the API's `/info` response `entry_types_by_format['json']`",
)
- unique_properties: Set[str] = Field(
+ unique_properties: set[str] = Field(
_UNIQUE_PROPERTIES,
description=(
"Fields that should be treated as unique indexes for all endpoints, "
@@ -149,7 +151,7 @@ class ValidatorConfig(BaseSettings):
),
)
- inclusive_operators: Dict[DataType, Set[str]] = Field(
+ inclusive_operators: dict[DataType, set[str]] = Field(
_INCLUSIVE_OPERATORS,
description=(
"Dictionary mapping OPTIMADE `DataType`s to a list of operators that are 'inclusive', "
@@ -157,7 +159,7 @@ class ValidatorConfig(BaseSettings):
),
)
- exclusive_operators: Dict[DataType, Set[str]] = Field(
+ exclusive_operators: dict[DataType, set[str]] = Field(
_EXCLUSIVE_OPERATORS,
description=(
"Dictionary mapping OPTIMADE `DataType`s to a list of operators that are 'exclusive', "
@@ -165,7 +167,7 @@ class ValidatorConfig(BaseSettings):
),
)
- field_specific_overrides: Dict[str, Dict[SupportLevel, Container[str]]] = Field(
+ field_specific_overrides: dict[str, dict[SupportLevel, Container[str]]] = Field(
_FIELD_SPECIFIC_OVERRIDES,
description=(
"Some fields do not require all type comparison operators to be supported. "
@@ -181,16 +183,16 @@ class ValidatorConfig(BaseSettings):
)
info_endpoint: str = Field("info", description="The name of the info endpoint")
- non_entry_endpoints: Set[str] = Field(
+ non_entry_endpoints: set[str] = Field(
_NON_ENTRY_ENDPOINTS,
description="The list specification-mandated endpoint names that do not contain entries",
)
- top_level_non_attribute_fields: Set[str] = Field(
+ top_level_non_attribute_fields: set[str] = Field(
BaseResourceMapper.TOP_LEVEL_NON_ATTRIBUTES_FIELDS,
description="Field names to treat as top-level",
)
- enum_fallback_values: Dict[str, Dict[str, List[str]]] = Field(
+ enum_fallback_values: dict[str, dict[str, list[str]]] = Field(
_ENUM_DUMMY_VALUES,
description="Provide fallback values for enum fields to use when validating filters.",
)
diff --git a/optimade/validator/utils.py b/optimade/validator/utils.py
index 81e5e11b7..265ee0c85 100644
--- a/optimade/validator/utils.py
+++ b/optimade/validator/utils.py
@@ -1,4 +1,4 @@
-""" This submodule contains utility methods and models
+"""This submodule contains utility methods and models
used by the validator. The two main features being:
1. The `@test_case` decorator can be used to decorate validation
@@ -18,7 +18,7 @@
import time
import traceback as tb
import urllib.parse
-from typing import Any, Callable, Dict, List, Optional, Tuple
+from typing import Any, Callable, Optional
import requests
from pydantic import Field, ValidationError
@@ -80,11 +80,11 @@ class ValidatorResults:
internal_failure_count: int = 0
optional_success_count: int = 0
optional_failure_count: int = 0
- failure_messages: List[Tuple[str, str]] = dataclasses.field(default_factory=list)
- internal_failure_messages: List[Tuple[str, str]] = dataclasses.field(
+ failure_messages: list[tuple[str, str]] = dataclasses.field(default_factory=list)
+ internal_failure_messages: list[tuple[str, str]] = dataclasses.field(
default_factory=list
)
- optional_failure_messages: List[Tuple[str, str]] = dataclasses.field(
+ optional_failure_messages: list[tuple[str, str]] = dataclasses.field(
default_factory=list
)
verbosity: int = 0
@@ -146,7 +146,7 @@ def add_failure(
self.optional_failure_count += 1
self.optional_failure_messages.append((summary, message))
- pprint_types: Dict[str, Tuple[Callable, Callable]] = {
+ pprint_types: dict[str, tuple[Callable, Callable]] = {
"internal": (print_notify, print_warning),
"optional": (print, print),
}
@@ -168,7 +168,7 @@ def __init__(
self,
base_url: str,
max_retries: int = 5,
- headers: Optional[Dict[str, str]] = None,
+ headers: Optional[dict[str, str]] = None,
timeout: Optional[float] = DEFAULT_CONN_TIMEOUT,
read_timeout: Optional[float] = DEFAULT_READ_TIMEOUT,
) -> None:
@@ -267,7 +267,7 @@ def get(self, request: str):
raise ResponseError(message)
-def test_case(test_fn: Callable[..., Tuple[Any, str]]):
+def test_case(test_fn: Callable[..., tuple[Any, str]]):
"""Wrapper for test case functions, which pretty-prints any errors
depending on verbosity level, collates the number and severity of
test failures, returns the response and summary string to the caller.
@@ -404,19 +404,19 @@ def wrapper(
class ValidatorLinksResponse(Success):
meta: ResponseMeta = Field(...)
- data: List[LinksResource] = Field(...)
+ data: list[LinksResource] = Field(...)
class ValidatorEntryResponseOne(Success):
meta: ResponseMeta = Field(...)
data: EntryResource = Field(...)
- included: Optional[List[Dict[str, Any]]] = Field(None) # type: ignore[assignment]
+ included: Optional[list[dict[str, Any]]] = Field(None) # type: ignore[assignment]
class ValidatorEntryResponseMany(Success):
meta: ResponseMeta = Field(...)
- data: List[EntryResource] = Field(...)
- included: Optional[List[Dict[str, Any]]] = Field(None) # type: ignore[assignment]
+ data: list[EntryResource] = Field(...)
+ included: Optional[list[dict[str, Any]]] = Field(None) # type: ignore[assignment]
class ValidatorReferenceResponseOne(ValidatorEntryResponseOne):
@@ -424,7 +424,7 @@ class ValidatorReferenceResponseOne(ValidatorEntryResponseOne):
class ValidatorReferenceResponseMany(ValidatorEntryResponseMany):
- data: List[ReferenceResource] = Field(...)
+ data: list[ReferenceResource] = Field(...)
class ValidatorStructureResponseOne(ValidatorEntryResponseOne):
@@ -432,4 +432,4 @@ class ValidatorStructureResponseOne(ValidatorEntryResponseOne):
class ValidatorStructureResponseMany(ValidatorEntryResponseMany):
- data: List[StructureResource] = Field(...)
+ data: list[StructureResource] = Field(...)
diff --git a/optimade/validator/validator.py b/optimade/validator/validator.py
index 82f4b583a..c0526d3e0 100644
--- a/optimade/validator/validator.py
+++ b/optimade/validator/validator.py
@@ -4,7 +4,6 @@ class that can be pointed at an OPTIMADE implementation and validated
against the specification via the pydantic models implemented in this package.
"""
-# pylint: disable=import-outside-toplevel
import dataclasses
import json
@@ -13,7 +12,7 @@ class that can be pointed at an OPTIMADE implementation and validated
import re
import sys
import urllib.parse
-from typing import Any, Dict, List, Optional, Set, Tuple, Union
+from typing import Any, Literal, Optional, Union
import requests
@@ -59,7 +58,7 @@ class ImplementationValidator:
valid: Optional[bool]
- def __init__( # pylint: disable=too-many-arguments
+ def __init__(
self,
client: Optional[Any] = None,
base_url: Optional[str] = None,
@@ -72,7 +71,7 @@ def __init__( # pylint: disable=too-many-arguments
as_type: Optional[str] = None,
index: bool = False,
minimal: bool = False,
- http_headers: Optional[Dict[str, str]] = None,
+ http_headers: Optional[dict[str, str]] = None,
timeout: float = DEFAULT_CONN_TIMEOUT,
read_timeout: float = DEFAULT_READ_TIMEOUT,
):
@@ -176,8 +175,8 @@ def __init__( # pylint: disable=too-many-arguments
self.valid = None
- self._test_id_by_type: Dict[str, Any] = {}
- self._entry_info_by_type: Dict[str, Any] = {}
+ self._test_id_by_type: dict[str, Any] = {}
+ self._entry_info_by_type: dict[str, Any] = {}
self.results = ValidatorResults(verbosity=self.verbosity)
@@ -353,7 +352,7 @@ def validate_implementation(self):
self.print_summary()
@test_case
- def _recurse_through_endpoint(self, endp: str) -> Tuple[Optional[bool], str]:
+ def _recurse_through_endpoint(self, endp: str) -> tuple[Optional[bool], str]:
"""For a given endpoint (`endp`), get the entry type
and supported fields, testing that all mandatory fields
are supported, then test queries on every property according
@@ -450,8 +449,8 @@ def _test_unknown_provider_property(self, endp):
)
def _check_entry_info(
- self, entry_info: Dict[str, Any], endp: str
- ) -> Dict[str, Dict[str, Any]]:
+ self, entry_info: dict[str, Any], endp: str
+ ) -> dict[str, dict[str, Any]]:
"""Checks that `entry_info` contains all the required properties,
and returns the property list for the endpoint.
@@ -473,8 +472,8 @@ def _check_entry_info(
@test_case
def _test_must_properties(
- self, properties: List[str], endp: str
- ) -> Tuple[bool, str]:
+ self, properties: list[str], endp: str
+ ) -> tuple[bool, str]:
"""Check that the entry info lists all properties with the "MUST"
support level for this endpoint.
@@ -486,13 +485,13 @@ def _test_must_properties(
`True` if the properties were found, and a string summary.
"""
- must_props = set(
+ must_props = {
prop
for prop in CONF.entry_schemas.get(endp, {})
if CONF.entry_schemas[endp].get(prop, {}).get("support")
== SupportLevel.MUST
- )
- must_props_supported = set(prop for prop in properties if prop in must_props)
+ }
+ must_props_supported = {prop for prop in properties if prop in must_props}
missing = must_props - must_props_supported
if len(missing) != 0:
raise ResponseError(
@@ -503,8 +502,8 @@ def _test_must_properties(
@test_case
def _get_archetypal_entry(
- self, endp: str, properties: List[str]
- ) -> Tuple[Optional[Dict[str, Any]], str]:
+ self, endp: str, properties: list[str]
+ ) -> tuple[Optional[dict[str, Any]], str]:
"""Get a random entry from the first page of results for this
endpoint.
@@ -544,8 +543,8 @@ def _get_archetypal_entry(
@test_case
def _check_response_fields(
- self, endp: str, fields: List[str]
- ) -> Tuple[Optional[bool], str]:
+ self, endp: str, fields: list[str]
+ ) -> tuple[Optional[bool], str]:
"""Check that the response field query parameter is obeyed.
Parameters:
@@ -561,7 +560,7 @@ def _check_response_fields(
test_query = f"{endp}?response_fields={','.join(subset_fields)}&page_limit=1"
response, _ = self._get_endpoint(test_query, multistage=True)
- if response and len(response.json()["data"]) >= 0:
+ if response and len(response.json()["data"]) > 0:
doc = response.json()["data"][0]
expected_fields = set(subset_fields)
expected_fields -= CONF.top_level_non_attribute_fields
@@ -593,8 +592,8 @@ def _construct_queries_for_property(
prop_type: DataType,
sortable: bool,
endp: str,
- chosen_entry: Dict[str, Any],
- ) -> Tuple[Optional[bool], str]:
+ chosen_entry: dict[str, Any],
+ ) -> tuple[Optional[bool], str]:
"""For the given property, property type and chose entry, this method
runs a series of queries for each field in the entry, testing that the
initial document is returned where expected.
@@ -704,9 +703,9 @@ def _construct_single_property_filters(
prop_type: DataType,
sortable: bool,
endp: str,
- chosen_entry: Dict[str, Any],
+ chosen_entry: dict[str, Any],
query_optional: bool,
- ) -> Tuple[Optional[bool], str]:
+ ) -> tuple[Optional[bool], str]:
"""This method constructs appropriate queries using all operators
for a certain field and applies some tests:
@@ -847,7 +846,7 @@ def _construct_single_property_filters(
# if we have all results on this page, check that the blessed ID is in the response
if excluded and (
chosen_entry.get("id", "")
- in set(entry.get("id") for entry in response["data"])
+ in {entry.get("id") for entry in response["data"]}
):
raise ResponseError(
f"Entry {chosen_entry['id']} with value {prop!r}: {test_value} was not excluded by {query!r}"
@@ -953,7 +952,9 @@ def _construct_single_property_filters(
return True, f"{prop} passed filter tests"
- def _test_info_or_links_endpoint(self, request_str: str) -> Union[bool, dict]:
+ def _test_info_or_links_endpoint(
+ self, request_str: str
+ ) -> Union[Literal[False], dict]:
"""Requests an info or links endpoint and attempts to deserialize
the response.
@@ -973,7 +974,7 @@ def _test_info_or_links_endpoint(self, request_str: str) -> Union[bool, dict]:
request=request_str,
)
if deserialized:
- return deserialized.dict()
+ return deserialized.model_dump()
return False
@@ -1044,6 +1045,7 @@ def _test_multi_entry_endpoint(self, endp: str) -> None:
response, _ = self._get_endpoint(request_str)
+ self._test_if_data_empty(response, request_str, optional=True)
self._test_meta_schema_reporting(response, request_str, optional=True)
self._test_page_limit(response)
@@ -1060,7 +1062,7 @@ def _test_multi_entry_endpoint(self, endp: str) -> None:
@test_case
def _test_data_available_matches_data_returned(
self, deserialized: Any
- ) -> Tuple[Optional[bool], str]:
+ ) -> tuple[Optional[bool], str]:
"""In the case where no query is requested, `data_available`
must equal `data_returned` in the meta response, which is tested
here.
@@ -1126,7 +1128,7 @@ def _test_versions_endpoint(self):
@test_case
def _test_versions_endpoint_content(
self, response: requests.Response
- ) -> Tuple[requests.Response, str]:
+ ) -> tuple[requests.Response, str]:
"""Checks that the response from the versions endpoint complies
with the specification and that its 'Content-Type' header complies with
[RFC 4180](https://tools.ietf.org/html/rfc4180.html).
@@ -1186,9 +1188,9 @@ def _test_versions_endpoint_content(
@test_case
def _test_versions_headers(
self,
- content_type: Dict[str, Any],
- expected_parameter: Union[str, List[str]],
- ) -> Tuple[Dict[str, Any], str]:
+ content_type: dict[str, Any],
+ expected_parameter: Union[str, list[str]],
+ ) -> tuple[dict[str, Any], str]:
"""Tests that the `Content-Type` field of the `/versions` header contains
the passed parameter.
@@ -1243,6 +1245,29 @@ def _test_bad_version_returns_553(self) -> None:
"v123123/info", expected_status_code=expected_status_code, optional=True
)
+ @test_case
+ def _test_if_data_empty(
+ self,
+ response: requests.models.Response,
+ request_str: str,
+ ):
+ """Tests whether an endpoint responds a entries under `data`."""
+ try:
+ if not response.json().get("data", []):
+ raise ResponseError(
+ f"Query {request_str} did not respond with any entries under `data`. This may not consitute an error, but should be checked."
+ )
+
+ except json.JSONDecodeError:
+ raise ResponseError(
+ f"Unable to test presence of `data` for query {request_str}: could not decode response as JSON.\n{str(response.content)}"
+ )
+
+ return (
+ True,
+ f"Query {request_str} successfully returned some entries.",
+ )
+
@test_case
def _test_meta_schema_reporting(
self,
@@ -1270,8 +1295,8 @@ def _test_page_limit(
self,
response: requests.models.Response,
check_next_link: int = 5,
- previous_links: Optional[Set[str]] = None,
- ) -> Tuple[Optional[bool], str]:
+ previous_links: Optional[set[str]] = None,
+ ) -> tuple[Optional[bool], str]:
"""Test that a multi-entry endpoint obeys the page limit by
following pagination links up to a depth of `check_next_link`.
@@ -1387,7 +1412,7 @@ def _deserialize_response(
response: requests.models.Response,
response_cls: Any,
request: Optional[str] = None,
- ) -> Tuple[Any, str]:
+ ) -> tuple[Any, str]:
"""Try to create the appropriate pydantic model from the response.
Parameters:
@@ -1416,13 +1441,13 @@ def _deserialize_response(
return (
response_cls(**json_response),
- "deserialized correctly as object of type {}".format(response_cls),
+ f"deserialized correctly as object of type {response_cls}",
)
@test_case
def _get_available_endpoints(
- self, base_info: Union[Any, Dict[str, Any]]
- ) -> Tuple[Optional[List[str]], str]:
+ self, base_info: Union[Any, dict[str, Any]]
+ ) -> tuple[Optional[list[str]], str]:
"""Tries to get `entry_types_by_format` from base info response
even if it could not be deserialized.
@@ -1478,8 +1503,8 @@ def _get_available_endpoints(
@test_case
def _get_endpoint(
- self, request_str: str, expected_status_code: Union[List[int], int] = 200
- ) -> Tuple[Optional[requests.Response], str]:
+ self, request_str: str, expected_status_code: Union[list[int], int] = 200
+ ) -> tuple[Optional[requests.Response], str]:
"""Gets the response from the endpoint specified by `request_str`.
function is wrapped by the `test_case` decorator
diff --git a/optimade_config.json b/optimade_config.json
index e22d9e3b0..776949fa4 100644
--- a/optimade_config.json
+++ b/optimade_config.json
@@ -6,6 +6,7 @@
"name": "Example implementation",
"source_url": "https://github.com/Materials-Consortia/optimade-python-tools",
"issue_tracker": "https://github.com/Materials-Consortia/optimade-python-tools/issues",
+ "homepage": "https://optimade.org/optimade-python-tools",
"maintainer": {"email": "dev@optimade.org"}
},
"provider": {
diff --git a/providers b/providers
index 564a49999..ad0e21417 160000
--- a/providers
+++ b/providers
@@ -1 +1 @@
-Subproject commit 564a499994de16c8480ea9378f93b94f51298b97
+Subproject commit ad0e21417adbadddfe37ca74666349d687f783a2
diff --git a/pyproject.toml b/pyproject.toml
index 1cbfffc3a..f9701386d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -16,6 +16,7 @@ classifiers = [
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
"Intended Audience :: Developers",
"Intended Audience :: Science/Research",
"Topic :: Database",
@@ -26,8 +27,8 @@ classifiers = [
requires-python = ">=3.9"
dependencies = [
"lark~=1.1",
- "pydantic~=1.10,>=1.10.2,!=1.10.7",
- "email_validator>=1.2",
+ "pydantic[email]~=2.2",
+ "pydantic-settings~=2.0",
"requests~=2.28",
]
@@ -73,65 +74,61 @@ server = [
# Client minded
aiida = ["aiida-core~=2.1"]
-http_client = [
+http-client = [
"httpx~=0.23",
"rich~=13.0",
"click~=8.1",
]
ase = ["ase~=3.22"]
-cif = ["numpy>=1.20"]
-# we don't support pydantic v2 yet, but pymatgen has a (uncessary IMO) tight coupling to mp-api which now enforces it
-# as "true" users of pydantic, if we can't update our stuff to v2 (including all the fieldinfo hacks) then we will
-# probably just have to hard pin or remove pymatgen support
-pymatgen = [
- "pymatgen>=2022",
- "mp-api<=0.36",
- "emmet-core<=0.68"
-]
+cif = ["numpy~=1.22"]
+pymatgen = ["pymatgen>=2022", "pandas~=2.2"]
jarvis = ["jarvis-tools>=2023.1.8"]
client = ["optimade[cif]"]
# General
docs = [
- "mike~=1.1",
+ "mike~=2.0",
"mkdocs~=1.4",
"mkdocs-awesome-pages-plugin~=2.8",
"mkdocs-material~=9.0",
- "mkdocstrings[python-legacy]~=0.20",
+ "mkdocstrings[python]~=0.20",
]
testing = [
"build~=1.0",
"jsondiff~=2.0",
- "pytest~=7.2",
- "pytest-cov~=4.0",
+ "pytest>=7.2,<9.0",
+ "pytest-cov>=4,<6",
"optimade[server]",
]
dev = [
- "black~=23.1",
- "isort~=5.12",
"mypy~=1.0",
"pre-commit~=3.0",
"invoke~=2.0",
"types-all==1.0.0",
- "ruff~=0.0",
- "optimade[docs,testing,client,http_client]"
+ "ruff~=0.1",
+ "optimade[docs,testing,client,http-client]"
]
-all = ["optimade[dev,elastic,aiida,ase,pymatgen,jarvis,http_client,client]"]
+http_client = [
+ "optimade[http-client]"
+]
+all = ["optimade[dev,elastic,aiida,ase,pymatgen,jarvis,http-client,client]"]
[tool.ruff]
-select = ["E", "F", "I", "W", "Q"]
-ignore = ["E501", "E402"]
-fixable = ["A", "B", "C", "D", "E", "F", "I"]
-unfixable = []
extend-exclude = [
"providers",
]
target-version = "py310"
+
+[tool.ruff.lint]
+select = ["E", "F", "I", "W", "Q"]
+ignore = ["E501", "E402"]
+fixable = ["A", "B", "C", "D", "E", "F", "I"]
+unfixable = []
per-file-ignores = {}
# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
@@ -142,6 +139,8 @@ ignore_missing_imports = true
follow_imports = "skip"
[tool.pytest.ini_options]
+testpaths = "tests"
+addopts = "-rs"
filterwarnings = [
"error",
"ignore:.*flaky.*:UserWarning",
@@ -157,10 +156,12 @@ filterwarnings = [
"ignore:.*has an unrecognised prefix.*:",
"ignore:.*pkg_resources is deprecated as an API*:DeprecationWarning",
"ignore:.*Deprecated call to `pkg_resources.declare_namespace*:DeprecationWarning",
+ "ignore:.*datetime.datetime.utcfromtimestamp()*:DeprecationWarning", # Raised indirectly by elasticsearch-DSL for Python 3.12
+ "ignore:.*ast.Num is deprecated and will be removed in Python 3.14*:DeprecationWarning", # Raised indirectly by aiida for Python 3.12
+ "ignore:.*ast.Str is deprecated and will be removed in Python 3.14*:DeprecationWarning", # Raised indirectly by aiida for Python 3.12
+ "ignore:.*ast.* is deprecated and will be removed in Python 3.14*:DeprecationWarning", # Raised indirectly by aiida for Python 3.12
+ "ignore:\\nPyarrow will become a required dependency of pandas.*:DeprecationWarning", # Raised indirectly by pymatgen for Python 3.12
+ "ignore:.*The 'app' shortcut is now deprecated.*:DeprecationWarning", # Raised by httpx for test client code; would rather not force upgrade of httpx to deal with it for now
+ "ignore::pytest.PytestUnraisableExceptionWarning", # Started raising with pytest 8.2.1, seemingly related to anyio warnings
+ "ignore:.*Unclosed.*:ResourceWarning", # Also started raising with pytest 8.2.1, seemingly related to anyio warnings
]
-testpaths = "tests"
-addopts = "-rs"
-
-[tool.isort]
-known_first_party = "optimade"
-profile = "black"
diff --git a/requirements-client.txt b/requirements-client.txt
index 2c0b7f4f4..6f32ef48b 100644
--- a/requirements-client.txt
+++ b/requirements-client.txt
@@ -1,8 +1,5 @@
-aiida-core==2.4.0
-ase==3.22.1
-emmet_core==0.68.0
-jarvis-tools==2023.9.20
-jarvis-tools==2023.9.20
-mp-api==0.36.1
+aiida-core==2.5.1
+ase==3.23.0
+jarvis-tools==2024.4.10
numpy>=1.20
-pymatgen==2023.9.10
+pymatgen==2024.5.1
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 33423d46c..0afbb8a37 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,13 +1,9 @@
-black==23.9.1
-build==1.0.3
-flake8==6.1.0
+build==1.2.1
invoke==2.2.0
-isort==5.12.0
jsondiff==2.0.0
-mypy==1.5.1
-pre-commit==3.4.0
-pylint==2.17.5
-pytest==7.4.2
-pytest-cov==4.1.0
-ruff==0.0.291
+mypy==1.10.0
+pre-commit==3.7.1
+pytest==8.2.1
+pytest-cov==5.0.0
+ruff==0.4.7
types-all==1.0.0
diff --git a/requirements-docs.txt b/requirements-docs.txt
index e8af19873..bd613dada 100644
--- a/requirements-docs.txt
+++ b/requirements-docs.txt
@@ -1,5 +1,5 @@
-mike==1.1.2
-mkdocs==1.5.3
+mike==2.1.1
+mkdocs==1.6.0
mkdocs-awesome-pages-plugin==2.9.2
-mkdocs-material==9.4.2
-mkdocstrings[python-legacy]==0.23.0
+mkdocs-material==9.5.25
+mkdocstrings[python]==0.25.1
diff --git a/requirements-http-client.txt b/requirements-http-client.txt
index 9e731df04..8ec758924 100644
--- a/requirements-http-client.txt
+++ b/requirements-http-client.txt
@@ -1,3 +1,3 @@
click==8.1.7
-httpx==0.25.0
-rich==13.5.3
+httpx==0.27.0
+rich==13.7.1
diff --git a/requirements-server.txt b/requirements-server.txt
index 04f5dc957..b7174f8b9 100644
--- a/requirements-server.txt
+++ b/requirements-server.txt
@@ -1,5 +1,5 @@
elasticsearch==7.17.7
elasticsearch-dsl==7.4.0
-fastapi==0.103.1
+fastapi==0.111.0
mongomock==4.1.2
-pymongo==4.5.0
+pymongo==4.7.2
diff --git a/requirements.txt b/requirements.txt
index d751af95f..c93fa7a76 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,6 +1,6 @@
-email_validator==2.0.0.post2
-lark==1.1.7
-pydantic==1.10.9
-pyyaml==6.0
-requests==2.31.0
-uvicorn==0.23.2
+lark==1.1.9
+pydantic[email]==2.7.2
+pydantic_settings==2.2.1
+pyyaml==6.0.1
+requests==2.32.3
+uvicorn==0.30.1
diff --git a/tasks.py b/tasks.py
index 216cdd753..1455d8bea 100644
--- a/tasks.py
+++ b/tasks.py
@@ -3,7 +3,7 @@
import re
import sys
from pathlib import Path
-from typing import TYPE_CHECKING, Optional, Tuple
+from typing import TYPE_CHECKING, Optional, Union
from invoke import task
from jsondiff import diff
@@ -15,9 +15,11 @@
TOP_DIR = Path(__file__).parent.resolve()
-def update_file(filename: str, sub_line: Tuple[str, str], strip: Optional[str] = None):
+def update_file(
+ filename: Union[Path, str], sub_line: tuple[str, str], strip: Optional[str] = None
+):
"""Utility function for tasks to read, update, and write files"""
- with open(filename, "r") as handle:
+ with open(filename) as handle:
lines = [
re.sub(sub_line[0], sub_line[1], line.rstrip(strip)) for line in handle
]
@@ -119,7 +121,7 @@ def setver(_, ver=""):
(r'"version": ".*",', f'"version": "{ver}",'),
)
- print("Bumped version to {}".format(ver))
+ print(f"Bumped version to {ver}")
@task(help={"ver": "OPTIMADE API version to set"}, post=[update_openapijson])
@@ -191,7 +193,7 @@ def create_api_reference_docs(context, pre_clean=False, pre_commit=False):
def write_file(full_path: Path, content: str):
"""Write file with `content` to `full_path`"""
if full_path.exists():
- with open(full_path, "r") as handle:
+ with open(full_path) as handle:
cached_content = handle.read()
if content == cached_content:
del cached_content
@@ -273,7 +275,7 @@ def write_file(full_path: Path, content: str):
# Check if there have been any changes.
# List changes if yes.
if TYPE_CHECKING:
- context: "Context" = context
+ context: "Context" = context # type: ignore[no-redef]
# NOTE: grep returns an exit code of 1 if it doesn't find anything
# (which will be good in this case).
@@ -306,7 +308,7 @@ def print_error(string):
print(f"\033[31m{line}\033[0m")
swagger_url = "https://validator.swagger.io/validator/debug"
- with open(fname, "r") as f:
+ with open(fname) as f:
schema = json.load(f)
response = requests.post(swagger_url, json=schema)
diff --git a/tests/adapters/references/conftest.py b/tests/adapters/references/conftest.py
index 52fbe3f36..5347e04f2 100644
--- a/tests/adapters/references/conftest.py
+++ b/tests/adapters/references/conftest.py
@@ -1,28 +1,35 @@
-import json
-from pathlib import Path
-from random import choice
+from typing import TYPE_CHECKING
import pytest
-from optimade.adapters.references import Reference
+if TYPE_CHECKING:
+ from typing import Any
+
+ from optimade.adapters.references import Reference
@pytest.fixture
-def RAW_REFERENCES():
+def RAW_REFERENCES() -> "list[dict[str, Any]]":
"""Read and return raw_references.json"""
- with open(
- Path(__file__).parent.joinpath("raw_test_references.json"), "r"
- ) as raw_data:
- return json.load(raw_data)
+ import json
+ from pathlib import Path
+
+ return json.loads(
+ Path(__file__).parent.joinpath("raw_test_references.json").read_bytes()
+ )
@pytest.fixture
-def raw_reference(RAW_REFERENCES):
+def raw_reference(RAW_REFERENCES: "list[dict[str, Any]]") -> "dict[str, Any]":
"""Return random raw reference from raw_references.json"""
+ from random import choice
+
return choice(RAW_REFERENCES)
@pytest.fixture
-def reference(raw_reference):
+def reference(raw_reference: "dict[str, Any]") -> "Reference":
"""Create and return adapters.Reference"""
+ from optimade.adapters.references import Reference
+
return Reference(raw_reference)
diff --git a/tests/adapters/references/test_references.py b/tests/adapters/references/test_references.py
index 7b500d265..25c2e96ee 100644
--- a/tests/adapters/references/test_references.py
+++ b/tests/adapters/references/test_references.py
@@ -1,26 +1,36 @@
"""Test Reference adapter"""
+
+from typing import TYPE_CHECKING
+
import pytest
-from optimade.adapters import Reference
-from optimade.models import ReferenceResource
+if TYPE_CHECKING:
+ from typing import Any, Union
+
+ from optimade.adapters.references import Reference
-def test_instantiate(RAW_REFERENCES):
+def test_instantiate(RAW_REFERENCES: "list[dict[str, Any]]") -> None:
"""Try instantiating Reference for all raw test references"""
+ from optimade.adapters.references import Reference
+ from optimade.models.references import ReferenceResource
+
for reference in RAW_REFERENCES:
new_Reference = Reference(reference)
assert isinstance(new_Reference.entry, ReferenceResource)
-def test_setting_entry(caplog, RAW_REFERENCES):
+def test_setting_entry(RAW_REFERENCES: "list[dict[str, Any]]") -> None:
"""Make sure entry can only be set once"""
+ from optimade.adapters.references import Reference
+
reference = Reference(RAW_REFERENCES[0])
- reference.entry = RAW_REFERENCES[1]
- assert "entry can only be set once and is already set." in caplog.text
+ with pytest.raises(AttributeError):
+ reference.entry = RAW_REFERENCES[1]
@pytest.mark.skip("Currently, there are no conversion types available for references")
-def test_convert(reference):
+def test_convert(reference: "Reference") -> None:
"""Test convert() works
Choose currently known entry type - must be updated if no longer available.
"""
@@ -40,15 +50,16 @@ def test_convert(reference):
assert converted_reference == reference._converted[chosen_type]
-def test_convert_wrong_format(reference):
+def test_convert_wrong_format(reference: "Reference") -> None:
"""Test AttributeError is raised if format does not exist"""
- nonexistant_format = 0
+ nonexistant_format: "Union[str, int]" = 0
right_wrong_format_found = False
while not right_wrong_format_found:
if str(nonexistant_format) not in reference._type_converters:
nonexistant_format = str(nonexistant_format)
right_wrong_format_found = True
else:
+ assert isinstance(nonexistant_format, int)
nonexistant_format += 1
with pytest.raises(
@@ -58,7 +69,7 @@ def test_convert_wrong_format(reference):
reference.convert(nonexistant_format)
-def test_getattr_order(reference):
+def test_getattr_order(reference: "Reference") -> None:
"""The order of getting an attribute should be:
1. `as_`
2. ``
diff --git a/tests/adapters/structures/conftest.py b/tests/adapters/structures/conftest.py
index 44afd7e47..49c9706d9 100644
--- a/tests/adapters/structures/conftest.py
+++ b/tests/adapters/structures/conftest.py
@@ -1,50 +1,64 @@
-import json
-from pathlib import Path
-from random import choice
-from typing import List
+from typing import TYPE_CHECKING
import pytest
-from optimade.adapters.structures import Structure
+if TYPE_CHECKING:
+ from typing import Any
+
+ from optimade.adapters.structures import Structure
@pytest.fixture
-def RAW_STRUCTURES() -> List[dict]:
+def RAW_STRUCTURES() -> "list[dict[str, Any]]":
"""Read and return raw_structures.json"""
- with open(
- Path(__file__).parent.joinpath("raw_test_structures.json"), "r"
- ) as raw_data:
- return json.load(raw_data)
+ import json
+ from pathlib import Path
+
+ return json.loads(
+ Path(__file__).parent.joinpath("raw_test_structures.json").read_bytes()
+ )
@pytest.fixture
-def SPECIAL_SPECIES_STRUCTURES() -> List[dict]:
+def SPECIAL_SPECIES_STRUCTURES() -> "list[dict[str, Any]]":
"""Read and return special_species.json"""
- with open(Path(__file__).parent.joinpath("special_species.json"), "r") as raw_data:
- return json.load(raw_data)
+ import json
+ from pathlib import Path
+
+ return json.loads(
+ Path(__file__).parent.joinpath("special_species.json").read_bytes()
+ )
@pytest.fixture
-def raw_structure(RAW_STRUCTURES) -> dict:
+def raw_structure(RAW_STRUCTURES: "list[dict[str, Any]]") -> "dict[str, Any]":
"""Return random raw structure from raw_structures.json"""
+ from random import choice
+
return choice(RAW_STRUCTURES)
@pytest.fixture
-def structure(raw_structure) -> Structure:
+def structure(raw_structure: "dict[str, Any]") -> "Structure":
"""Create and return adapters.Structure"""
+ from optimade.adapters.structures import Structure
+
return Structure(raw_structure)
@pytest.fixture
-def structures(RAW_STRUCTURES) -> List[Structure]:
+def structures(RAW_STRUCTURES: "list[dict[str, Any]]") -> "list[Structure]":
"""Create and return list of adapters.Structure"""
+ from optimade.adapters.structures import Structure
+
return [Structure(_) for _ in RAW_STRUCTURES]
@pytest.fixture
-def null_lattice_vector_structure(raw_structure) -> Structure:
+def null_lattice_vector_structure(raw_structure: "dict[str, Any]") -> "Structure":
"""Create and return adapters.Structure with lattice_vectors that have None values"""
+ from optimade.adapters.structures import Structure
+
raw_structure["attributes"]["lattice_vectors"][0] = [None] * 3
raw_structure["attributes"]["dimension_types"][0] = 0
raw_structure["attributes"]["nperiodic_dimensions"] = sum(
@@ -54,6 +68,9 @@ def null_lattice_vector_structure(raw_structure) -> Structure:
@pytest.fixture
-def null_species_structure(raw_structure) -> Structure:
+def null_species_structure(raw_structure: "dict[str, Any]") -> "Structure":
+ """Create and return Structure with species that have None values"""
+ from optimade.adapters.structures import Structure
+
raw_structure["attributes"]["species"] = None
return Structure(raw_structure)
diff --git a/tests/adapters/structures/special_species.json b/tests/adapters/structures/special_species.json
index 928c66806..8ee6579d9 100644
--- a/tests/adapters/structures/special_species.json
+++ b/tests/adapters/structures/special_species.json
@@ -46,7 +46,7 @@
],
"nsites": 1,
"species_at_sites": [
- "Ac"
+ "Ac"
],
"species": [
{
diff --git a/tests/adapters/structures/test_aiida.py b/tests/adapters/structures/test_aiida.py
index 4aeedf95c..a16377eb0 100644
--- a/tests/adapters/structures/test_aiida.py
+++ b/tests/adapters/structures/test_aiida.py
@@ -1,5 +1,3 @@
-# pylint: disable=import-error
-
import pytest
from .utils import get_min_ver
diff --git a/tests/adapters/structures/test_ase.py b/tests/adapters/structures/test_ase.py
index 8e4103c84..939d0553d 100644
--- a/tests/adapters/structures/test_ase.py
+++ b/tests/adapters/structures/test_ase.py
@@ -1,4 +1,3 @@
-# pylint: disable=import-error
import pytest
from .utils import get_min_ver
@@ -59,7 +58,7 @@ def test_extra_info_keys(RAW_STRUCTURES):
assert atoms.info["another_key"] == [1, 2, 3]
assert atoms.info["_key_without_ase_prefix"] == [4, 5, 6]
- roundtrip_structure = Structure.ingest_from(atoms).attributes.dict()
+ roundtrip_structure = Structure.ingest_from(atoms).attributes.model_dump()
assert roundtrip_structure["_ase_key"] == "some value"
assert roundtrip_structure["_ase_another_key"] == [1, 2, 3]
diff --git a/tests/adapters/structures/test_cif.py b/tests/adapters/structures/test_cif.py
index 60b4bcb7a..a88014264 100644
--- a/tests/adapters/structures/test_cif.py
+++ b/tests/adapters/structures/test_cif.py
@@ -1,5 +1,3 @@
-# pylint: disable=import-error
-
import pytest
from .utils import get_min_ver
diff --git a/tests/adapters/structures/test_jarvis.py b/tests/adapters/structures/test_jarvis.py
index 5a44820c7..209d88f58 100644
--- a/tests/adapters/structures/test_jarvis.py
+++ b/tests/adapters/structures/test_jarvis.py
@@ -1,4 +1,3 @@
-# pylint: disable=import-error
import pytest
from .utils import get_min_ver
diff --git a/tests/adapters/structures/test_pdb.py b/tests/adapters/structures/test_pdb.py
index 9920488d8..e95af1db7 100644
--- a/tests/adapters/structures/test_pdb.py
+++ b/tests/adapters/structures/test_pdb.py
@@ -1,4 +1,3 @@
-# pylint: disable=import-error
import pytest
from .utils import get_min_ver
diff --git a/tests/adapters/structures/test_pdbx_mmcif.py b/tests/adapters/structures/test_pdbx_mmcif.py
index bce2ace4f..d4bad3f6e 100644
--- a/tests/adapters/structures/test_pdbx_mmcif.py
+++ b/tests/adapters/structures/test_pdbx_mmcif.py
@@ -1,4 +1,3 @@
-# pylint: disable=import-error
import pytest
from .utils import get_min_ver
diff --git a/tests/adapters/structures/test_pymatgen.py b/tests/adapters/structures/test_pymatgen.py
index f51c66491..d674155c9 100644
--- a/tests/adapters/structures/test_pymatgen.py
+++ b/tests/adapters/structures/test_pymatgen.py
@@ -1,4 +1,3 @@
-# pylint: disable=import-error
import pytest
from .utils import get_min_ver
diff --git a/tests/adapters/structures/test_structures.py b/tests/adapters/structures/test_structures.py
index 0a96f57bc..cf9ac1d65 100644
--- a/tests/adapters/structures/test_structures.py
+++ b/tests/adapters/structures/test_structures.py
@@ -1,9 +1,8 @@
"""Test Structure adapter"""
-import pytest
+from typing import TYPE_CHECKING
-from optimade.adapters import Structure
-from optimade.models import StructureResource
+import pytest
try:
import aiida # noqa: F401
@@ -16,22 +15,32 @@
else:
all_modules_found = True
+if TYPE_CHECKING:
+ from typing import Any, Union
+
+ from optimade.adapters.structures import Structure
-def test_instantiate(RAW_STRUCTURES):
+
+def test_instantiate(RAW_STRUCTURES: "list[dict[str, Any]]") -> None:
"""Try instantiating Structure for all raw test structures"""
+ from optimade.adapters.structures import Structure
+ from optimade.models.structures import StructureResource
+
for structure in RAW_STRUCTURES:
new_Structure = Structure(structure)
assert isinstance(new_Structure.entry, StructureResource)
-def test_setting_entry(caplog, RAW_STRUCTURES):
+def test_setting_entry(RAW_STRUCTURES: "list[dict[str, Any]]") -> None:
"""Make sure entry can only be set once"""
+ from optimade.adapters.structures import Structure
+
structure = Structure(RAW_STRUCTURES[0])
- structure.entry = RAW_STRUCTURES[1]
- assert "entry can only be set once and is already set." in caplog.text
+ with pytest.raises(AttributeError):
+ structure.entry = RAW_STRUCTURES[1]
-def test_convert(structure):
+def test_convert(structure: "Structure") -> None:
"""Test convert() works
Choose currently known entry type - must be updated if no longer available.
"""
@@ -52,15 +61,16 @@ def test_convert(structure):
assert converted_structure == structure._converted[chosen_type]
-def test_convert_wrong_format(structure):
+def test_convert_wrong_format(structure: "Structure") -> None:
"""Test AttributeError is raised if format does not exist"""
- nonexistant_format = 0
+ nonexistant_format: "Union[int, str]" = 0
right_wrong_format_found = False
while not right_wrong_format_found:
if str(nonexistant_format) not in structure._type_converters:
nonexistant_format = str(nonexistant_format)
right_wrong_format_found = True
else:
+ assert isinstance(nonexistant_format, int)
nonexistant_format += 1
with pytest.raises(
@@ -70,7 +80,7 @@ def test_convert_wrong_format(structure):
structure.convert(nonexistant_format)
-def test_getattr_order(structure):
+def test_getattr_order(structure: "Structure") -> None:
"""The order of getting an attribute should be:
1. `as_`
2. ``
@@ -101,7 +111,7 @@ def test_getattr_order(structure):
reason="This test checks what happens if a conversion-dependent module cannot be found. "
"All could be found, i.e., it has no meaning.",
)
-def test_no_module_conversion(structure):
+def test_no_module_conversion(structure: "Structure") -> None:
"""Make sure a warnings is raised and None is returned for conversions with non-existing modules"""
import importlib
@@ -134,22 +144,31 @@ def test_no_module_conversion(structure):
assert converted_structure is None
-def test_common_converters(raw_structure, RAW_STRUCTURES):
+def test_common_converters(
+ raw_structure: "dict[str, Any]", RAW_STRUCTURES: "list[dict[str, Any]]"
+) -> None:
"""Test common converters"""
+ from optimade.adapters.structures import Structure
+ from optimade.models.structures import StructureResource
+
structure = Structure(raw_structure)
- assert structure.as_json == StructureResource(**raw_structure).json()
- assert structure.as_dict == StructureResource(**raw_structure).dict()
+ assert structure.as_json == StructureResource(**raw_structure).model_dump_json()
+ assert structure.as_dict == StructureResource(**raw_structure).model_dump()
- # Since calling .dict() and .json() will return also all default-valued properties,
- # the raw structure should at least be a sub-set of the resource's full list of properties.
+ # Since calling .model_dump() and .model_dump_json() will return also all
+ # default-valued properties, the raw structure should at least be a sub-set of the
+ # resource's full list of properties.
for raw_structure in RAW_STRUCTURES:
raw_structure_property_set = set(raw_structure.keys())
resource_property_set = set(Structure(raw_structure).as_dict.keys())
assert raw_structure_property_set.issubset(resource_property_set)
-def compare_lossy_conversion(structure_attributes, reconverted_structure_attributes):
+def compare_lossy_conversion(
+ structure_attributes: "dict[str, Any]",
+ reconverted_structure_attributes: "dict[str, Any]",
+) -> None:
"""Compare two structures, allowing for some loss of information and mapping of prefixed keys."""
try:
@@ -197,11 +216,25 @@ def compare_lossy_conversion(structure_attributes, reconverted_structure_attribu
assert reconverted_structure_attributes[k] == structure_attributes[k]
+def _get_formats() -> "list[str]":
+ """Get all available formats"""
+ from optimade.adapters.structures import Structure
+
+ return [
+ k for k in Structure._type_ingesters.keys() if k in Structure._type_converters
+ ]
+
+
@pytest.mark.parametrize(
"format",
- [k for k in Structure._type_ingesters.keys() if k in Structure._type_converters],
+ _get_formats(),
)
-def test_two_way_conversion(RAW_STRUCTURES, format):
+def test_two_way_conversion(
+ RAW_STRUCTURES: "list[dict[str, Any]]", format: str
+) -> None:
+ """Test two-way conversion"""
+ from optimade.adapters.structures import Structure
+
for structure in RAW_STRUCTURES:
new_structure = Structure(structure)
converted_structure = new_structure.convert(format)
@@ -209,7 +242,7 @@ def test_two_way_conversion(RAW_STRUCTURES, format):
continue
reconverted_structure = Structure.ingest_from(
converted_structure, format
- ).entry.dict()
+ ).entry.model_dump()
compare_lossy_conversion(
structure["attributes"], reconverted_structure["attributes"]
)
@@ -217,9 +250,14 @@ def test_two_way_conversion(RAW_STRUCTURES, format):
@pytest.mark.parametrize(
"format",
- [k for k in Structure._type_ingesters.keys() if k in Structure._type_converters],
+ _get_formats(),
)
-def test_two_way_conversion_with_implicit_type(RAW_STRUCTURES, format):
+def test_two_way_conversion_with_implicit_type(
+ RAW_STRUCTURES: "list[dict[str, Any]]", format: str
+) -> None:
+ """Test two-way conversion with implicit type"""
+ from optimade.adapters.structures import Structure
+
for structure in RAW_STRUCTURES:
new_structure = Structure(structure)
converted_structure = new_structure.convert(format)
@@ -227,7 +265,7 @@ def test_two_way_conversion_with_implicit_type(RAW_STRUCTURES, format):
continue
reconverted_structure = Structure.ingest_from(
converted_structure, format=None
- ).entry.dict()
+ ).entry.model_dump()
compare_lossy_conversion(
structure["attributes"], reconverted_structure["attributes"]
diff --git a/tests/adapters/structures/test_utils.py b/tests/adapters/structures/test_utils.py
index 107c90b18..f8d83a1b0 100644
--- a/tests/adapters/structures/test_utils.py
+++ b/tests/adapters/structures/test_utils.py
@@ -123,7 +123,9 @@ def test_scaled_cell_consistency(structures):
def test_species_from_species_at_sites():
"""Test that species can be inferred from species_at_sites"""
species_at_sites = ["Si"]
- assert [d.dict() for d in species_from_species_at_sites(species_at_sites)] == [
+ assert [
+ d.model_dump() for d in species_from_species_at_sites(species_at_sites)
+ ] == [
{
"name": "Si",
"concentration": [1.0],
@@ -137,7 +139,7 @@ def test_species_from_species_at_sites():
species_at_sites = ["Si", "Si", "O", "O", "O", "O"]
assert sorted(
- [d.dict() for d in species_from_species_at_sites(species_at_sites)],
+ [d.model_dump() for d in species_from_species_at_sites(species_at_sites)],
key=lambda _: _["name"],
) == sorted(
[
diff --git a/tests/adapters/structures/utils.py b/tests/adapters/structures/utils.py
index b11df3182..ae7fc9ed3 100644
--- a/tests/adapters/structures/utils.py
+++ b/tests/adapters/structures/utils.py
@@ -5,7 +5,7 @@
def get_min_ver(dependency: str) -> str:
"""Retrieve version of `dependency` from setup.py, raise if not found."""
pyproject_toml = Path(__file__).parent.joinpath("../../../pyproject.toml")
- with open(pyproject_toml, "r") as setup_file:
+ with open(pyproject_toml) as setup_file:
for line in setup_file.readlines():
min_ver = re.findall(rf'"{dependency}((=|!|<|>|~)=|>|<)(.+)"', line)
if min_ver:
diff --git a/tests/conftest.py b/tests/conftest.py
index 266f5adb7..33b2d9279 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,8 +1,17 @@
import os
from pathlib import Path
+from typing import TYPE_CHECKING
import pytest
+if TYPE_CHECKING:
+ from typing import Protocol
+
+ from optimade.server.mappers import BaseResourceMapper
+
+ class MapperInner(Protocol):
+ def __call__(self, name: str) -> type[BaseResourceMapper]: ...
+
def pytest_configure(config):
"""Method that runs before pytest collects tests so no modules are imported"""
@@ -17,17 +26,16 @@ def top_dir() -> Path:
@pytest.fixture(scope="module")
-def mapper():
+def mapper() -> "MapperInner":
"""Mapper-factory to import a mapper from optimade.server.mappers"""
from optimade.server import mappers
- def _mapper(name: str) -> mappers.BaseResourceMapper: # type: ignore[return]
+ def _mapper(name: str) -> type[mappers.BaseResourceMapper]:
"""Return named resource mapper"""
try:
- res = getattr(mappers, name)
+ res: type[mappers.BaseResourceMapper] = getattr(mappers, name)
except AttributeError:
pytest.fail(f"Could not retrieve {name!r} from optimade.server.mappers.")
- else:
- return res
+ return res
return _mapper
diff --git a/tests/filterparser/test_filterparser.py b/tests/filterparser/test_filterparser.py
index 5296bf797..5a01130b9 100644
--- a/tests/filterparser/test_filterparser.py
+++ b/tests/filterparser/test_filterparser.py
@@ -1,5 +1,4 @@
import abc
-from typing import Tuple
import pytest
from lark import Tree
@@ -11,7 +10,7 @@
class BaseTestFilterParser(abc.ABC):
"""Base class for parsing different versions of the grammar using `LarkParser`."""
- version: Tuple[int, int, int]
+ version: tuple[int, int, int]
variant: str = "default"
@pytest.fixture(autouse=True)
diff --git a/tests/filtertransformers/test_base.py b/tests/filtertransformers/test_base.py
index fda20728f..57ef9129e 100644
--- a/tests/filtertransformers/test_base.py
+++ b/tests/filtertransformers/test_base.py
@@ -1,7 +1,16 @@
-from optimade.filtertransformers import BaseTransformer
+"""Tests for optimade.filtertransformers.BaseTransformer"""
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from typing import Callable
+
+ from optimade.server.mappers import BaseResourceMapper
+
+
+def test_quantity_builder(mapper: "Callable[[str], type[BaseResourceMapper]]") -> None:
+ from optimade.filtertransformers.base_transformer import BaseTransformer, Quantity
-def test_quantity_builder(mapper):
class DummyTransformer(BaseTransformer):
pass
@@ -18,9 +27,14 @@ class AwkwardMapper(mapper("StructureMapper")):
t = DummyTransformer(mapper=m)
assert "_exmpl_chemsys" in t.quantities
+ assert isinstance(t.quantities["_exmpl_chemsys"], Quantity)
assert t.quantities["_exmpl_chemsys"].name == "_exmpl_chemsys"
assert t.quantities["_exmpl_chemsys"].backend_field == "chemsys"
+
+ assert isinstance(t.quantities["_exmpl_chemsys"].length_quantity, Quantity)
assert t.quantities["_exmpl_chemsys"].length_quantity.name == "nelements"
assert t.quantities["_exmpl_chemsys"].length_quantity.backend_field == "nelem"
+ assert isinstance(t.quantities["elements"], Quantity)
+ assert isinstance(t.quantities["elements"].length_quantity, Quantity)
assert t.quantities["elements"].length_quantity.backend_field == "nelem"
diff --git a/tests/models/conftest.py b/tests/models/conftest.py
index a6cc48564..461c8d5a3 100644
--- a/tests/models/conftest.py
+++ b/tests/models/conftest.py
@@ -12,7 +12,7 @@ def load_test_data(filename: str) -> list:
if not json_file_path.exists():
raise RuntimeError(f"Could not find {filename!r} in 'tests.models.test_data'")
- with open(json_file_path, "r") as handle:
+ with open(json_file_path) as handle:
data = json.load(handle)
return data
diff --git a/tests/models/test_baseinfo.py b/tests/models/test_baseinfo.py
index e7230f297..6d24a1166 100644
--- a/tests/models/test_baseinfo.py
+++ b/tests/models/test_baseinfo.py
@@ -1,4 +1,5 @@
import pytest
+from pydantic import ValidationError
from optimade.models.baseinfo import AvailableApiVersion
@@ -39,27 +40,20 @@ def test_available_api_versions():
]
for data in bad_urls:
- with pytest.raises(ValueError) as exc:
- AvailableApiVersion(**data)
- assert (
- "url MUST be a versioned base URL" in exc.exconly()
- or "URL scheme not permitted" in exc.exconly()
- ), f"Validator 'url_must_be_versioned_base_url' not triggered as it should.\nException message: {exc.exconly()}.\nInputs: {data}"
+ if not data["url"].startswith("http"):
+ with pytest.raises(ValidationError):
+ AvailableApiVersion(**data)
+ else:
+ with pytest.raises(ValueError):
+ AvailableApiVersion(**data)
for data in bad_versions:
- with pytest.raises(ValueError) as exc:
+ with pytest.raises(ValueError):
AvailableApiVersion(**data)
- assert (
- f"Unable to validate the version string {data['version']!r} as a semantic version (expected ..)"
- in exc.exconly()
- ), f"SemVer validator not triggered as it should.\nException message: {exc.exconly()}.\nInputs: {data}"
for data in bad_combos:
- with pytest.raises(ValueError) as exc:
+ with pytest.raises(ValueError):
AvailableApiVersion(**data)
- assert "is not compatible with url version" in exc.exconly(), (
- f"Validator 'crosscheck_url_and_version' not triggered as it should.\nException message: {exc.exconly()}.\nInputs: {data}",
- )
for data in good_combos:
assert isinstance(AvailableApiVersion(**data), AvailableApiVersion)
diff --git a/tests/models/test_jsonapi.py b/tests/models/test_jsonapi.py
index b2e09cb4a..4623e79e5 100644
--- a/tests/models/test_jsonapi.py
+++ b/tests/models/test_jsonapi.py
@@ -8,7 +8,7 @@ def test_hashability():
from optimade.models.jsonapi import Error
error = Error(id="test")
- assert set([error])
+ assert {error}
def test_toplevel_links():
diff --git a/tests/models/test_links.py b/tests/models/test_links.py
index c65cc9edf..eab2c790a 100644
--- a/tests/models/test_links.py
+++ b/tests/models/test_links.py
@@ -1,7 +1,7 @@
-# pylint: disable=no-member
import pytest
-from optimade.models.links import LinksResource
+from optimade.models import LinksResponse
+from optimade.models.links import Aggregate, LinksResource, LinkType
MAPPER = "LinksMapper"
@@ -9,7 +9,35 @@
def test_good_links(starting_links, mapper):
"""Check well-formed links used as example data"""
# Test starting_links is a good links resource
- LinksResource(**mapper(MAPPER).map_back(starting_links))
+ resource = LinksResource(**mapper(MAPPER).map_back(starting_links))
+ assert resource.attributes.link_type == LinkType.CHILD
+ assert resource.attributes.aggregate == Aggregate.TEST
+
+
+def test_edge_case_links():
+ response = LinksResponse(
+ data=[
+ {
+ "id": "aflow",
+ "type": "links",
+ "attributes": {
+ "name": "AFLOW",
+ "description": "The AFLOW OPTIMADE endpoint",
+ "base_url": "http://aflow.org/API/optimade/",
+ "homepage": "http://aflow.org",
+ "link_type": "child",
+ },
+ },
+ ],
+ meta={
+ "query": {"representation": "/links"},
+ "more_data_available": False,
+ "api_version": "1.0.0",
+ },
+ )
+
+ assert isinstance(response.data[0], LinksResource)
+ assert response.data[0].attributes.link_type == LinkType.CHILD
def test_bad_links(starting_links, mapper):
diff --git a/tests/models/test_optimade_json.py b/tests/models/test_optimade_json.py
index 49c91d985..f96b88aa1 100644
--- a/tests/models/test_optimade_json.py
+++ b/tests/models/test_optimade_json.py
@@ -31,7 +31,7 @@ def test_convert_python_types():
test_none = None
python_types_as_objects = [
- str("Test"),
+ "Test",
42,
42.42,
["Test", 42],
diff --git a/tests/models/test_references.py b/tests/models/test_references.py
index de86bebb4..c16701ae2 100644
--- a/tests/models/test_references.py
+++ b/tests/models/test_references.py
@@ -1,4 +1,3 @@
-# pylint: disable=no-member
import pytest
from pydantic import ValidationError
diff --git a/tests/models/test_structures.py b/tests/models/test_structures.py
index 213dfd5eb..d9d1aaeb4 100644
--- a/tests/models/test_structures.py
+++ b/tests/models/test_structures.py
@@ -1,21 +1,28 @@
-# pylint: disable=no-member
import itertools
+from typing import TYPE_CHECKING
import pytest
-from pydantic import ValidationError
-from optimade.models.structures import CORRELATED_STRUCTURE_FIELDS, StructureResource
+from optimade.models.structures import Periodicity, StructureFeatures
from optimade.warnings import MissingExpectedField
+if TYPE_CHECKING:
+ from collections.abc import Callable, Generator
+ from typing import Any, Optional
+
+ from optimade.server.mappers import BaseResourceMapper
+
MAPPER = "StructureMapper"
@pytest.mark.filterwarnings("ignore", category=MissingExpectedField)
-def test_good_structure_with_missing_data(mapper, good_structure):
+def test_good_structure_with_missing_data(good_structure: "dict[str, Any]") -> None:
"""Check deserialization of well-formed structure used
as example data with all combinations of null values
in non-mandatory fields.
"""
+ from optimade.models.structures import StructureResource
+
structure = {field: good_structure[field] for field in good_structure}
# Have to include `assemblies` here, although it is only optional,
@@ -37,11 +44,22 @@ def test_good_structure_with_missing_data(mapper, good_structure):
StructureResource(**incomplete_structure)
-def test_more_good_structures(good_structures, mapper):
+def test_more_good_structures(
+ good_structures: "list[dict[str, Any]]",
+ mapper: "Callable[[str], BaseResourceMapper]",
+) -> None:
"""Check well-formed structures with specific edge-cases"""
+ from pydantic import ValidationError
+
+ from optimade.models.structures import StructureResource
+
for index, structure in enumerate(good_structures):
try:
- StructureResource(**mapper(MAPPER).map_back(structure))
+ s = StructureResource(**mapper(MAPPER).map_back(structure))
+ if s.attributes.structure_features:
+ assert isinstance(s.attributes.structure_features[0], StructureFeatures)
+ for dim in s.attributes.dimension_types:
+ assert isinstance(dim, Periodicity)
except ValidationError:
# Printing to keep the original exception as is, while still being informational
print(
@@ -50,15 +68,28 @@ def test_more_good_structures(good_structures, mapper):
raise
-def test_bad_structures(bad_structures, mapper):
- """Check badly formed structures"""
+def test_bad_structures(
+ bad_structures: "list[dict[str, Any]]",
+ mapper: "Callable[[str], BaseResourceMapper]",
+) -> None:
+ """Check badly formed structures.
+
+ NOTE: Only ValueError, AssertionError, and PydanticCustomError are wrapped in
+ ValidationError exceptions. All other exceptions are "bubbled up" as is. See
+ https://docs.pydantic.dev/latest/concepts/validators/#handling-errors-in-validators
+ for more information.
+ """
+ from pydantic import ValidationError
+
+ from optimade.models.structures import StructureResource
+
with pytest.warns(MissingExpectedField):
for index, structure in enumerate(bad_structures):
# This is for helping devs finding any errors that may occur
print(
f"Trying structure number {index}/{len(bad_structures)} from 'test_bad_structures.json'"
)
- with pytest.raises(ValidationError):
+ with pytest.raises((ValidationError, TypeError)):
StructureResource(**mapper(MAPPER).map_back(structure))
@@ -74,11 +105,11 @@ def test_bad_structures(bad_structures, mapper):
),
(
{"chemical_formula_anonymous": "A1B1"},
- "string does not match regex",
+ "String should match pattern",
),
(
{"chemical_formula_anonymous": "BC1"},
- "string does not match regex",
+ "String should match pattern",
),
(
{"chemical_formula_anonymous": "A9C"},
@@ -86,7 +117,7 @@ def test_bad_structures(bad_structures, mapper):
),
(
{"chemical_formula_anonymous": "A9.2B"},
- "chemical_formula_anonymous\n string does not match regex",
+ "chemical_formula_anonymous\n String should match pattern",
),
(
{"chemical_formula_anonymous": "A2B90"},
@@ -114,23 +145,23 @@ def test_bad_structures(bad_structures, mapper):
),
(
{"chemical_formula_reduced": "Ge1.0Si1.0"},
- "chemical_formula_reduced\n string does not match regex",
+ "chemical_formula_reduced\n String should match pattern",
),
(
{"chemical_formula_reduced": "GeSi2.0"},
- "chemical_formula_reduced\n string does not match regex",
+ "chemical_formula_reduced\n String should match pattern",
),
(
{"chemical_formula_reduced": "GeSi.2"},
- "chemical_formula_reduced\n string does not match regex",
+ "chemical_formula_reduced\n String should match pattern",
),
(
{"chemical_formula_reduced": "Ge1Si"},
- "string does not match regex",
+ "String should match pattern",
),
(
{"chemical_formula_reduced": "GeSi1"},
- "string does not match regex",
+ "String should match pattern",
),
(
{"chemical_formula_reduced": "SiGe2"},
@@ -142,19 +173,19 @@ def test_bad_structures(bad_structures, mapper):
),
(
{"chemical_formula_reduced": "abcd"},
- "chemical_formula_reduced\n string does not match regex",
+ "chemical_formula_reduced\n String should match pattern",
),
(
{"chemical_formula_reduced": "a2BeH"},
- "chemical_formula_reduced\n string does not match regex",
+ "chemical_formula_reduced\n String should match pattern",
),
(
{"chemical_formula_reduced": "............"},
- "chemical_formula_reduced\n string does not match regex",
+ "chemical_formula_reduced\n String should match pattern",
),
(
{"chemical_formula_reduced": "Ag6 Cl2"},
- "chemical_formula_reduced\n string does not match regex",
+ "chemical_formula_reduced\n String should match pattern",
),
(
{"chemical_formula_reduced": "Ge2Si2"},
@@ -170,39 +201,51 @@ def test_bad_structures(bad_structures, mapper):
),
(
{"chemical_formula_anonymous": "A44B15C9D4E3F2GHI0J0K0L0"},
- "string does not match regex",
+ "String should match pattern",
),
)
@pytest.mark.parametrize("deformity", deformities)
-def test_structure_fatal_deformities(good_structure, deformity):
+def test_structure_fatal_deformities(
+ good_structure: "dict[str, Any]", deformity: "Optional[tuple[dict[str, str], str]]"
+) -> None:
"""Make specific checks upon performing single invalidating deformations
of the data of a good structure.
"""
import re
+ from pydantic import ValidationError
+
+ from optimade.models.structures import StructureResource
+
if deformity is None:
StructureResource(**good_structure)
return
- deformity, message = deformity
- good_structure["attributes"].update(deformity)
+ deformity_change, message = deformity
+ good_structure["attributes"].update(deformity_change)
with pytest.raises(ValidationError, match=rf".*{re.escape(message)}.*"):
StructureResource(**good_structure)
-minor_deformities = (
- {f: None} for f in set(f for _ in CORRELATED_STRUCTURE_FIELDS for f in _)
-)
+def _minor_deformities() -> "Generator[dict[str, Any], None, None]":
+ """Generate minor deformities from correlated structure fields"""
+ from optimade.models.structures import CORRELATED_STRUCTURE_FIELDS
+
+ return ({f: None} for f in {f for _ in CORRELATED_STRUCTURE_FIELDS for f in _})
-@pytest.mark.parametrize("deformity", minor_deformities)
-def test_structure_minor_deformities(good_structure, deformity):
+@pytest.mark.parametrize("deformity", _minor_deformities())
+def test_structure_minor_deformities(
+ good_structure: "dict[str, Any]", deformity: "Optional[dict[str, Any]]"
+) -> None:
"""Make specific checks upon performing single minor invalidations
of the data of a good structure that should emit warnings.
"""
+ from optimade.models.structures import StructureResource
+
if deformity is None:
StructureResource(**good_structure)
else:
diff --git a/tests/models/test_utils.py b/tests/models/test_utils.py
index e5b3f7502..e111dd1e8 100644
--- a/tests/models/test_utils.py
+++ b/tests/models/test_utils.py
@@ -1,4 +1,5 @@
-from typing import Callable, List
+import re
+from typing import Callable
import pytest
from pydantic import BaseModel, Field, ValidationError
@@ -43,39 +44,60 @@ def test_compatible_strict_optimade_field() -> None:
produce the same schemas when given the same arguments.
"""
+ from optimade.models.utils import (
+ OPTIMADE_SCHEMA_EXTENSION_KEYS,
+ OPTIMADE_SCHEMA_EXTENSION_PREFIX,
+ )
class CorrectModelWithStrictField(BaseModel):
# check that unit and uniqueItems are passed through
- good_field: List[str] = StrictField(
+ good_field: list[str] = StrictField(
...,
support=SupportLevel.MUST,
queryable=SupportLevel.OPTIONAL,
description="Unit test to make sure that StrictField allows through OptimadeField keys",
- pattern="^structures$",
+ # pattern="^structures$", # pattern is only allowed for string type
unit="stringiness",
uniqueItems=True,
sortable=True,
)
class CorrectModelWithOptimadeField(BaseModel):
- good_field: List[str] = OptimadeField(
+ good_field: list[str] = OptimadeField(
...,
# Only difference here is that OptimadeField allows case-insensitive
# strings to be passed instead of support levels directly
support="must",
queryable="optional",
description="Unit test to make sure that StrictField allows through OptimadeField keys",
- pattern="^structures$",
+ # pattern="^structures$", # pattern is only allowed for string type
uniqueItems=True,
unit="stringiness",
sortable=True,
)
- optimade_schema = CorrectModelWithOptimadeField.schema()
- strict_schema = CorrectModelWithStrictField.schema()
+ optimade_schema = CorrectModelWithOptimadeField.model_json_schema(mode="validation")
+ strict_schema = CorrectModelWithStrictField.model_json_schema(mode="validation")
strict_schema["title"] = optimade_schema["title"]
assert strict_schema == optimade_schema
+ assert "uniqueItems" in strict_schema["properties"]["good_field"]
+ assert (
+ "uniqueItems"
+ in CorrectModelWithStrictField.model_fields["good_field"].json_schema_extra
+ )
+
+ for key in OPTIMADE_SCHEMA_EXTENSION_KEYS:
+ assert key not in strict_schema["properties"]["good_field"]
+ assert (
+ f"{OPTIMADE_SCHEMA_EXTENSION_PREFIX}{key}"
+ in CorrectModelWithStrictField.model_fields["good_field"].json_schema_extra
+ )
+ assert (
+ f"{OPTIMADE_SCHEMA_EXTENSION_PREFIX}{key}"
+ in strict_schema["properties"]["good_field"]
+ )
+
def test_formula_regexp() -> None:
"""This test checks some simple chemical formulae with the
@@ -87,7 +109,7 @@ def test_formula_regexp() -> None:
from optimade.models.utils import CHEMICAL_FORMULA_REGEXP
class DummyModel(BaseModel):
- formula: str = Field(regex=CHEMICAL_FORMULA_REGEXP)
+ formula: str = Field(pattern=CHEMICAL_FORMULA_REGEXP)
good_formulae = (
"AgCl",
@@ -138,3 +160,12 @@ def test_anonymize_formula():
assert anonymize_formula("Si1 O2") == "A2B"
assert anonymize_formula("Si11 O2") == "A11B2"
assert anonymize_formula("Si10 O2C4") == "A5B2C"
+
+
+@pytest.mark.parametrize(
+ "symops", ["x,y,z", "-x,y,-z", "x+1/2,y+1/2,z", "-x+1/2,y+1/2,-z"]
+)
+def test_symop_regex(symops):
+ from optimade.models.utils import SPACE_GROUP_SYMMETRY_OPERATION_REGEX
+
+ assert re.match(SPACE_GROUP_SYMMETRY_OPERATION_REGEX, symops)
diff --git a/tests/server/conftest.py b/tests/server/conftest.py
index a943b36ca..f3d2b0138 100644
--- a/tests/server/conftest.py
+++ b/tests/server/conftest.py
@@ -1,12 +1,19 @@
-from typing import Dict, Optional, Union
+from typing import TYPE_CHECKING, Optional, Union
import pytest
from optimade.warnings import OptimadeWarning
+if TYPE_CHECKING:
+ from typing import Callable
+
+ from requests import Response
+
+ from .utils import OptimadeTestClient
+
@pytest.fixture(scope="session")
-def client():
+def client() -> "OptimadeTestClient":
"""Return TestClient for the regular OPTIMADE server"""
from .utils import client_factory
@@ -14,7 +21,7 @@ def client():
@pytest.fixture(scope="session")
-def index_client():
+def index_client() -> "OptimadeTestClient":
"""Return TestClient for the index OPTIMADE server"""
from .utils import client_factory
@@ -22,7 +29,9 @@ def index_client():
@pytest.fixture(scope="session", params=["regular"])
-def client_with_empty_extension_endpoint(request):
+def client_with_empty_extension_endpoint(
+ request: pytest.FixtureRequest,
+) -> "OptimadeTestClient":
"""Return TestClient for the regular OPTIMADE server with an additional
empty test endpoint added at `/extensions/test_empty_body`.
"""
@@ -32,7 +41,7 @@ def client_with_empty_extension_endpoint(request):
@pytest.fixture(scope="session", params=["regular", "index"])
-def both_clients(request):
+def both_clients(request: pytest.FixtureRequest) -> "OptimadeTestClient":
"""Return TestClient for both the regular and index OPTIMADE server"""
from .utils import client_factory
@@ -40,7 +49,7 @@ def both_clients(request):
@pytest.fixture(scope="session", params=["regular", "index"])
-def both_fake_remote_clients(request):
+def both_fake_remote_clients(request: pytest.FixtureRequest) -> "OptimadeTestClient":
"""Return TestClient for both the regular and index OPTIMADE server, with
the additional option `raise_server_exceptions` set to `False`, to mimic a
remote webserver.
@@ -52,7 +61,9 @@ def both_fake_remote_clients(request):
@pytest.fixture
-def get_good_response(client, index_client):
+def get_good_response(
+ client: "OptimadeTestClient", index_client: "OptimadeTestClient"
+) -> "Callable[[str, Union[str, OptimadeTestClient], bool], Union[dict, Response]]":
"""Get response with some sanity checks, expecting '200 OK'"""
import json
@@ -124,7 +135,6 @@ def check_response(get_good_response):
server: The type of server to test, or the actual test client class.
"""
- from typing import List
from optimade.server.config import CONFIG
@@ -132,11 +142,11 @@ def check_response(get_good_response):
def inner(
request: str,
- expected_ids: Union[str, List[str]],
+ expected_ids: Union[str, list[str]],
page_limit: int = CONFIG.page_limit,
expected_return: Optional[int] = None,
expected_as_is: bool = False,
- expected_warnings: Optional[List[Dict[str, str]]] = None,
+ expected_warnings: Optional[list[dict[str, str]]] = None,
server: Union[str, OptimadeTestClient] = "regular",
):
if expected_warnings:
@@ -169,7 +179,7 @@ def inner(
for key in warn:
assert response["meta"]["warnings"][ind][key] == warn[key]
else:
- assert "warnings" not in response["meta"]
+ assert "warnings" not in response["meta"], response["meta"]["warnings"]
return response
diff --git a/tests/server/entry_collections/test_entry_collections.py b/tests/server/entry_collections/test_entry_collections.py
index a5f537698..5387d77be 100644
--- a/tests/server/entry_collections/test_entry_collections.py
+++ b/tests/server/entry_collections/test_entry_collections.py
@@ -21,6 +21,6 @@ def test_get_attribute_fields():
for entry_name, attributes_model in entry_name_attributes.items():
assert (
- set(attributes_model.__fields__.keys())
+ set(attributes_model.model_fields.keys())
== ENTRY_COLLECTIONS[entry_name].get_attribute_fields()
)
diff --git a/tests/server/middleware/test_api_hint.py b/tests/server/middleware/test_api_hint.py
index 204c4a791..9eb0da1f5 100644
--- a/tests/server/middleware/test_api_hint.py
+++ b/tests/server/middleware/test_api_hint.py
@@ -1,4 +1,5 @@
"""Test HandleApiHint middleware and the `api_hint` query parameter"""
+
from urllib.parse import unquote
import pytest
diff --git a/tests/server/middleware/test_cors.py b/tests/server/middleware/test_cors.py
index 44ea1e49e..a847e2980 100644
--- a/tests/server/middleware/test_cors.py
+++ b/tests/server/middleware/test_cors.py
@@ -3,8 +3,8 @@
def test_regular_CORS_request(both_clients):
response = both_clients.get("/info", headers={"Origin": "http://example.org"})
- assert ("access-control-allow-origin", "*") in tuple(
- response.headers.items()
+ assert (
+ ("access-control-allow-origin", "*") in tuple(response.headers.items())
), f"Access-Control-Allow-Origin header not found in response headers: {response.headers}"
diff --git a/tests/server/middleware/test_query_param.py b/tests/server/middleware/test_query_param.py
index d2ded037e..2b0da4df9 100644
--- a/tests/server/middleware/test_query_param.py
+++ b/tests/server/middleware/test_query_param.py
@@ -1,4 +1,5 @@
"""Test EntryListingQueryParams middleware"""
+
import pytest
from optimade.exceptions import BadRequest
diff --git a/tests/server/middleware/test_versioned_url.py b/tests/server/middleware/test_versioned_url.py
index 4463be7ab..1b9ab0470 100644
--- a/tests/server/middleware/test_versioned_url.py
+++ b/tests/server/middleware/test_versioned_url.py
@@ -1,4 +1,5 @@
"""Test CheckWronglyVersionedBaseUrls middleware"""
+
import urllib.parse
import pytest
diff --git a/tests/server/middleware/test_warnings.py b/tests/server/middleware/test_warnings.py
index 17281e6f4..4fc46f893 100644
--- a/tests/server/middleware/test_warnings.py
+++ b/tests/server/middleware/test_warnings.py
@@ -1,7 +1,16 @@
"""Test `AddWarning` middleware."""
+from typing import TYPE_CHECKING
-def test_showwarning_overload(both_clients, recwarn):
+if TYPE_CHECKING:
+ from pytest import WarningsRecorder
+
+ from ..utils import OptimadeTestClient
+
+
+def test_showwarning_overload(
+ both_clients: "OptimadeTestClient", recwarn: "WarningsRecorder"
+) -> None:
"""Make sure warnings.showwarning can be overloaded correctly"""
import warnings
@@ -25,7 +34,7 @@ def test_showwarning_overload(both_clients, recwarn):
# Make sure a "normal" warning is treated as usual
warnings.warn(warning_message, UserWarning)
assert len(add_warning_middleware._warnings) == 1
- assert len(recwarn.list) == 2
+ assert len(recwarn.list) == 2, ", ".join(str(_) for _ in recwarn.list)
assert recwarn.pop(OptimadeWarning)
assert recwarn.pop(UserWarning)
diff --git a/tests/server/query_params/conftest.py b/tests/server/query_params/conftest.py
index ce11819be..b7f0e59f0 100644
--- a/tests/server/query_params/conftest.py
+++ b/tests/server/query_params/conftest.py
@@ -12,13 +12,13 @@ def structures():
@pytest.fixture
def check_include_response(get_good_response):
"""Fixture to check "good" `include` response"""
- from typing import List, Optional, Set, Union
+ from typing import Optional, Union
def inner(
request: str,
- expected_included_types: Union[List, Set],
- expected_included_resources: Union[List, Set],
- expected_relationship_types: Optional[Union[List, Set]] = None,
+ expected_included_types: Union[list, set],
+ expected_included_resources: Union[list, set],
+ expected_relationship_types: Optional[Union[list, set]] = None,
server: str = "regular",
):
response = get_good_response(request, server)
diff --git a/tests/server/query_params/test_filter.py b/tests/server/query_params/test_filter.py
index 9d8c758a6..c144e2667 100644
--- a/tests/server/query_params/test_filter.py
+++ b/tests/server/query_params/test_filter.py
@@ -1,4 +1,5 @@
"""Make sure filters are handled correctly"""
+
import pytest
from optimade.server.config import CONFIG, SupportedBackend
diff --git a/tests/server/routers/test_utils.py b/tests/server/routers/test_utils.py
index 37b1f8ee5..b2216e837 100644
--- a/tests/server/routers/test_utils.py
+++ b/tests/server/routers/test_utils.py
@@ -1,5 +1,7 @@
"""Tests specifically for optimade.servers.routers.utils."""
-from typing import Mapping, Optional, Tuple, Union
+
+from collections.abc import Mapping
+from typing import Optional, Union
from unittest import mock
import pytest
@@ -8,7 +10,7 @@
def mocked_providers_list_response(
url: Union[str, bytes] = "",
- param: Optional[Union[Mapping[str, str], Tuple[str, str]]] = None,
+ param: Optional[Union[Mapping[str, str], tuple[str, str]]] = None,
**kwargs,
):
"""This function will be used to mock requests.get
@@ -19,7 +21,7 @@ def mocked_providers_list_response(
https://stackoverflow.com/questions/15753390/how-can-i-mock-requests-and-the-response
"""
try:
- from optimade.server.data import providers # type: ignore[attr-defined]
+ from optimade.server.data import providers
except ImportError:
pytest.fail(
"Cannot import providers from optimade.server.data, "
@@ -34,6 +36,9 @@ def __init__(self, data: Union[list, dict], status_code: int):
def json(self) -> Union[list, dict]:
return self.data
+ def content(self) -> str:
+ return str(self.data)
+
return MockResponse(providers, 200)
@@ -72,6 +77,12 @@ def test_get_providers():
assert get_providers() == providers_list
+def test_get_all_databases():
+ from optimade.utils import get_all_databases
+
+ assert list(get_all_databases())
+
+
def test_get_providers_warning(caplog, top_dir):
"""Make sure a warning is logged as a last resort."""
import copy
@@ -87,7 +98,7 @@ def test_get_providers_warning(caplog, top_dir):
caplog.clear()
with mock.patch("requests.get", side_effect=ConnectionError):
- del data.providers # pylint: disable=no-member
+ del data.providers
assert get_providers() == []
warning_message = """Could not retrieve a list of providers!
@@ -96,9 +107,7 @@ def test_get_providers_warning(caplog, top_dir):
{}
The list of providers will not be included in the `/links`-endpoint.
-""".format(
- "".join([f" * {_}\n" for _ in PROVIDER_LIST_URLS])
- )
+""".format("".join([f" * {_}\n" for _ in PROVIDER_LIST_URLS]))
assert warning_message in caplog.messages
finally:
diff --git a/tests/server/test_client.py b/tests/server/test_client.py
index 732385bcc..fa2c82016 100644
--- a/tests/server/test_client.py
+++ b/tests/server/test_client.py
@@ -1,13 +1,13 @@
"""This module uses the reference test server to test the OPTIMADE client."""
-
import json
import warnings
from functools import partial
from pathlib import Path
-from typing import Dict, Optional
+from typing import Optional
import httpx
+import numpy as np
import pytest
from optimade.client.cli import _get
@@ -78,10 +78,33 @@ def test_client_endpoints(async_http_client, http_client, use_async):
count_results = cli.references.count()
assert count_results["references"][filter][TEST_URL] > 0
+ if use_async:
+ cli._force_binary_search = True
+ count_results_binary = cli.references.count()
+ assert (
+ count_results_binary["references"][filter][TEST_URL]
+ == count_results["references"][filter][TEST_URL]
+ )
+ cli._force_binary_search = False
+
filter = 'elements HAS "Ag"'
count_results = cli.count(filter)
assert count_results["structures"][filter][TEST_URL] > 0
+ filter = 'elements HAS "Ac"'
+ count_results = cli.count(filter)
+ assert count_results["structures"][filter][TEST_URL] == 6
+
+ if use_async:
+ cli._force_binary_search = True
+ filter = 'elements HAS "Ac"'
+ count_results_binary = cli.count(filter)
+ assert (
+ count_results_binary["structures"][filter][TEST_URL]
+ == count_results["structures"][filter][TEST_URL]
+ )
+ cli._force_binary_search = False
+
count_results = cli.info.get()
assert count_results["info"][""][TEST_URL]["data"]["type"] == "info"
@@ -218,6 +241,8 @@ def test_command_line_client(async_http_client, http_client, use_async, capsys):
exclude_databases=None,
http_client=async_http_client if use_async else http_client,
http_timeout=httpx.Timeout(2.0),
+ verbosity=0,
+ skip_ssl=False,
)
# Test multi-provider query
@@ -254,6 +279,8 @@ def test_command_line_client_silent(async_http_client, http_client, use_async, c
exclude_databases=None,
http_client=async_http_client if use_async else http_client,
http_timeout=httpx.Timeout(2.0),
+ verbosity=0,
+ skip_ssl=False,
)
# Test silent mode
@@ -293,6 +320,8 @@ def test_command_line_client_multi_provider(
exclude_databases=None,
http_client=async_http_client if use_async else http_client,
http_timeout=httpx.Timeout(2.0),
+ verbosity=0,
+ skip_ssl=False,
)
_get(**args)
captured = capsys.readouterr()
@@ -327,6 +356,8 @@ def test_command_line_client_write_to_file(
exclude_databases=None,
http_client=async_http_client if use_async else http_client,
http_timeout=httpx.Timeout(2.0),
+ verbosity=0,
+ skip_ssl=False,
)
test_filename = "test-optimade-client.json"
if Path(test_filename).is_file():
@@ -337,7 +368,7 @@ def test_command_line_client_write_to_file(
assert 'Performing query structures/?filter=elements HAS "Ag"' in captured.err
assert not captured.out
assert Path(test_filename).is_file()
- with open(test_filename, "r") as f:
+ with open(test_filename) as f:
results = json.load(f)
for url in TEST_URLS:
assert len(results["structures"]['elements HAS "Ag"'][url]["data"]) == 11
@@ -360,9 +391,9 @@ def test_strict_async(async_http_client, http_client, use_async):
@pytest.mark.parametrize("use_async", [True, False])
def test_client_global_data_callback(async_http_client, http_client, use_async):
- container: Dict[str, str] = {}
+ container: dict[str, str] = {}
- def global_database_callback(_: str, results: Dict):
+ def global_database_callback(_: str, results: dict):
"""A test callback that creates a flat dictionary of results via global state"""
for structure in results["data"]:
@@ -386,7 +417,7 @@ def global_database_callback(_: str, results: Dict):
@pytest.mark.parametrize("use_async", [True, False])
def test_client_page_skip_callback(async_http_client, http_client, use_async):
- def page_skip_callback(_: str, results: Dict) -> Optional[Dict]:
+ def page_skip_callback(_: str, results: dict) -> Optional[dict]:
"""A test callback that skips to the final page of results."""
if len(results["data"]) > 16:
return {"next": f"{TEST_URL}/structures?page_offset=16"}
@@ -407,10 +438,10 @@ def page_skip_callback(_: str, results: Dict) -> Optional[Dict]:
@pytest.mark.parametrize("use_async", [True, False])
def test_client_mutable_data_callback(async_http_client, http_client, use_async):
- container: Dict[str, str] = {}
+ container: dict[str, str] = {}
def mutable_database_callback(
- _: str, results: Dict, db: Optional[Dict[str, str]] = None
+ _: str, results: dict, db: Optional[dict[str, str]] = None
) -> None:
"""A test callback that creates a flat dictionary of results via mutable args."""
@@ -436,7 +467,7 @@ def mutable_database_callback(
def test_client_asynchronous_write_callback(
async_http_client, http_client, use_async, tmp_path
):
- def write_to_file(_: str, results: Dict):
+ def write_to_file(_: str, results: dict):
"""A test callback that creates a flat dictionary of results via global state"""
with open(tmp_path / "formulae.csv", "a") as f:
@@ -458,7 +489,7 @@ def write_to_file(_: str, results: Dict):
cli.get(response_fields=["chemical_formula_reduced"])
- with open(tmp_path / "formulae.csv", "r") as f:
+ with open(tmp_path / "formulae.csv") as f:
lines = f.readlines()
assert len(lines) == 17 * len(TEST_URLS) + 1
@@ -480,8 +511,37 @@ def test_list_properties(
results = cli.list_properties("structures")
for database in results:
- assert len(results[database]) == 22
+ assert len(results[database]) == 27
results = cli.search_property("structures", "site")
for database in results:
assert results[database] == ["nsites", "cartesian_site_positions"]
+
+
+@pytest.mark.parametrize(
+ "trial_counts", [1, 2] + [int(x) for x in np.logspace(1, 10, 102)]
+)
+def test_binary_search_internals(trial_counts):
+ cli = OptimadeClient(
+ base_urls=TEST_URLS,
+ )
+ max_attempts = 100
+ attempts = 0
+ window, probe = cli._update_probe_and_window()
+ while attempts < max_attempts:
+ if trial_counts + 1 > probe:
+ below = True
+ else:
+ below = False
+ window, probe = cli._update_probe_and_window(window, probe, below=below)
+ # print(trial_counts, window, probe)
+ if window[0] == window[1] == probe:
+ assert (
+ window[0] == trial_counts
+ ), "Binary search did not converge to the correct value."
+ break
+ attempts += 1
+ else:
+ raise RuntimeError(
+ f"Could not converge binary search for {trial_counts} in {max_attempts} attempts."
+ )
diff --git a/tests/server/test_config.py b/tests/server/test_config.py
index db0175b3f..a09a8f796 100644
--- a/tests/server/test_config.py
+++ b/tests/server/test_config.py
@@ -1,10 +1,13 @@
-# pylint: disable=protected-access,pointless-statement,relative-beyond-top-level
import json
import os
from pathlib import Path
+from typing import TYPE_CHECKING
+if TYPE_CHECKING:
+ from .utils import OptimadeTestClient
-def test_env_variable():
+
+def test_env_variable() -> None:
"""Set OPTIMADE_DEBUG environment variable and check CONFIG picks up on it correctly"""
from optimade.server.config import ServerConfig
@@ -25,7 +28,7 @@ def test_env_variable():
assert os.getenv("OPTIMADE_DEBUG") is None
-def test_default_config_path(top_dir):
+def test_default_config_path(top_dir: Path) -> None:
"""Make sure the default config path works
Expected default config path: PATH/TO/USER/HOMEDIR/.optimade.json
"""
@@ -33,7 +36,7 @@ def test_default_config_path(top_dir):
org_env_var = os.getenv("OPTIMADE_CONFIG_FILE")
- with open(top_dir.joinpath("tests/test_config.json"), "r") as config_file:
+ with open(top_dir.joinpath("tests/test_config.json")) as config_file:
config = json.load(config_file)
different_base_url = "http://something_you_will_never_think_of.com"
@@ -75,7 +78,7 @@ def test_default_config_path(top_dir):
os.environ["OPTIMADE_CONFIG_FILE"] = org_env_var
-def test_debug_is_respected_when_off(both_clients):
+def test_debug_is_respected_when_off(both_clients: "OptimadeTestClient") -> None:
"""Make sure traceback is toggleable according to debug mode - here OFF
TODO: This should be moved to a separate test file that tests the exception handlers.
@@ -102,7 +105,7 @@ def test_debug_is_respected_when_off(both_clients):
CONFIG.debug = org_value
-def test_debug_is_respected_when_on(both_clients):
+def test_debug_is_respected_when_on(both_clients: "OptimadeTestClient") -> None:
"""Make sure traceback is toggleable according to debug mode - here ON
TODO: This should be moved to a separate test file that tests the exception handlers.
@@ -128,7 +131,7 @@ def test_debug_is_respected_when_on(both_clients):
CONFIG.debug = org_value
-def test_yaml_config_file():
+def test_yaml_config_file() -> None:
"""Test loading a YAML config file
First, pass a correctly formatted YAML file that only includes a single YAML document.
diff --git a/tests/server/test_schemas.py b/tests/server/test_schemas.py
index b50b66274..25a44b342 100644
--- a/tests/server/test_schemas.py
+++ b/tests/server/test_schemas.py
@@ -1,45 +1,53 @@
-from optimade.server.schemas import ENTRY_INFO_SCHEMAS, retrieve_queryable_properties
+"""Tests for optimade.server.schemas"""
-def test_schemas():
+def test_retrieve_queryable_properties() -> None:
"""Test that the default `ENTRY_INFO_SCHEMAS` contain
all the required information about the OPTIMADE properties
after dereferencing.
"""
+ from optimade.models.entries import EntryResourceAttributes
+ from optimade.server.schemas import (
+ ENTRY_INFO_SCHEMAS,
+ retrieve_queryable_properties,
+ )
+
for entry in ("Structures", "References"):
- schema = ENTRY_INFO_SCHEMAS[entry.lower()]()
+ schema = ENTRY_INFO_SCHEMAS[entry.lower()]
top_level_props = ("id", "type", "attributes")
properties = retrieve_queryable_properties(schema, top_level_props)
- fields = list(
- schema["definitions"][f"{entry[:-1]}ResourceAttributes"][
- "properties"
- ].keys()
- )
+ attributes_annotation = schema.model_fields["attributes"].annotation
+ assert issubclass(attributes_annotation, EntryResourceAttributes)
+ fields = list(attributes_annotation.model_fields)
fields += ["id", "type"]
# Check all fields are present
assert all(field in properties for field in fields)
- # Check that there are no references to definitions remaining
- assert "$ref" not in properties
- assert not any("$ref" in properties[field] for field in properties)
-
# Check that all expected keys are present for OPTIMADE fields
for key in ("type", "sortable", "queryable", "description"):
assert all(key in properties[field] for field in properties)
+ # Check that all fields are queryable
+ assert all(properties[field]["queryable"] for field in properties)
-def test_provider_field_schemas():
+
+def test_provider_field_schemas() -> None:
"""Tests that the default configured provider fields that have descriptions
are dereferenced appropriately.
"""
+ from optimade.server.schemas import (
+ ENTRY_INFO_SCHEMAS,
+ retrieve_queryable_properties,
+ )
+
entry = "structures"
test_field = "chemsys"
- schema = ENTRY_INFO_SCHEMAS[entry]()
+ schema = ENTRY_INFO_SCHEMAS[entry]
top_level_props = ("id", "type", "attributes")
properties = retrieve_queryable_properties(schema, top_level_props, entry)
name = f"_exmpl_{test_field}"
diff --git a/tests/server/test_server_validation.py b/tests/server/test_server_validation.py
index 9dc45c7b0..3956ba9be 100644
--- a/tests/server/test_server_validation.py
+++ b/tests/server/test_server_validation.py
@@ -1,14 +1,18 @@
import dataclasses
import json
+from typing import TYPE_CHECKING
import pytest
from optimade.validator import ImplementationValidator
+if TYPE_CHECKING:
+ from .utils import OptimadeTestClient
+
pytestmark = pytest.mark.filterwarnings("ignore")
-def test_with_validator(both_fake_remote_clients):
+def test_with_validator(both_fake_remote_clients: "OptimadeTestClient") -> None:
from optimade.server.main_index import app
validator = ImplementationValidator(
@@ -20,7 +24,9 @@ def test_with_validator(both_fake_remote_clients):
assert validator.valid
-def test_with_validator_skip_optional(both_fake_remote_clients):
+def test_with_validator_skip_optional(
+ both_fake_remote_clients: "OptimadeTestClient",
+) -> None:
from optimade.server.main_index import app
validator = ImplementationValidator(
@@ -33,7 +39,9 @@ def test_with_validator_skip_optional(both_fake_remote_clients):
assert validator.valid
-def test_with_validator_json_response(both_fake_remote_clients, capsys):
+def test_with_validator_json_response(
+ both_fake_remote_clients: "OptimadeTestClient", capsys: pytest.CaptureFixture
+) -> None:
"""Test that the validator writes compliant JSON when requested."""
from optimade.server.main_index import app
@@ -56,7 +64,9 @@ def test_with_validator_json_response(both_fake_remote_clients, capsys):
assert validator.valid
-def test_as_type_with_validator(client, capsys):
+def test_as_type_with_validator(
+ client: "OptimadeTestClient", capsys: pytest.CaptureFixture
+) -> None:
from unittest.mock import Mock, patch
test_urls = {
@@ -87,7 +97,7 @@ def test_as_type_with_validator(client, capsys):
assert dataclasses.asdict(validator.results) == json_response
-def test_query_value_formatting(client):
+def test_query_value_formatting() -> None:
from optimade.models.optimade_json import DataType
format_value_fn = ImplementationValidator._format_test_value
@@ -103,7 +113,9 @@ def test_query_value_formatting(client):
@pytest.mark.parametrize("server", ["regular", "index"])
-def test_versioned_base_urls(client, index_client, server: str):
+def test_versioned_base_urls(
+ client: "OptimadeTestClient", index_client: "OptimadeTestClient", server: str
+) -> None:
"""Test all expected versioned base URLs responds with 200
This depends on the routers for each kind of server.
@@ -137,7 +149,9 @@ def test_versioned_base_urls(client, index_client, server: str):
@pytest.mark.parametrize("server", ["regular", "index"])
-def test_meta_schema_value_obeys_index(client, index_client, server: str):
+def test_meta_schema_value_obeys_index(
+ client: "OptimadeTestClient", index_client: "OptimadeTestClient", server: str
+) -> None:
"""Test that the reported `meta->schema` is correct for index/non-index
servers.
"""
diff --git a/tests/server/utils.py b/tests/server/utils.py
index 6a046c36b..ec8d72c62 100644
--- a/tests/server/utils.py
+++ b/tests/server/utils.py
@@ -1,19 +1,34 @@
import json
import re
import warnings
-from typing import Iterable, Optional, Type, Union
+from typing import TYPE_CHECKING
from urllib.parse import urlparse
import httpx
import pytest
import requests
from fastapi.testclient import TestClient
-from starlette import testclient
-import optimade.models.jsonapi as jsonapi
from optimade import __api_version__
from optimade.models import ResponseMeta
+if TYPE_CHECKING:
+ from collections.abc import Iterable
+ from typing import Optional, Protocol, Type, Union
+
+ from starlette import testclient
+
+ import optimade.models.jsonapi as jsonapi
+
+ class ClientFactoryInner(Protocol):
+ def __call__(
+ self,
+ version: "Optional[str]" = None,
+ server: str = "regular",
+ raise_server_exceptions: bool = True,
+ add_empty_endpoint: bool = False,
+ ) -> "OptimadeTestClient": ...
+
class OptimadeTestClient(TestClient):
"""Special OPTIMADE edition of FastAPI's (Starlette's) TestClient
@@ -25,13 +40,13 @@ class OptimadeTestClient(TestClient):
def __init__(
self,
- app: Union[testclient.ASGI2App, testclient.ASGI3App],
+ app: "Union[testclient.ASGI2App, testclient.ASGI3App]",
base_url: str = "http://example.org",
raise_server_exceptions: bool = True,
root_path: str = "",
version: str = "",
) -> None:
- super(OptimadeTestClient, self).__init__(
+ super().__init__(
app=app,
base_url=base_url,
raise_server_exceptions=raise_server_exceptions,
@@ -50,7 +65,7 @@ def __init__(
version = f"/v{__api_version__.split('.')[0]}"
self.version = version
- def request( # pylint: disable=too-many-locals
+ def request(
self,
method: str,
url: httpx._types.URLTypes,
@@ -64,7 +79,7 @@ def request( # pylint: disable=too-many-locals
while url.startswith("/"):
url = url[1:]
url = f"{self.version}/{url}"
- return super(OptimadeTestClient, self).request(
+ return super().request(
method=method,
url=url,
**kwargs,
@@ -74,14 +89,13 @@ def request( # pylint: disable=too-many-locals
class BaseEndpointTests:
"""Base class for common tests of endpoints"""
- request_str: Optional[str] = None
- response_cls: Optional[Type[jsonapi.Response]] = None
-
- response: Optional[httpx.Response] = None
- json_response: Optional[dict] = None
+ request_str: "Optional[str]" = None
+ response_cls: "Optional[Type[jsonapi.Response]]" = None
+ response: "Optional[httpx.Response]" = None
+ json_response: "Optional[dict]" = None
@staticmethod
- def check_keys(keys: list, response_subset: Iterable):
+ def check_keys(keys: list, response_subset: "Iterable[str]"):
for key in keys:
assert (
key in response_subset
@@ -97,9 +111,11 @@ def test_response_okay(self):
def test_meta_response(self):
"""General test for `meta` property in response"""
assert "meta" in self.json_response
- meta_required_keys = ResponseMeta.schema()["required"]
+ meta_required_keys = ResponseMeta.model_json_schema(mode="validation")[
+ "required"
+ ]
meta_optional_keys = list(
- set(ResponseMeta.schema()["properties"].keys()) - set(meta_required_keys)
+ set(ResponseMeta.model_fields) - set(meta_required_keys)
)
implemented_optional_keys = [
"time_stamp",
@@ -107,7 +123,7 @@ def test_meta_response(self):
"provider",
"data_available",
"implementation",
- "schema",
+ # "optimade_schema",
# TODO: These keys are not implemented in the example server implementations
# Add them in when they are.
# "last_id",
@@ -121,7 +137,7 @@ def test_meta_response(self):
def test_serialize_response(self):
assert self.response_cls is not None, "Response class unset for this endpoint"
- self.response_cls(**self.json_response) # pylint: disable=not-callable
+ self.response_cls(**self.json_response)
class EndpointTests(BaseEndpointTests):
@@ -160,11 +176,11 @@ def get_response(self, index_client):
self.json_response = None
-def client_factory():
+def client_factory() -> "ClientFactoryInner":
"""Return TestClient for OPTIMADE server"""
def inner(
- version: Optional[str] = None,
+ version: "Optional[str]" = None,
server: str = "regular",
raise_server_exceptions: bool = True,
add_empty_endpoint: bool = False,
@@ -222,10 +238,9 @@ async def empty(_):
class NoJsonEndpointTests:
"""A simplified mixin class for tests on non-JSON endpoints."""
- request_str: Optional[str] = None
- response_cls: Optional[Type] = None
-
- response: Optional[httpx.Response] = None
+ request_str: "Optional[str]" = None
+ response_cls: "Optional[Type]" = None
+ response: "Optional[httpx.Response]" = None
@pytest.fixture(autouse=True)
def get_response(self, both_clients):
@@ -246,7 +261,7 @@ class HttpxTestClient(httpx.Client):
client = client_factory()(server="regular")
- def request( # pylint: disable=too-many-locals
+ def request(
self,
method: str,
url: httpx._types.URLTypes,
@@ -260,7 +275,7 @@ class RequestsTestClient(requests.Session):
client = client_factory()(server="regular")
- def request( # pylint: disable=too-many-locals
+ def request(
self,
method,
url,
@@ -275,7 +290,7 @@ class AsyncHttpxTestClient(httpx.AsyncClient):
client = client_factory()(server="regular")
- async def request( # pylint: disable=too-many-locals
+ async def request(
self,
method: str,
url: httpx._types.URLTypes,
diff --git a/tests/test_config.json b/tests/test_config.json
index f64e139d3..3b55242e1 100644
--- a/tests/test_config.json
+++ b/tests/test_config.json
@@ -14,6 +14,7 @@
"prefix": "exmpl",
"homepage": "https://example.com"
},
+ "mongo_count_timeout": 0,
"index_base_url": "http://localhost:5001",
"provider_fields": {
"structures": [