├── .coveragerc ├── .github └── workflows │ ├── lint.yml │ ├── release.yml │ ├── testing.yml │ └── upgrade_dependencies.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── docs ├── Makefile ├── _templates │ ├── custom-class-template.rst │ └── custom-module-template.rst ├── conf.py ├── index.rst ├── make.bat └── modules.rst ├── mp_api └── client │ ├── __init__.py │ ├── core │ ├── __init__.py │ ├── client.py │ ├── settings.py │ └── utils.py │ ├── mprester.py │ └── routes │ ├── __init__.py │ ├── _general_store.py │ ├── _messages.py │ ├── _user_settings.py │ ├── materials │ ├── __init__.py │ ├── absorption.py │ ├── alloys.py │ ├── bonds.py │ ├── chemenv.py │ ├── dielectric.py │ ├── doi.py │ ├── elasticity.py │ ├── electrodes.py │ ├── electronic_structure.py │ ├── eos.py │ ├── grain_boundaries.py │ ├── magnetism.py │ ├── materials.py │ ├── oxidation_states.py │ ├── phonon.py │ ├── piezo.py │ ├── provenance.py │ ├── robocrys.py │ ├── similarity.py │ ├── substrates.py │ ├── summary.py │ ├── surface_properties.py │ ├── synthesis.py │ ├── tasks.py │ ├── thermo.py │ └── xas.py │ └── molecules │ ├── __init__.py │ ├── bonds.py │ ├── jcesr.py │ ├── molecules.py │ ├── orbitals.py │ ├── partial_charges.py │ ├── partial_spins.py │ ├── redox.py │ ├── summary.py │ ├── tasks.py │ ├── thermo.py │ └── vibrations.py ├── pyproject.toml ├── requirements ├── requirements-ubuntu-latest_py3.11.txt ├── requirements-ubuntu-latest_py3.11_extras.txt ├── requirements-ubuntu-latest_py3.12.txt └── requirements-ubuntu-latest_py3.12_extras.txt ├── test_files ├── Si_mp_149.cif ├── calcs_reversed_mp_1031016.json ├── materials_Li_Fe_V.json ├── synth_doc.json ├── synth_doc_adaptor.json ├── synth_doc_adaptor_synpro.json └── tasks_Li_Fe_V.json └── tests ├── __init__.py ├── materials ├── core_function.py ├── test_bonds.py ├── test_chemenv.py ├── test_dielectric.py ├── test_elasticity.py ├── test_electrodes.py ├── test_electronic_structure.py ├── test_eos.py ├── test_grain_boundary.py ├── test_magnetism.py ├── test_materials.py ├── test_oxidation_states.py ├── test_piezo.py ├── test_provenance.py ├── test_robocrys.py ├── test_substrates.py ├── test_summary.py ├── test_surface_properties.py ├── test_synthesis.py ├── test_tasks.py ├── test_thermo.py └── test_xas.py ├── molecules ├── __init__.py ├── core_function.py ├── test_jcesr.py └── test_summary.py ├── test_client.py ├── test_core_client.py └── test_mprester.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | *test* 4 | */resources.py 5 | */models.py 6 | */models/* 7 | */_consumer/client.py 8 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Linting 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - name: Set up Python 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: "3.11" 22 | cache: pip 23 | 24 | - name: Install dependencies 25 | run: | 26 | pip install pre-commit 27 | 28 | - name: Run pre-commit 29 | run: | 30 | pre-commit run --all-files --show-diff-on-failure 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | max-parallel: 6 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - uses: actions/setup-python@v4 17 | with: 18 | python-version: 3.9 19 | 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | pip install setuptools setuptools_scm wheel build 24 | 25 | - name: Build packages 26 | run: python -m build 27 | working-directory: ./ 28 | 29 | - name: Publish package 30 | uses: pypa/gh-action-pypi-publish@master 31 | with: 32 | user: __token__ 33 | password: ${{ secrets.PYPY_API_TOKEN }} 34 | packages_dir: ./dist/ 35 | 36 | docs: 37 | runs-on: ubuntu-latest 38 | needs: 39 | - deploy 40 | 41 | steps: 42 | - uses: actions/checkout@v3 43 | 44 | - uses: actions/setup-python@v4 45 | with: 46 | python-version: 3.9 47 | 48 | - name: Install dependencies 49 | run: | 50 | python -m pip install --upgrade pip 51 | pip install -r requirements.txt 52 | pip install -e . 53 | pip install -r requirements-docs.txt 54 | 55 | - name: Build 56 | run: sphinx-build ./docs ./docs/_build 57 | 58 | - name: Deploy 59 | uses: peaceiris/actions-gh-pages@v3.8.0 60 | with: 61 | github_token: ${{ secrets.GITHUB_TOKEN }} 62 | publish_dir: ./docs/_build 63 | -------------------------------------------------------------------------------- /.github/workflows/testing.yml: -------------------------------------------------------------------------------- 1 | name: testing 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [main] 7 | 8 | pull_request: 9 | branches: [main] 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | test: 17 | strategy: 18 | max-parallel: 2 19 | matrix: 20 | os: ["ubuntu-latest"] 21 | python-version: ["3.10", "3.11"] 22 | 23 | runs-on: ${{ matrix.os }} 24 | 25 | steps: 26 | - uses: actions/checkout@v4 27 | 28 | - name: Set up Python ${{ matrix.python-version }} 29 | uses: actions/setup-python@v5 30 | with: 31 | python-version: ${{ matrix.python-version }} 32 | 33 | - name: Install Python dependencies 34 | run: | 35 | python -m pip install --upgrade pip 36 | pip install -r requirements/requirements-${{ matrix.os }}_py${{ matrix.python-version }}.txt 37 | pip install -r requirements/requirements-${{ matrix.os }}_py${{ matrix.python-version }}_extras.txt 38 | 39 | - name: Set SSL_CERT_FILE (Linux) 40 | if: matrix.os == 'ubuntu-latest' || matrix.os == 'macos-latest' 41 | run: echo "SSL_CERT_FILE=$(python -m certifi)" >> $GITHUB_ENV 42 | 43 | - name: Set SSL_CERT_FILE (Windows) 44 | if: matrix.os == 'windows-latest' 45 | run: echo "SSL_CERT_FILE=$(python -m certifi)" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append 46 | 47 | - name: Format API key name (Linux/MacOS) 48 | if: matrix.os == 'ubuntu-latest' || matrix.os == 'macos-latest' 49 | run: | 50 | echo "API_KEY_NAME=$(echo ${{ format('MP_API_KEY_{0}_{1}', matrix.os, matrix.python-version) }} | awk '{gsub(/-|\./, "_"); print}' | tr '[:lower:]' '[:upper:]')" >> $GITHUB_ENV 51 | 52 | - name: Format API key name (Windows) 53 | if: matrix.os == 'windows-latest' 54 | run: | 55 | echo "API_KEY_NAME=$(echo ${{ format('MP_API_KEY_{0}_{1}', matrix.os, matrix.python-version) }} | awk '{gsub(/-|\./, "_"); print}' | tr '[:lower:]' '[:upper:]')" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append 56 | 57 | - name: Test with pytest 58 | env: 59 | MP_API_KEY: ${{ secrets[env.API_KEY_NAME] }} 60 | #MP_API_ENDPOINT: https://api-preview.materialsproject.org/ 61 | run: | 62 | pip install -e . 63 | pytest -x --cov=mp_api --cov-report=xml 64 | - uses: codecov/codecov-action@v1 65 | with: 66 | token: ${{ secrets.CODECOV_TOKEN }} 67 | file: ./coverage.xml 68 | 69 | auto-gen-release: 70 | needs: [test] 71 | runs-on: ubuntu-latest 72 | env: 73 | GITHUB_TOKEN: ${{ secrets.API_VER_BUMP_TOKEN }} 74 | steps: 75 | - uses: rymndhng/release-on-push-action@v0.20.0 76 | with: 77 | bump_version_scheme: norelease 78 | -------------------------------------------------------------------------------- /.github/workflows/upgrade_dependencies.yml: -------------------------------------------------------------------------------- 1 | name: Upgrade Dependencies 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | upgrade: 8 | name: ${{ matrix.package }} (${{ matrix.os }}/py${{ matrix.python-version }}) 9 | runs-on: ${{ matrix.os }} 10 | strategy: 11 | matrix: 12 | os: ['ubuntu-latest'] 13 | package: ["mp-api"] 14 | python-version: ["3.11", "3.12"] 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | - uses: actions/setup-python@v5 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | cache: 'pip' 23 | - name: Upgrade Python dependencies 24 | shell: bash 25 | run: | 26 | python -m pip install --upgrade pip pip-tools 27 | python -m piptools compile -q --upgrade --resolver=backtracking -o requirements/requirements-${{ matrix.os }}_py${{ matrix.python-version }}.txt pyproject.toml 28 | python -m piptools compile -q --upgrade --resolver=backtracking --all-extras -o requirements/requirements-${{ matrix.os }}_py${{ matrix.python-version }}_extras.txt pyproject.toml 29 | - name: Detect changes 30 | id: changes 31 | shell: bash 32 | run: | 33 | echo "count=$(git diff-index HEAD | wc -l | xargs)" >> $GITHUB_OUTPUT 34 | echo "files=$(git ls-files --exclude-standard --others | wc -l | xargs)" >> $GITHUB_OUTPUT 35 | - name: commit & push changes 36 | if: steps.changes.outputs.count > 0 || steps.changes.outputs.files > 0 37 | shell: bash 38 | run: | 39 | git config user.name github-actions 40 | git config user.email github-actions@github.com 41 | git add requirements/requirements-${{ matrix.os }}_py${{ matrix.python-version }}.txt 42 | git add requirements/requirements-${{ matrix.os }}_py${{ matrix.python-version }}_extras.txt 43 | git commit --allow-empty -m "Update ${{ matrix.os }} dependencies for ${{ matrix.package }}" 44 | git push -f origin ${{ github.ref_name }}:auto-dependency-upgrades-${{ matrix.package }}-${{ matrix.os }}-py${{ matrix.python-version }} 45 | 46 | pull_request: 47 | name: Merge all branches and open PR 48 | runs-on: ubuntu-latest 49 | needs: upgrade 50 | strategy: 51 | matrix: 52 | python-version: ["3.11"] 53 | steps: 54 | - uses: actions/checkout@v4 55 | with: 56 | fetch-depth: 0 57 | - uses: actions/setup-python@v5 58 | with: 59 | python-version: ${{ matrix.python-version }} 60 | - name: detect auto-upgrade-dependency branches 61 | id: changes 62 | run: echo "count=$(git branch -r | grep auto-dependency-upgrades- | wc -l | xargs)" >> $GITHUB_OUTPUT 63 | - name: merge all auto-dependency-upgrades branches 64 | if: steps.changes.outputs.count > 0 65 | run: | 66 | git config user.name github-actions 67 | git config user.email github-actions@github.com 68 | git checkout -b auto-dependency-upgrades 69 | git branch -r | grep auto-dependency-upgrades- | xargs -I {} git merge {} 70 | git rebase ${GITHUB_REF##*/} 71 | git push -f origin auto-dependency-upgrades 72 | git branch -r | grep auto-dependency-upgrades- | cut -d/ -f2 | xargs -I {} git push origin :{} 73 | - name: Open pull request if needed 74 | if: steps.changes.outputs.count > 0 75 | env: 76 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 77 | # Only open a PR if the branch is not attached to an existing one 78 | run: | 79 | PR=$(gh pr list --head auto-dependency-upgrades --json number -q '.[0].number') 80 | if [ -z $PR ]; then 81 | gh pr create \ 82 | --head auto-dependency-upgrades \ 83 | --title "Automated dependency upgrades" \ 84 | --body "Full log: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} (THIS IS AN AUTOMATED COMMENT)" 85 | else 86 | echo "Pull request already exists, won't create a new one." 87 | fi 88 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | source/apidoc/maggma.* 67 | source/apidoc/modules.rst 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # dotenv 85 | .env 86 | 87 | # virtualenv 88 | .venv 89 | venv/ 90 | ENV/ 91 | 92 | # Spyder project settings 93 | .spyderproject 94 | .spyproject 95 | 96 | # Rope project settings 97 | .ropeproject 98 | 99 | # mkdocs documentation 100 | /site 101 | 102 | # mypy 103 | .mypy_cache/ 104 | 105 | # eclipse 106 | .project 107 | .pydevproject 108 | 109 | # fleet 110 | .fleet 111 | 112 | *~ 113 | 114 | .idea 115 | .DS_Store 116 | .vscode 117 | .pytest_cache 118 | 119 | # Doc folder 120 | _autosummary 121 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_stages: [commit] 2 | default_install_hook_types: [pre-commit, commit-msg] 3 | 4 | ci: 5 | autoupdate_commit_msg: "chore: update pre-commit hooks" 6 | 7 | repos: 8 | - repo: https://github.com/astral-sh/ruff-pre-commit 9 | rev: v0.0.292 10 | hooks: 11 | - id: ruff 12 | args: [--fix, --show-fixes] 13 | 14 | - repo: https://github.com/psf/black 15 | rev: 23.9.1 16 | hooks: 17 | - id: black 18 | 19 | - repo: https://github.com/asottile/blacken-docs 20 | rev: "1.16.0" 21 | hooks: 22 | - id: blacken-docs 23 | additional_dependencies: [black>=23.7.0] 24 | 25 | - repo: https://github.com/pre-commit/pre-commit-hooks 26 | rev: v4.5.0 27 | hooks: 28 | - id: check-case-conflict 29 | - id: check-symlinks 30 | - id: check-yaml 31 | - id: destroyed-symlinks 32 | - id: end-of-file-fixer 33 | - id: mixed-line-ending 34 | - id: trailing-whitespace 35 | 36 | - repo: https://github.com/codespell-project/codespell 37 | rev: v2.2.6 38 | hooks: 39 | - id: codespell 40 | stages: [commit, commit-msg] 41 | exclude_types: [json, bib, svg] 42 | args: [--ignore-words-list, "mater,fwe,te"] 43 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating 6 | documentation, submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in this project a harassment-free 9 | experience for everyone, regardless of level of experience, gender, gender 10 | identity and expression, sexual orientation, disability, personal appearance, 11 | body size, race, ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | * The use of sexualized language or imagery 16 | * Personal attacks 17 | * Trolling or insulting/derogatory comments 18 | * Public or private harassment 19 | * Publishing other's private information, such as physical or electronic 20 | addresses, without explicit permission 21 | * Other unethical or unprofessional conduct 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or 24 | reject comments, commits, code, wiki edits, issues, and other contributions 25 | that are not aligned to this Code of Conduct, or to ban temporarily or 26 | permanently any contributor for other behaviors that they deem inappropriate, 27 | threatening, offensive, or harmful. 28 | 29 | By adopting this Code of Conduct, project maintainers commit themselves to 30 | fairly and consistently applying these principles to every aspect of managing 31 | this project. Project maintainers who do not follow or enforce the Code of 32 | Conduct may be permanently removed from the project team. 33 | 34 | This Code of Conduct applies both within project spaces and in public spaces 35 | when an individual is representing the project or its community. 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 38 | reported by contacting a project maintainer at conduct@materialsproject.org. All 39 | complaints will be reviewed and investigated and will result in a response that 40 | is deemed necessary and appropriate to the circumstances. Maintainers are 41 | obligated to maintain confidentiality with regard to the reporter of an 42 | incident to the extent possible by law and institutional policy. 43 | 44 | 45 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 46 | version 1.3.0, available at https://www.contributor-covenant.org/version/1/3/0/code-of-conduct.html 47 | 48 | [homepage]: https://www.contributor-covenant.org 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | maggma Copyright (c) 2017, The Regents of the University of 2 | California, through Lawrence Berkeley National Laboratory (subject 3 | to receipt of any required approvals from the U.S. Dept. of Energy). 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions 8 | are met: 9 | 10 | (1) Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | 13 | (2) Redistributions in binary form must reproduce the above 14 | copyright notice, this list of conditions and the following 15 | disclaimer in the documentation and/or other materials provided with 16 | the distribution. 17 | 18 | (3) Neither the name of the University of California, Lawrence 19 | Berkeley National Laboratory, U.S. Dept. of Energy nor the names of 20 | its contributors may be used to endorse or promote products derived 21 | from this software without specific prior written permission. 22 | 23 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 26 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 27 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 28 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 29 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 30 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 31 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 32 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 33 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 34 | POSSIBILITY OF SUCH DAMAGE. 35 | 36 | You are under no obligation whatsoever to provide any bug fixes, 37 | patches, or upgrades to the features, functionality or performance 38 | of the source code ("Enhancements") to anyone; however, if you 39 | choose to make your Enhancements available either publicly, or 40 | directly to Lawrence Berkeley National Laboratory or its 41 | contributors, without imposing a separate written license agreement 42 | for such Enhancements, then you hereby grant the following license: 43 | a non-exclusive, royalty-free perpetual license to install, use, 44 | modify, prepare derivative works, incorporate into other computer 45 | software, distribute, and sublicense such enhancements or derivative 46 | works thereof, in binary and source code form. 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Materials Project API 2 | 3 | [![testing](https://github.com/materialsproject/api/workflows/testing/badge.svg)](https://github.com/materialsproject/api/actions?query=workflow%3Atesting) 4 | [![codecov](https://codecov.io/gh/materialsproject/api/branch/main/graph/badge.svg)](https://codecov.io/gh/materialsproject/api) 5 | ![python](https://img.shields.io/badge/Python-3.9+-blue.svg?logo=python&logoColor=white) 6 | 7 | This repository is the development environment for the new Materials Project API. A core client implementation will reside here. For information on how to use the API, please see the updated [documentation](https://docs.materialsproject.org/downloading-data/how-do-i-download-the-materials-project-database). 8 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/_templates/custom-class-template.rst: -------------------------------------------------------------------------------- 1 | {{ fullname | escape | underline}} 2 | 3 | .. currentmodule:: {{ module }} 4 | 5 | .. autoclass:: {{ objname }} 6 | :members: 7 | :show-inheritance: 8 | :inherited-members: 9 | 10 | {% block methods %} 11 | .. automethod:: __init__ 12 | 13 | {% if methods %} 14 | .. rubric:: {{ _('Methods') }} 15 | 16 | .. autosummary:: 17 | {% for item in methods %} 18 | ~{{ name }}.{{ item }} 19 | {%- endfor %} 20 | {% endif %} 21 | {% endblock %} 22 | 23 | {% block attributes %} 24 | {% if attributes %} 25 | .. rubric:: {{ _('Attributes') }} 26 | 27 | .. autosummary:: 28 | {% for item in attributes %} 29 | ~{{ name }}.{{ item }} 30 | {%- endfor %} 31 | {% endif %} 32 | {% endblock %} 33 | -------------------------------------------------------------------------------- /docs/_templates/custom-module-template.rst: -------------------------------------------------------------------------------- 1 | {{ fullname | escape | underline}} 2 | 3 | .. automodule:: {{ fullname }} 4 | 5 | {% block attributes %} 6 | {% if attributes %} 7 | .. rubric:: Module Attributes 8 | 9 | .. autosummary:: 10 | :toctree: 11 | {% for item in attributes %} 12 | {{ item }} 13 | {%- endfor %} 14 | {% endif %} 15 | {% endblock %} 16 | 17 | {% block functions %} 18 | {% if functions %} 19 | .. rubric:: {{ _('Functions') }} 20 | 21 | .. autosummary:: 22 | :toctree: 23 | {% for item in functions %} 24 | {{ item }} 25 | {%- endfor %} 26 | {% endif %} 27 | {% endblock %} 28 | 29 | {% block classes %} 30 | {% if classes %} 31 | .. rubric:: {{ _('Classes') }} 32 | 33 | .. autosummary:: 34 | :toctree: 35 | :template: custom-class-template.rst 36 | {% for item in classes %} 37 | {{ item }} 38 | {%- endfor %} 39 | {% endif %} 40 | {% endblock %} 41 | 42 | {% block exceptions %} 43 | {% if exceptions %} 44 | .. rubric:: {{ _('Exceptions') }} 45 | 46 | .. autosummary:: 47 | :toctree: 48 | {% for item in exceptions %} 49 | {{ item }} 50 | {%- endfor %} 51 | {% endif %} 52 | {% endblock %} 53 | 54 | {% block modules %} 55 | {% if modules %} 56 | .. rubric:: Modules 57 | 58 | .. autosummary:: 59 | :toctree: 60 | :template: custom-module-template.rst 61 | :recursive: 62 | {% for item in modules %} 63 | {{ item }} 64 | {%- endfor %} 65 | {% endif %} 66 | {% endblock %} 67 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | 9 | project = "mp-api" 10 | copyright = "2022, The Materials Project" 11 | author = "The Materials Project" 12 | release = "2022" 13 | 14 | # -- General configuration --------------------------------------------------- 15 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 16 | 17 | extensions = [ 18 | "sphinx.ext.autodoc", 19 | "sphinx.ext.viewcode", 20 | "sphinx.ext.intersphinx", 21 | "sphinx.ext.napoleon", 22 | "sphinx.ext.autosummary", 23 | ] 24 | 25 | ## Include Python objects as they appear in source files 26 | ## Default: alphabetically ('alphabetical') 27 | autodoc_member_order = "bysource" 28 | ## Default flags used by autodoc directives 29 | autodoc_default_flags = ["members", "show-inheritance"] 30 | autoclass_content = "init" 31 | ## Generate autodoc stubs with summaries from code 32 | autosummary_generate = True 33 | 34 | templates_path = ["_templates"] 35 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 36 | 37 | # The suffix(es) of source filenames. 38 | # You can specify multiple suffix as a list of string: 39 | # 40 | # source_suffix = ['.rst', '.md'] 41 | source_suffix = ".rst" 42 | 43 | # The master toctree document. 44 | master_doc = "index" 45 | 46 | # The name of the Pygments (syntax highlighting) style to use. 47 | pygments_style = "sphinx" 48 | 49 | # -- Options for HTML output ------------------------------------------------- 50 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 51 | 52 | html_theme = "alabaster" 53 | # html_static_path = ['_static'] 54 | 55 | # Theme options are theme-specific and customize the look and feel of a theme 56 | # further. For a list of options available for each theme, see the 57 | # documentation. 58 | # 59 | html_theme_options = { 60 | "github_button": True, 61 | "github_type": "star&v=2", 62 | "github_user": "materialsproject", 63 | "github_repo": "api", 64 | "github_banner": True, 65 | } 66 | 67 | # Custom sidebar templates, must be a dictionary that maps document names 68 | # to template names. 69 | # 70 | # The default sidebars (for documents that don't match any pattern) are 71 | # defined by theme itself. Builtin themes are using these templates by 72 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 73 | # 'searchbox.html']``. 74 | # 75 | html_sidebars = { 76 | "**": [ 77 | "about.html", 78 | "navigation.html", 79 | "searchbox.html", 80 | ] 81 | } 82 | 83 | # -- Options for HTMLHelp output --------------------------------------------- 84 | 85 | # Output file base name for HTML help builder. 86 | htmlhelp_basename = "mapidoc" 87 | 88 | 89 | # -- Options for manual page output ------------------------------------------ 90 | 91 | # One entry per manual page. List of tuples 92 | # (source start file, name, description, authors, manual section). 93 | man_pages = [(master_doc, "mp-api", "mp-api Documentation", [author], 1)] 94 | 95 | 96 | # -- Options for Texinfo output ---------------------------------------------- 97 | 98 | # Grouping the document tree into Texinfo files. List of tuples 99 | # (source start file, target name, title, author, 100 | # dir menu entry, description, category) 101 | texinfo_documents = [ 102 | ( 103 | master_doc, 104 | "mp-api", 105 | "mp-api Documentation", 106 | author, 107 | "mp-api", 108 | "The Materials Project API.", 109 | "", 110 | ), 111 | ] 112 | 113 | # -- Extension configuration ------------------------------------------------- 114 | 115 | autodoc_mock_imports = [] 116 | 117 | # Example configuration for intersphinx: refer to the Python standard library. 118 | ## Add Python version number to the default address to corretcly reference 119 | ## the Python standard library 120 | intersphinx_mapping = {"https://docs.python.org/3.8": None} 121 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. mp-api documentation master file, created by 2 | sphinx-quickstart on Mon Oct 3 13:56:19 2022. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to the **mp-api** code documentation! 7 | ================================== 8 | **mp-api** contains the core client implementation for the updated Materials Project API. 9 | 10 | This is intended to be barebones documentation for the client classes and their methods. 11 | For more comprehensive documentation on how to use the code to access Materials Project data, `see the main updated documentation `_. 12 | 13 | Support 14 | ------- 15 | 16 | If you are having issues, please let us know through `github `_, or `on the MatSci community discourse `_. 17 | 18 | Contents 19 | -------- 20 | 21 | .. toctree:: 22 | :maxdepth: 2 23 | 24 | modules 25 | 26 | 27 | 28 | Indices and tables 29 | ================== 30 | 31 | * :ref:`genindex` 32 | * :ref:`modindex` 33 | * :ref:`search` 34 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/modules.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | Project Modules 4 | =============== 5 | 6 | This page contains the list of project's modules 7 | 8 | .. autosummary:: 9 | :toctree: _autosummary 10 | :template: custom-module-template.rst 11 | :recursive: 12 | 13 | mp_api.client.mprester 14 | mp_api.client.routes 15 | -------------------------------------------------------------------------------- /mp_api/client/__init__.py: -------------------------------------------------------------------------------- 1 | """Primary MAPI module.""" 2 | from __future__ import annotations 3 | 4 | import os 5 | from importlib.metadata import PackageNotFoundError, version 6 | 7 | from .core import MPRestError 8 | from .mprester import MPRester 9 | 10 | try: 11 | __version__ = version("mp_api") 12 | except PackageNotFoundError: # pragma: no cover 13 | __version__ = os.getenv("SETUPTOOLS_SCM_PRETEND_VERSION") 14 | -------------------------------------------------------------------------------- /mp_api/client/core/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from .client import BaseRester, MPRestError, MPRestWarning 4 | from .settings import MAPIClientSettings 5 | -------------------------------------------------------------------------------- /mp_api/client/core/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | from multiprocessing import cpu_count 3 | from typing import List 4 | 5 | from pydantic import Field 6 | from pydantic_settings import BaseSettings 7 | from pymatgen.core import _load_pmg_settings 8 | 9 | from mp_api.client import __file__ as root_dir 10 | 11 | PMG_SETTINGS = _load_pmg_settings() 12 | _NUM_PARALLEL_REQUESTS = min(PMG_SETTINGS.get("MPRESTER_NUM_PARALLEL_REQUESTS", 4), 4) 13 | _MAX_RETRIES = min(PMG_SETTINGS.get("MPRESTER_MAX_RETRIES", 3), 3) 14 | _MUTE_PROGRESS_BAR = PMG_SETTINGS.get("MPRESTER_MUTE_PROGRESS_BARS", False) 15 | _MAX_HTTP_URL_LENGTH = PMG_SETTINGS.get("MPRESTER_MAX_HTTP_URL_LENGTH", 2000) 16 | _MAX_LIST_LENGTH = min(PMG_SETTINGS.get("MPRESTER_MAX_LIST_LENGTH", 10000), 10000) 17 | 18 | try: 19 | CPU_COUNT = cpu_count() 20 | except NotImplementedError: 21 | pass 22 | 23 | 24 | class MAPIClientSettings(BaseSettings): 25 | """Special class to store settings for MAPI client 26 | python module. 27 | """ 28 | 29 | TEST_FILES: str = Field( 30 | os.path.join(os.path.dirname(os.path.abspath(root_dir)), "../../test_files"), 31 | description="Directory with test files", 32 | ) 33 | 34 | QUERY_NO_PARALLEL: List[str] = Field( 35 | [ 36 | "elements", 37 | "exclude_elements", 38 | "possible_species", 39 | "coordination_envs", 40 | "coordination_envs_anonymous", 41 | "has_props", 42 | "gb_plane", 43 | "rotation_axis", 44 | "keywords", 45 | "substrate_orientation", 46 | "film_orientation", 47 | "synthesis_type", 48 | "operations", 49 | "condition_mixing_device", 50 | "condition_mixing_media", 51 | "condition_heating_atmosphere", 52 | "operations", 53 | "_fields", 54 | ], 55 | description="List API query parameters that do not support parallel requests.", 56 | ) 57 | 58 | NUM_PARALLEL_REQUESTS: int = Field( 59 | _NUM_PARALLEL_REQUESTS, 60 | description="Number of parallel requests to send.", 61 | ) 62 | 63 | MAX_RETRIES: int = Field( 64 | _MAX_RETRIES, description="Maximum number of retries for requests." 65 | ) 66 | 67 | BACKOFF_FACTOR: float = Field( 68 | 0.1, 69 | description="A backoff factor to apply between retry attempts. To disable, set to 0.", 70 | ) 71 | 72 | MUTE_PROGRESS_BARS: bool = Field( 73 | _MUTE_PROGRESS_BAR, 74 | description="Whether to mute progress bars when data is retrieved.", 75 | ) 76 | 77 | MAX_HTTP_URL_LENGTH: int = Field( 78 | _MAX_HTTP_URL_LENGTH, 79 | description="Number of characters to use to define the maximum length of a given HTTP URL.", 80 | ) 81 | 82 | MIN_EMMET_VERSION: str = Field( 83 | "0.54.0", description="Minimum compatible version of emmet-core for the client." 84 | ) 85 | 86 | MAX_LIST_LENGTH: int = Field( 87 | _MAX_LIST_LENGTH, description="Maximum length of query parameter list" 88 | ) 89 | 90 | class Config: 91 | env_prefix = "MPRESTER_" 92 | -------------------------------------------------------------------------------- /mp_api/client/core/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import re 4 | from functools import cache 5 | from typing import Optional, get_args 6 | 7 | from maggma.utils import get_flat_models_from_model 8 | from monty.json import MSONable 9 | from pydantic import BaseModel 10 | from pydantic._internal._utils import lenient_issubclass 11 | from pydantic.fields import FieldInfo 12 | 13 | from mp_api.client.core.settings import MAPIClientSettings 14 | 15 | 16 | def validate_ids(id_list: list[str]): 17 | """Function to validate material and task IDs. 18 | 19 | Args: 20 | id_list (List[str]): List of material or task IDs. 21 | 22 | Raises: 23 | ValueError: If at least one ID is not formatted correctly. 24 | 25 | Returns: 26 | id_list: Returns original ID list if everything is formatted correctly. 27 | """ 28 | if len(id_list) > MAPIClientSettings().MAX_LIST_LENGTH: 29 | raise ValueError( 30 | "List of material/molecule IDs provided is too long. Consider removing the ID filter to automatically pull" 31 | " data for all IDs and filter locally." 32 | ) 33 | 34 | pattern = "(mp|mvc|mol|mpcule)-.*" 35 | 36 | for entry in id_list: 37 | if re.match(pattern, entry) is None: 38 | raise ValueError(f"{entry} is not formatted correctly!") 39 | 40 | return id_list 41 | 42 | 43 | @cache 44 | def api_sanitize( 45 | pydantic_model: BaseModel, 46 | fields_to_leave: list[str] | None = None, 47 | allow_dict_msonable=False, 48 | ): 49 | """Function to clean up pydantic models for the API by: 50 | 1.) Making fields optional 51 | 2.) Allowing dictionaries in-place of the objects for MSONable quantities. 52 | 53 | WARNING: This works in place, so it mutates the model and all sub-models 54 | 55 | Args: 56 | pydantic_model (BaseModel): Pydantic model to alter 57 | fields_to_leave (list[str] | None): list of strings for model fields as "model__name__.field". 58 | Defaults to None. 59 | allow_dict_msonable (bool): Whether to allow dictionaries in place of MSONable quantities. 60 | Defaults to False 61 | """ 62 | models = [ 63 | model 64 | for model in get_flat_models_from_model(pydantic_model) 65 | if issubclass(model, BaseModel) 66 | ] # type: list[BaseModel] 67 | 68 | fields_to_leave = fields_to_leave or [] 69 | fields_tuples = [f.split(".") for f in fields_to_leave] 70 | assert all(len(f) == 2 for f in fields_tuples) 71 | 72 | for model in models: 73 | model_fields_to_leave = {f[1] for f in fields_tuples if model.__name__ == f[0]} 74 | for name in model.model_fields: 75 | field = model.model_fields[name] 76 | field_json_extra = field.json_schema_extra 77 | field_type = field.annotation 78 | 79 | if field_type is not None and allow_dict_msonable: 80 | if lenient_issubclass(field_type, MSONable): 81 | field_type = allow_msonable_dict(field_type) 82 | else: 83 | for sub_type in get_args(field_type): 84 | if lenient_issubclass(sub_type, MSONable): 85 | allow_msonable_dict(sub_type) 86 | 87 | if name not in model_fields_to_leave: 88 | new_field = FieldInfo.from_annotated_attribute( 89 | Optional[field_type], None 90 | ) 91 | new_field.json_schema_extra = field_json_extra or {} 92 | model.model_fields[name] = new_field 93 | 94 | model.model_rebuild(force=True) 95 | 96 | return pydantic_model 97 | 98 | 99 | def allow_msonable_dict(monty_cls: type[MSONable]): 100 | """Patch Monty to allow for dict values for MSONable.""" 101 | 102 | def validate_monty(cls, v, _): 103 | """Stub validator for MSONable as a dictionary only.""" 104 | if isinstance(v, cls): 105 | return v 106 | elif isinstance(v, dict): 107 | # Just validate the simple Monty Dict Model 108 | errors = [] 109 | if v.get("@module", "") != monty_cls.__module__: 110 | errors.append("@module") 111 | 112 | if v.get("@class", "") != monty_cls.__name__: 113 | errors.append("@class") 114 | 115 | if len(errors) > 0: 116 | raise ValueError( 117 | "Missing Monty seriailzation fields in dictionary: {errors}" 118 | ) 119 | 120 | return v 121 | else: 122 | raise ValueError(f"Must provide {cls.__name__} or MSONable dictionary") 123 | 124 | monty_cls.validate_monty_v2 = classmethod(validate_monty) 125 | 126 | return monty_cls 127 | -------------------------------------------------------------------------------- /mp_api/client/routes/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from ._general_store import GeneralStoreRester 4 | from ._messages import MessagesRester 5 | from ._user_settings import UserSettingsRester 6 | -------------------------------------------------------------------------------- /mp_api/client/routes/_general_store.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from emmet.core._general_store import GeneralStoreDoc 4 | 5 | from mp_api.client.core import BaseRester 6 | 7 | 8 | class GeneralStoreRester(BaseRester[GeneralStoreDoc]): # pragma: no cover 9 | suffix = "_general_store" 10 | document_model = GeneralStoreDoc # type: ignore 11 | primary_key = "submission_id" 12 | monty_decode = False 13 | use_document_model = False 14 | 15 | def add_item(self, kind: str, markdown: str, meta: dict): # pragma: no cover 16 | """Set general store data. 17 | 18 | Args: 19 | kind: Data type description 20 | markdown: Markdown data 21 | meta: Metadata 22 | Returns: 23 | Dictionary with written data and submission id. 24 | 25 | 26 | Raises: 27 | MPRestError. 28 | """ 29 | return self._post_resource( 30 | body=meta, params={"kind": kind, "markdown": markdown} 31 | ).get("data") 32 | 33 | def get_items(self, kind): # pragma: no cover 34 | """Get general store data. 35 | 36 | Args: 37 | kind: Data type description 38 | Returns: 39 | List of dictionaries with kind, markdown, metadata, and submission_id. 40 | 41 | 42 | Raises: 43 | MPRestError. 44 | """ 45 | return self.search(kind=kind) 46 | -------------------------------------------------------------------------------- /mp_api/client/routes/_messages.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from datetime import datetime 4 | 5 | from emmet.core._messages import MessagesDoc, MessageType 6 | 7 | from mp_api.client.core import BaseRester 8 | 9 | 10 | class MessagesRester(BaseRester[MessagesDoc]): # pragma: no cover 11 | suffix = "_messages" 12 | document_model = MessagesDoc # type: ignore 13 | primary_key = "title" 14 | monty_decode = False 15 | use_document_model = False 16 | 17 | def set_message( 18 | self, 19 | title: str, 20 | body: str, 21 | type: MessageType = MessageType.generic, 22 | authors: list[str] = None, 23 | ): # pragma: no cover 24 | """Set user settings. 25 | 26 | Args: 27 | title: Message title 28 | body: Message text body 29 | type: Message type 30 | authors: Message authors 31 | Returns: 32 | Dictionary with updated message data 33 | 34 | 35 | Raises: 36 | MPRestError. 37 | """ 38 | d = {"title": title, "body": body, "type": type.value, "authors": authors or []} 39 | 40 | return self._post_resource(body=d).get("data") 41 | 42 | def get_messages( 43 | self, 44 | last_updated: datetime, 45 | sort_fields: list[str] | None = None, 46 | num_chunks: int | None = None, 47 | chunk_size: int = 1000, 48 | all_fields: bool = True, 49 | fields: list[str] | None = None, 50 | ): # pragma: no cover 51 | """Get user settings. 52 | 53 | Args: 54 | last_updated (datetime): Datetime to use to query for newer messages 55 | sort_fields (List[str]): Fields used to sort results. Prefix with '-' to sort in descending order. 56 | num_chunks (int): Maximum number of chunks of data to yield. None will yield all possible. 57 | chunk_size (int): Number of data entries per chunk. 58 | all_fields (bool): Whether to return all fields in the document. Defaults to True. 59 | fields (List[str]): List of fields to project. 60 | 61 | Returns: 62 | Dictionary with messages data 63 | 64 | 65 | Raises: 66 | MPRestError. 67 | """ 68 | query_params = {} 69 | 70 | if sort_fields: 71 | query_params.update( 72 | {"_sort_fields": ",".join([s.strip() for s in sort_fields])} 73 | ) 74 | 75 | return self._search( 76 | last_updated=last_updated, 77 | num_chunks=num_chunks, 78 | chunk_size=chunk_size, 79 | all_fields=all_fields, 80 | fields=fields, 81 | **query_params, 82 | ) 83 | -------------------------------------------------------------------------------- /mp_api/client/routes/_user_settings.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from emmet.core._user_settings import UserSettingsDoc 4 | 5 | from mp_api.client.core import BaseRester 6 | 7 | 8 | class UserSettingsRester(BaseRester[UserSettingsDoc]): # pragma: no cover 9 | suffix = "_user_settings" 10 | document_model = UserSettingsDoc # type: ignore 11 | primary_key = "consumer_id" 12 | monty_decode = False 13 | use_document_model = False 14 | 15 | def create_user_settings(self, consumer_id, settings): 16 | """Create user settings. 17 | 18 | Args: 19 | consumer_id: Consumer ID for the user 20 | settings: Dictionary with user settings that 21 | use UserSettingsDoc schema 22 | Returns: 23 | Dictionary with consumer_id and write status. 24 | """ 25 | return self._post_resource( 26 | body=settings, params={"consumer_id": consumer_id} 27 | ).get("data") 28 | 29 | def patch_user_settings(self, consumer_id, settings): # pragma: no cover 30 | """Patch user settings. 31 | 32 | Args: 33 | consumer_id: Consumer ID for the user 34 | settings: Dictionary with user settings 35 | Returns: 36 | Dictionary with consumer_id and write status. 37 | 38 | 39 | Raises: 40 | MPRestError. 41 | """ 42 | body = dict() 43 | for key in settings: 44 | if key not in [ 45 | "institution", 46 | "sector", 47 | "job_role", 48 | "is_email_subscribed", 49 | "agreed_terms", 50 | ]: 51 | raise ValueError( 52 | f"Invalid setting key {key}. Must be one of" 53 | "institution, sector, job_role, is_email_subscribed, agreed_terms" 54 | ) 55 | body[f"settings.{key}"] = settings[key] 56 | 57 | return self._patch_resource(body=body, params={"consumer_id": consumer_id}).get( 58 | "data" 59 | ) 60 | 61 | def patch_user_time_settings(self, consumer_id, time): # pragma: no cover 62 | """Set user settings last_read_message field. 63 | 64 | Args: 65 | consumer_id: Consumer ID for the user 66 | time: utc datetime object for when the user last see messages 67 | Returns: 68 | Dictionary with consumer_id and write status. 69 | 70 | 71 | Raises: 72 | MPRestError. 73 | """ 74 | return self._patch_resource( 75 | body={"settings.message_last_read": time.isoformat()}, 76 | params={"consumer_id": consumer_id}, 77 | ).get("data") 78 | 79 | def get_user_settings(self, consumer_id, fields): # pragma: no cover 80 | """Get user settings. 81 | 82 | Args: 83 | consumer_id: Consumer ID for the user 84 | fields: List of fields to project 85 | Returns: 86 | Dictionary with consumer_id and settings. 87 | 88 | 89 | Raises: 90 | MPRestError. 91 | """ 92 | return self._query_resource( 93 | suburl=f"{consumer_id}", fields=fields, num_chunks=1, chunk_size=1 94 | ).get("data") 95 | -------------------------------------------------------------------------------- /mp_api/client/routes/materials/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from .absorption import AbsorptionRester 4 | from .bonds import BondsRester 5 | from .chemenv import ChemenvRester 6 | from .dielectric import DielectricRester 7 | from .doi import DOIRester 8 | from .elasticity import ElasticityRester 9 | from .electrodes import ElectrodeRester 10 | from .electronic_structure import ( 11 | BandStructureRester, 12 | DosRester, 13 | ElectronicStructureRester, 14 | ) 15 | from .eos import EOSRester 16 | from .grain_boundaries import GrainBoundaryRester 17 | from .magnetism import MagnetismRester 18 | from .oxidation_states import OxidationStatesRester 19 | from .phonon import PhononRester 20 | from .piezo import PiezoRester 21 | from .provenance import ProvenanceRester 22 | from .robocrys import RobocrysRester 23 | from .similarity import SimilarityRester 24 | from .substrates import SubstratesRester 25 | from .summary import SummaryRester 26 | from .surface_properties import SurfacePropertiesRester 27 | from .synthesis import SynthesisRester 28 | from .tasks import TaskRester 29 | from .thermo import ThermoRester 30 | from .xas import XASRester 31 | 32 | try: 33 | from .alloys import AlloysRester 34 | except ImportError: 35 | AlloysRester = None # type: ignore 36 | 37 | try: 38 | from .charge_density import ChargeDensityRester 39 | except ImportError: 40 | ChargeDensityRester = None # type: ignore 41 | -------------------------------------------------------------------------------- /mp_api/client/routes/materials/absorption.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections import defaultdict 4 | 5 | from emmet.core.absorption import AbsorptionDoc 6 | 7 | from mp_api.client.core import BaseRester 8 | from mp_api.client.core.utils import validate_ids 9 | 10 | 11 | class AbsorptionRester(BaseRester[AbsorptionDoc]): 12 | suffix = "materials/absorption" 13 | document_model = AbsorptionDoc # type: ignore 14 | primary_key = "material_id" 15 | 16 | def search( 17 | self, 18 | material_ids: str | list[str] | None = None, 19 | chemsys: str | list[str] | None = None, 20 | elements: list[str] | None = None, 21 | exclude_elements: list[str] | None = None, 22 | formula: list[str] | None = None, 23 | num_chunks: int | None = None, 24 | chunk_size: int = 1000, 25 | all_fields: bool = True, 26 | fields: list[str] | None = None, 27 | ) -> list[AbsorptionDoc] | list[dict]: 28 | """Query for optical absorption spectra data. 29 | 30 | Arguments: 31 | material_ids (str, List[str]): Search for optical absorption data associated with the specified Material IDs 32 | chemsys (str, List[str]): A chemical system or list of chemical systems 33 | (e.g., Li-Fe-O, Si-*, [Si-O, Li-Fe-P]). 34 | elements (List[str]): A list of elements. 35 | exclude_elements (List[str]): A list of elements to exclude. 36 | formula (str, List[str]): A formula including anonymized formula 37 | or wild cards (e.g., Fe2O3, ABO3, Si*). A list of chemical formulas can also be passed 38 | (e.g., [Fe2O3, ABO3]). 39 | num_chunks (int): Maximum number of chunks of data to yield. None will yield all possible. 40 | chunk_size (int): Number of data entries per chunk. 41 | all_fields (bool): Whether to return all fields in the document. Defaults to True. 42 | fields (List[str]): List of fields in AbsorptionDoc to return data for. 43 | 44 | Returns: 45 | ([AbsorptionDoc], [dict]) List of optical absorption documents or dictionaries. 46 | """ 47 | query_params = defaultdict(dict) # type: dict 48 | 49 | if formula: 50 | if isinstance(formula, str): 51 | formula = [formula] 52 | 53 | query_params.update({"formula": ",".join(formula)}) 54 | 55 | if chemsys: 56 | if isinstance(chemsys, str): 57 | chemsys = [chemsys] 58 | 59 | query_params.update({"chemsys": ",".join(chemsys)}) 60 | 61 | if elements: 62 | query_params.update({"elements": ",".join(elements)}) 63 | 64 | if exclude_elements: 65 | query_params.update({"exclude_elements": ",".join(exclude_elements)}) 66 | 67 | if material_ids: 68 | if isinstance(material_ids, str): 69 | material_ids = [material_ids] 70 | 71 | query_params.update({"material_ids": ",".join(validate_ids(material_ids))}) 72 | 73 | query_params = { 74 | entry: query_params[entry] 75 | for entry in query_params 76 | if query_params[entry] is not None 77 | } 78 | 79 | return super()._search( 80 | formulae=formula, 81 | num_chunks=num_chunks, 82 | chunk_size=chunk_size, 83 | all_fields=all_fields, 84 | fields=fields, 85 | **query_params, 86 | ) 87 | -------------------------------------------------------------------------------- /mp_api/client/routes/materials/alloys.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections import defaultdict 4 | 5 | from emmet.core.alloys import AlloyPairDoc 6 | 7 | from mp_api.client.core import BaseRester 8 | from mp_api.client.core.utils import validate_ids 9 | 10 | 11 | class AlloysRester(BaseRester[AlloyPairDoc]): 12 | suffix = "materials/alloys" 13 | document_model = AlloyPairDoc # type: ignore 14 | primary_key = "pair_id" 15 | 16 | def search( 17 | self, 18 | material_ids: str | list[str] | None = None, 19 | formulae: list[str] | None = None, 20 | num_chunks: int | None = None, 21 | chunk_size: int = 1000, 22 | all_fields: bool = True, 23 | fields: list[str] | None = None, 24 | ) -> list[AlloyPairDoc] | list[dict]: 25 | """Query for hypothetical alloys formed between two commensurate 26 | crystal structures, following the methodology in 27 | https://doi.org/10.48550/arXiv.2206.10715. 28 | 29 | Please cite the relevant publication if data provided by this 30 | endpoint is useful. 31 | 32 | Arguments: 33 | material_ids (str, List[str]): Search for alloys containing the specified Material IDs 34 | formulae (List[str]): Search for alloys containing the specified formulae 35 | num_chunks (int): Maximum number of chunks of data to yield. None will yield all possible. 36 | chunk_size (int): Number of data entries per chunk. 37 | all_fields (bool): Whether to return all fields in the document. Defaults to True. 38 | fields (List[str]): List of fields in AlloyPairDoc to return data for. 39 | 40 | Returns: 41 | ([AlloyPairDoc], [dict]) List of alloy pair documents or dictionaries. 42 | """ 43 | query_params = defaultdict(dict) # type: dict 44 | 45 | query_params = { 46 | entry: query_params[entry] 47 | for entry in query_params 48 | if query_params[entry] is not None 49 | } 50 | 51 | if material_ids: 52 | if isinstance(material_ids, str): 53 | material_ids = [material_ids] 54 | 55 | query_params.update({"material_ids": ",".join(validate_ids(material_ids))}) 56 | 57 | return super()._search( 58 | formulae=formulae, 59 | num_chunks=num_chunks, 60 | chunk_size=chunk_size, 61 | all_fields=all_fields, 62 | fields=fields, 63 | **query_params, 64 | ) 65 | -------------------------------------------------------------------------------- /mp_api/client/routes/materials/bonds.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections import defaultdict 4 | 5 | from emmet.core.bonds import BondingDoc 6 | 7 | from mp_api.client.core import BaseRester 8 | from mp_api.client.core.utils import validate_ids 9 | 10 | 11 | class BondsRester(BaseRester[BondingDoc]): 12 | suffix = "materials/bonds" 13 | document_model = BondingDoc # type: ignore 14 | primary_key = "material_id" 15 | 16 | def search( 17 | self, 18 | material_ids: str | list[str] | None = None, 19 | coordination_envs: list[str] | None = None, 20 | coordination_envs_anonymous: list[str] | None = None, 21 | max_bond_length: tuple[float, float] | None = None, 22 | mean_bond_length: tuple[float, float] | None = None, 23 | min_bond_length: tuple[float, float] | None = None, 24 | num_chunks: int | None = None, 25 | chunk_size: int = 1000, 26 | all_fields: bool = True, 27 | fields: list[str] | None = None, 28 | ) -> list[BondingDoc] | list[dict]: 29 | """Query bonding docs using a variety of search criteria. 30 | 31 | Arguments: 32 | material_ids (str, List[str]): Search for bonding data for the specified Material IDs 33 | coordination_envs (List[str]): List of coordination environments to consider (e.g. ['Mo-S(6)', 'S-Mo(3)']). 34 | coordination_envs_anonymous (List[str]): List of anonymous coordination environments to consider 35 | (e.g. ['A-B(6)', 'A-B(3)']). 36 | max_bond_length (Tuple[float,float]): Minimum and maximum value for the maximum bond length 37 | in the structure to consider. 38 | mean_bond_length (Tuple[float,float]): Minimum and maximum value for the mean bond length 39 | in the structure to consider. 40 | min_bond_length (Tuple[float,float]): Minimum and maximum value for the minimum bond length 41 | in the structure to consider. 42 | num_chunks (int): Maximum number of chunks of data to yield. None will yield all possible. 43 | chunk_size (int): Number of data entries per chunk. 44 | all_fields (bool): Whether to return all fields in the document. Defaults to True. 45 | fields (List[str]): List of fields in DielectricDoc to return data for. 46 | Default is material_id and last_updated if all_fields is False. 47 | 48 | Returns: 49 | ([BondingDoc], [dict]) List of bonding documents or dictionaries. 50 | """ 51 | query_params = defaultdict(dict) # type: dict 52 | 53 | if material_ids: 54 | if isinstance(material_ids, str): 55 | material_ids = [material_ids] 56 | 57 | query_params.update({"material_ids": ",".join(validate_ids(material_ids))}) 58 | 59 | if max_bond_length: 60 | query_params.update( 61 | { 62 | "max_bond_length_min": max_bond_length[0], 63 | "max_bond_length_max": max_bond_length[1], 64 | } 65 | ) 66 | 67 | if min_bond_length: 68 | query_params.update( 69 | { 70 | "min_bond_length_min": min_bond_length[0], 71 | "min_bond_length_max": min_bond_length[1], 72 | } 73 | ) 74 | 75 | if mean_bond_length: 76 | query_params.update( 77 | { 78 | "mean_bond_length_min": mean_bond_length[0], 79 | "mean_bond_length_max": mean_bond_length[1], 80 | } 81 | ) 82 | 83 | if coordination_envs is not None: 84 | query_params.update({"coordination_envs": ",".join(coordination_envs)}) 85 | 86 | if coordination_envs_anonymous is not None: 87 | query_params.update( 88 | {"coordination_envs_anonymous": ",".join(coordination_envs_anonymous)} 89 | ) 90 | 91 | query_params = { 92 | entry: query_params[entry] 93 | for entry in query_params 94 | if query_params[entry] is not None 95 | } 96 | 97 | return super()._search( 98 | num_chunks=num_chunks, 99 | chunk_size=chunk_size, 100 | all_fields=all_fields, 101 | fields=fields, 102 | **query_params, 103 | ) 104 | -------------------------------------------------------------------------------- /mp_api/client/routes/materials/dielectric.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections import defaultdict 4 | 5 | from emmet.core.polar import DielectricDoc 6 | 7 | from mp_api.client.core import BaseRester 8 | from mp_api.client.core.utils import validate_ids 9 | 10 | 11 | class DielectricRester(BaseRester[DielectricDoc]): 12 | suffix = "materials/dielectric" 13 | document_model = DielectricDoc # type: ignore 14 | primary_key = "material_id" 15 | 16 | def search( 17 | self, 18 | material_ids: str | list[str] | None = None, 19 | e_total: tuple[float, float] | None = None, 20 | e_ionic: tuple[float, float] | None = None, 21 | e_electronic: tuple[float, float] | None = None, 22 | n: tuple[float, float] | None = None, 23 | num_chunks: int | None = None, 24 | chunk_size: int = 1000, 25 | all_fields: bool = True, 26 | fields: list[str] | None = None, 27 | ) -> list[DielectricDoc] | list[dict]: 28 | """Query dielectric docs using a variety of search criteria. 29 | 30 | Arguments: 31 | material_ids (str, List[str]): A single Material ID string or list of strings 32 | (e.g., mp-149, [mp-149, mp-13]). 33 | e_total (Tuple[float,float]): Minimum and maximum total dielectric constant to consider. 34 | e_ionic (Tuple[float,float]): Minimum and maximum ionic dielectric constant to consider. 35 | e_electronic (Tuple[float,float]): Minimum and maximum electronic dielectric constant to consider. 36 | n (Tuple[float,float]): Minimum and maximum refractive index to consider. 37 | num_chunks (int): Maximum number of chunks of data to yield. None will yield all possible. 38 | chunk_size (int): Number of data entries per chunk. 39 | all_fields (bool): Whether to return all fields in the document. Defaults to True. 40 | fields (List[str]): List of fields in DielectricDoc to return data for. 41 | Default is material_id and last_updated if all_fields is False. 42 | 43 | Returns: 44 | ([DielectricDoc], [dict]) List of dielectric documents or dictionaries. 45 | """ 46 | query_params = defaultdict(dict) # type: dict 47 | 48 | if material_ids: 49 | if isinstance(material_ids, str): 50 | material_ids = [material_ids] 51 | 52 | query_params.update({"material_ids": ",".join(validate_ids(material_ids))}) 53 | 54 | if e_total: 55 | query_params.update({"e_total_min": e_total[0], "e_total_max": e_total[1]}) 56 | 57 | if e_ionic: 58 | query_params.update({"e_ionic_min": e_ionic[0], "e_ionic_max": e_ionic[1]}) 59 | 60 | if e_electronic: 61 | query_params.update( 62 | { 63 | "e_electronic_min": e_electronic[0], 64 | "e_electronic_max": e_electronic[1], 65 | } 66 | ) 67 | 68 | if n: 69 | query_params.update({"n_min": n[0], "n_max": n[1]}) 70 | 71 | query_params = { 72 | entry: query_params[entry] 73 | for entry in query_params 74 | if query_params[entry] is not None 75 | } 76 | 77 | return super()._search( 78 | num_chunks=num_chunks, 79 | chunk_size=chunk_size, 80 | all_fields=all_fields, 81 | fields=fields, 82 | **query_params, 83 | ) 84 | -------------------------------------------------------------------------------- /mp_api/client/routes/materials/doi.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections import defaultdict 4 | 5 | from emmet.core.dois import DOIDoc 6 | 7 | from mp_api.client.core import BaseRester 8 | from mp_api.client.core.utils import validate_ids 9 | 10 | 11 | class DOIRester(BaseRester[DOIDoc]): 12 | suffix = "doi" 13 | document_model = DOIDoc # type: ignore 14 | primary_key = "material_id" 15 | 16 | def search( 17 | self, 18 | material_ids: str | list[str] | None = None, 19 | num_chunks: int | None = None, 20 | chunk_size: int = 1000, 21 | all_fields: bool = True, 22 | fields: list[str] | None = None, 23 | ) -> list[DOIDoc] | list[dict]: 24 | """Query for DOI data. 25 | 26 | Arguments: 27 | material_ids (str, List[str]): Search for DOI data associated with the specified Material IDs 28 | num_chunks (int): Maximum number of chunks of data to yield. None will yield all possible. 29 | chunk_size (int): Number of data entries per chunk. 30 | all_fields (bool): Whether to return all fields in the document. Defaults to True. 31 | fields (List[str]): List of fields in DOIDoc to return data for. 32 | 33 | Returns: 34 | ([DOIDoc], [dict]) List of DOIDoc documents or dictionaries. 35 | """ 36 | query_params = defaultdict(dict) # type: dict 37 | 38 | if material_ids: 39 | if isinstance(material_ids, str): 40 | material_ids = [material_ids] 41 | 42 | query_params.update({"material_ids": ",".join(validate_ids(material_ids))}) 43 | 44 | query_params = { 45 | entry: query_params[entry] 46 | for entry in query_params 47 | if query_params[entry] is not None 48 | } 49 | 50 | return super()._search( 51 | num_chunks=num_chunks, 52 | chunk_size=chunk_size, 53 | all_fields=all_fields, 54 | fields=fields, 55 | **query_params, 56 | ) 57 | -------------------------------------------------------------------------------- /mp_api/client/routes/materials/elasticity.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections import defaultdict 4 | 5 | from emmet.core.elasticity import ElasticityDoc 6 | 7 | from mp_api.client.core import BaseRester 8 | from mp_api.client.core.utils import validate_ids 9 | 10 | 11 | class ElasticityRester(BaseRester[ElasticityDoc]): 12 | suffix = "materials/elasticity" 13 | document_model = ElasticityDoc # type: ignore 14 | primary_key = "material_id" 15 | 16 | def search( 17 | self, 18 | material_ids: str | list[str] | None = None, 19 | elastic_anisotropy: tuple[float, float] | None = None, 20 | g_voigt: tuple[float, float] | None = None, 21 | g_reuss: tuple[float, float] | None = None, 22 | g_vrh: tuple[float, float] | None = None, 23 | k_voigt: tuple[float, float] | None = None, 24 | k_reuss: tuple[float, float] | None = None, 25 | k_vrh: tuple[float, float] | None = None, 26 | poisson_ratio: tuple[float, float] | None = None, 27 | num_chunks: int | None = None, 28 | chunk_size: int = 1000, 29 | all_fields: bool = True, 30 | fields: list[str] | None = None, 31 | ) -> list[ElasticityDoc] | list[dict]: 32 | """Query elasticity docs using a variety of search criteria. 33 | 34 | Arguments: 35 | material_ids (str, List[str]): A single Material ID string or list of strings 36 | (e.g., mp-149, [mp-149, mp-13]). 37 | elastic_anisotropy (Tuple[float,float]): Minimum and maximum value to consider for 38 | the elastic anisotropy. 39 | g_voigt (Tuple[float,float]): Minimum and maximum value in GPa to consider for 40 | the Voigt average of the shear modulus. 41 | g_reuss (Tuple[float,float]): Minimum and maximum value in GPa to consider for 42 | the Reuss average of the shear modulus. 43 | g_vrh (Tuple[float,float]): Minimum and maximum value in GPa to consider for 44 | the Voigt-Reuss-Hill average of the shear modulus. 45 | k_voigt (Tuple[float,float]): Minimum and maximum value in GPa to consider for 46 | the Voigt average of the bulk modulus. 47 | k_reuss (Tuple[float,float]): Minimum and maximum value in GPa to consider for 48 | the Reuss average of the bulk modulus. 49 | k_vrh (Tuple[float,float]): Minimum and maximum value in GPa to consider for 50 | the Voigt-Reuss-Hill average of the bulk modulus. 51 | poisson_ratio (Tuple[float,float]): Minimum and maximum value to consider for 52 | Poisson's ratio. 53 | num_chunks (int): Maximum number of chunks of data to yield. None will yield all possible. 54 | chunk_size (int): Number of data entries per chunk. 55 | all_fields (bool): Whether to return all fields in the document. Defaults to True. 56 | fields (List[str]): List of fields in ElasticityDoc to return data for. 57 | Default is material_id and prett-formula if all_fields is False. 58 | 59 | Returns: 60 | ([ElasticityDoc], [dict]) List of elasticity documents or dictionaries. 61 | """ 62 | query_params = defaultdict(dict) # type: dict 63 | 64 | if material_ids: 65 | if isinstance(material_ids, str): 66 | material_ids = [material_ids] 67 | 68 | query_params.update({"material_ids": ",".join(validate_ids(material_ids))}) 69 | 70 | if k_voigt: 71 | query_params.update({"k_voigt_min": k_voigt[0], "k_voigt_max": k_voigt[1]}) 72 | 73 | if k_reuss: 74 | query_params.update({"k_reuss_min": k_reuss[0], "k_reuss_max": k_reuss[1]}) 75 | 76 | if k_vrh: 77 | query_params.update({"k_vrh_min": k_vrh[0], "g_vrh_max": k_vrh[1]}) 78 | 79 | if g_voigt: 80 | query_params.update({"g_voigt_min": g_voigt[0], "g_voigt_max": g_voigt[1]}) 81 | 82 | if g_reuss: 83 | query_params.update({"g_reuss_min": g_reuss[0], "g_reuss_max": g_reuss[1]}) 84 | 85 | if g_vrh: 86 | query_params.update({"g_vrh_min": g_vrh[0], "g_vrh_max": g_vrh[1]}) 87 | 88 | if elastic_anisotropy: 89 | query_params.update( 90 | { 91 | "elastic_anisotropy_min": elastic_anisotropy[0], 92 | "elastic_anisotropy_max": elastic_anisotropy[1], 93 | } 94 | ) 95 | 96 | if poisson_ratio: 97 | query_params.update( 98 | {"poisson_min": poisson_ratio[0], "poisson_max": poisson_ratio[1]} 99 | ) 100 | 101 | query_params = { 102 | entry: query_params[entry] 103 | for entry in query_params 104 | if query_params[entry] is not None 105 | } 106 | 107 | return super()._search( 108 | num_chunks=num_chunks, 109 | chunk_size=chunk_size, 110 | all_fields=all_fields, 111 | fields=fields, 112 | **query_params, 113 | ) 114 | -------------------------------------------------------------------------------- /mp_api/client/routes/materials/eos.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections import defaultdict 4 | 5 | from emmet.core.eos import EOSDoc 6 | 7 | from mp_api.client.core import BaseRester 8 | from mp_api.client.core.utils import validate_ids 9 | 10 | 11 | class EOSRester(BaseRester[EOSDoc]): 12 | suffix = "materials/eos" 13 | document_model = EOSDoc # type: ignore 14 | primary_key = "material_id" 15 | 16 | def search( 17 | self, 18 | material_ids: str | list[str] | None = None, 19 | energies: tuple[float, float] | None = None, 20 | volumes: tuple[float, float] | None = None, 21 | num_chunks: int | None = None, 22 | chunk_size: int = 1000, 23 | all_fields: bool = True, 24 | fields: list[str] | None = None, 25 | ) -> list[EOSDoc] | list[dict]: 26 | """Query equations of state docs using a variety of search criteria. 27 | 28 | Arguments: 29 | material_ids (str, List[str]): Search for equation of states associated with the specified Material IDs 30 | energies (Tuple[float,float]): Minimum and maximum energy in eV/atom to consider for EOS plot range. 31 | volumes (Tuple[float,float]): Minimum and maximum volume in A³/atom to consider for EOS plot range. 32 | num_chunks (int): Maximum number of chunks of data to yield. None will yield all possible. 33 | chunk_size (int): Number of data entries per chunk. 34 | all_fields (bool): Whether to return all fields in the document. Defaults to True. 35 | fields (List[str]): List of fields in EOSDoc to return data for. 36 | Default is material_id only if all_fields is False. 37 | 38 | Returns: 39 | ([EOSDoc], [dict]) List of equations of state docs or dictionaries. 40 | """ 41 | query_params = defaultdict(dict) # type: dict 42 | 43 | if material_ids: 44 | if isinstance(material_ids, str): 45 | material_ids = [material_ids] 46 | 47 | query_params.update({"material_ids": ",".join(validate_ids(material_ids))}) 48 | 49 | if volumes: 50 | query_params.update({"volumes_min": volumes[0], "volumes_max": volumes[1]}) 51 | 52 | if energies: 53 | query_params.update( 54 | {"energies_min": energies[0], "energies_max": energies[1]} 55 | ) 56 | 57 | query_params = { 58 | entry: query_params[entry] 59 | for entry in query_params 60 | if query_params[entry] is not None 61 | } 62 | 63 | return super()._search( 64 | num_chunks=num_chunks, 65 | chunk_size=chunk_size, 66 | all_fields=all_fields, 67 | fields=fields, 68 | **query_params, 69 | ) 70 | -------------------------------------------------------------------------------- /mp_api/client/routes/materials/grain_boundaries.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections import defaultdict 4 | 5 | from emmet.core.grain_boundary import GBTypeEnum, GrainBoundaryDoc 6 | 7 | from mp_api.client.core import BaseRester 8 | from mp_api.client.core.utils import validate_ids 9 | 10 | 11 | class GrainBoundaryRester(BaseRester[GrainBoundaryDoc]): 12 | suffix = "materials/grain_boundaries" 13 | document_model = GrainBoundaryDoc # type: ignore 14 | primary_key = "material_id" 15 | 16 | def search( 17 | self, 18 | material_ids: str | list[str] | None = None, 19 | chemsys: str | None = None, 20 | gb_plane: list[str] | None = None, 21 | gb_energy: tuple[float, float] | None = None, 22 | pretty_formula: str | None = None, 23 | rotation_axis: list[str] | None = None, 24 | rotation_angle: tuple[float, float] | None = None, 25 | separation_energy: tuple[float, float] | None = None, 26 | sigma: int | None = None, 27 | type: GBTypeEnum | None = None, 28 | num_chunks: int | None = None, 29 | chunk_size: int = 1000, 30 | all_fields: bool = True, 31 | fields: list[str] | None = None, 32 | ) -> list[GrainBoundaryDoc] | list[dict]: 33 | """Query grain boundary docs using a variety of search criteria. 34 | 35 | Arguments: 36 | material_ids (str, List[str]): Search for grain boundary data associated with the specified Material IDs 37 | chemsys (str): Dash-delimited string of elements in the material. 38 | gb_plane(List[str]): The Miller index of grain boundary plane. e.g., [1, 1, 1] 39 | gb_energy (Tuple[float,float]): Minimum and maximum grain boundary energy in J/m³ to consider. 40 | material_ids (List[str]): List of Materials Project IDs to query with. 41 | pretty_formula (str): Formula of the material. 42 | rotation_angle (Tuple[float,float]): Minimum and maximum rotation angle in degrees to consider. 43 | rotation_axis(List[str]): The Miller index of rotation axis. e.g., [1, 0, 0], [1, 1, 0], and [1, 1, 1] 44 | sigma (int): Sigma value of grain boundary. 45 | separation_energy (Tuple[float,float]): Minimum and maximum work of separation energy in J/m³ to consider. 46 | sigma (int): Sigma value of the boundary. 47 | type (GBTypeEnum): Grain boundary type. 48 | num_chunks (int): Maximum number of chunks of data to yield. None will yield all possible. 49 | chunk_size (int): Number of data entries per chunk. 50 | all_fields (bool): Whether to return all fields in the document. Defaults to True. 51 | fields (List[str]): List of fields in GrainBoundaryDoc to return data for. 52 | Default is material_id and last_updated if all_fields is False. 53 | 54 | Returns: 55 | ([GrainBoundaryDoc], [dict]) List of grain boundary documents or dictionaries. 56 | """ 57 | query_params = defaultdict(dict) # type: dict 58 | 59 | if material_ids: 60 | if isinstance(material_ids, str): 61 | material_ids = [material_ids] 62 | 63 | query_params.update({"material_ids": ",".join(validate_ids(material_ids))}) 64 | 65 | if gb_plane: 66 | query_params.update({"gb_plane": ",".join([str(n) for n in gb_plane])}) 67 | 68 | if gb_energy: 69 | query_params.update( 70 | {"gb_energy_min": gb_energy[0], "gb_energy_max": gb_energy[1]} 71 | ) 72 | 73 | if separation_energy: 74 | query_params.update( 75 | {"w_sep_min": separation_energy[0], "w_sep_max": separation_energy[1]} 76 | ) 77 | 78 | if rotation_angle: 79 | query_params.update( 80 | { 81 | "rotation_angle_min": rotation_angle[0], 82 | "rotation_angle_max": rotation_angle[1], 83 | } 84 | ) 85 | 86 | if rotation_axis: 87 | query_params.update( 88 | {"rotation_axis": ",".join([str(n) for n in rotation_axis])} 89 | ) 90 | 91 | if sigma: 92 | query_params.update({"sigma": sigma}) 93 | 94 | if type: 95 | query_params.update({"type": type.value}) 96 | 97 | if chemsys: 98 | query_params.update({"chemsys": chemsys}) 99 | 100 | if pretty_formula: 101 | query_params.update({"pretty_formula": pretty_formula}) 102 | 103 | query_params = { 104 | entry: query_params[entry] 105 | for entry in query_params 106 | if query_params[entry] is not None 107 | } 108 | 109 | return super()._search( 110 | num_chunks=num_chunks, 111 | chunk_size=chunk_size, 112 | all_fields=all_fields, 113 | fields=fields, 114 | **query_params, 115 | ) 116 | -------------------------------------------------------------------------------- /mp_api/client/routes/materials/magnetism.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections import defaultdict 4 | 5 | from emmet.core.magnetism import MagnetismDoc 6 | from pymatgen.analysis.magnetism import Ordering 7 | 8 | from mp_api.client.core import BaseRester 9 | from mp_api.client.core.utils import validate_ids 10 | 11 | 12 | class MagnetismRester(BaseRester[MagnetismDoc]): 13 | suffix = "materials/magnetism" 14 | document_model = MagnetismDoc # type: ignore 15 | primary_key = "material_id" 16 | 17 | def search( 18 | self, 19 | material_ids: str | list[str] | None = None, 20 | num_magnetic_sites: tuple[int, int] | None = None, 21 | num_unique_magnetic_sites: tuple[int, int] | None = None, 22 | ordering: Ordering | None = None, 23 | total_magnetization: tuple[float, float] | None = None, 24 | total_magnetization_normalized_vol: tuple[float, float] | None = None, 25 | total_magnetization_normalized_formula_units: tuple[float, float] | None = None, 26 | num_chunks: int | None = None, 27 | chunk_size: int = 1000, 28 | all_fields: bool = True, 29 | fields: list[str] | None = None, 30 | ) -> list[MagnetismDoc] | list[dict]: 31 | """Query magnetism docs using a variety of search criteria. 32 | 33 | Arguments: 34 | material_ids (str, List[str]): A single Material ID string or list of strings (e.g., mp-149, [mp-149, mp-13]). 35 | num_magnetic_sites (Tuple[int,int]): Minimum and maximum number of magnetic sites to consider. 36 | num_unique_magnetic_sites (Tuple[int,int]): Minimum and maximum number of unique magnetic sites 37 | to consider. 38 | ordering (Ordering]): The magnetic ordering of the material. 39 | total_magnetization (Tuple[float,float]): Minimum and maximum total magnetization values to consider. 40 | total_magnetization_normalized_vol (Tuple[float,float]): Minimum and maximum total magnetization values 41 | normalized by volume to consider. 42 | total_magnetization_normalized_formula_units (Tuple[float,float]): Minimum and maximum total magnetization 43 | values normalized by formula units to consider. 44 | num_chunks (int): Maximum number of chunks of data to yield. None will yield all possible. 45 | chunk_size (int): Number of data entries per chunk. 46 | all_fields (bool): Whether to return all fields in the document. Defaults to True. 47 | fields (List[str]): List of fields in MagnetismDoc to return data for. 48 | Default is material_id and last_updated if all_fields is False. 49 | 50 | Returns: 51 | ([MagnetismDoc], [dict]) List of magnetism documents or dictionaries. 52 | """ 53 | query_params = defaultdict(dict) # type: dict 54 | 55 | if material_ids: 56 | if isinstance(material_ids, str): 57 | material_ids = [material_ids] 58 | 59 | query_params.update({"material_ids": ",".join(validate_ids(material_ids))}) 60 | 61 | if total_magnetization: 62 | query_params.update( 63 | { 64 | "total_magnetization_min": total_magnetization[0], 65 | "total_magnetization_max": total_magnetization[1], 66 | } 67 | ) 68 | 69 | if total_magnetization_normalized_vol: 70 | query_params.update( 71 | { 72 | "total_magnetization_normalized_vol_min": total_magnetization_normalized_vol[ 73 | 0 74 | ], 75 | "total_magnetization_normalized_vol_max": total_magnetization_normalized_vol[ 76 | 1 77 | ], 78 | } 79 | ) 80 | 81 | if total_magnetization_normalized_formula_units: 82 | query_params.update( 83 | { 84 | "total_magnetization_normalized_formula_units_min": total_magnetization_normalized_formula_units[ 85 | 0 86 | ], 87 | "total_magnetization_normalized_formula_units_max": total_magnetization_normalized_formula_units[ 88 | 1 89 | ], 90 | } 91 | ) 92 | 93 | if num_magnetic_sites: 94 | query_params.update( 95 | { 96 | "num_magnetic_sites_min": num_magnetic_sites[0], 97 | "num_magnetic_sites_max": num_magnetic_sites[1], 98 | } 99 | ) 100 | 101 | if num_unique_magnetic_sites: 102 | query_params.update( 103 | { 104 | "num_unique_magnetic_sites_min": num_unique_magnetic_sites[0], 105 | "num_unique_magnetic_sites_max": num_unique_magnetic_sites[1], 106 | } 107 | ) 108 | 109 | if ordering: 110 | query_params.update({"ordering": ordering.value}) 111 | 112 | query_params = { 113 | entry: query_params[entry] 114 | for entry in query_params 115 | if query_params[entry] is not None 116 | } 117 | 118 | return super()._search( 119 | num_chunks=num_chunks, 120 | chunk_size=chunk_size, 121 | all_fields=all_fields, 122 | fields=fields, 123 | **query_params, 124 | ) 125 | -------------------------------------------------------------------------------- /mp_api/client/routes/materials/oxidation_states.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections import defaultdict 4 | 5 | from emmet.core.oxidation_states import OxidationStateDoc 6 | 7 | from mp_api.client.core import BaseRester 8 | from mp_api.client.core.utils import validate_ids 9 | 10 | 11 | class OxidationStatesRester(BaseRester[OxidationStateDoc]): 12 | suffix = "materials/oxidation_states" 13 | document_model = OxidationStateDoc # type: ignore 14 | primary_key = "material_id" 15 | 16 | def search( 17 | self, 18 | material_ids: str | list[str] | None = None, 19 | chemsys: str | list[str] | None = None, 20 | formula: str | list[str] | None = None, 21 | possible_species: str | list[str] | None = None, 22 | num_chunks: int | None = None, 23 | chunk_size: int = 1000, 24 | all_fields: bool = True, 25 | fields: list[str] | None = None, 26 | ) -> list[OxidationStateDoc] | list[dict]: 27 | """Query oxidation state docs using a variety of search criteria. 28 | 29 | Arguments: 30 | material_ids (str, List[str]): A single Material ID string or list of strings 31 | (e.g., mp-149, [mp-149, mp-13]). 32 | chemsys (str, List[str]): A chemical system or list of chemical systems 33 | (e.g., Li-Fe-O, Si-*, [Si-O, Li-Fe-P]). 34 | formula (str, List[str]): A formula including anonymized formula 35 | or wild cards (e.g., Fe2O3, ABO3, Si*). A list of chemical formulas can also be passed 36 | (e.g., [Fe2O3, ABO3]). 37 | possible_species (List[str]): A list of element symbols appended with oxidation states (e.g. [Cr2+, O2-]]). 38 | num_chunks (int): Maximum number of chunks of data to yield. None will yield all possible. 39 | chunk_size (int): Number of data entries per chunk. 40 | all_fields (bool): Whether to return all fields in the document. Defaults to True. 41 | fields (List[str]): List of fields in OxidationStateDoc to return data for. 42 | Default is material_id, last_updated, and formula_pretty if all_fields is False. 43 | 44 | Returns: 45 | ([OxidationStateDoc], [dict]) List of oxidation state documents or dictionaries. 46 | """ 47 | query_params = defaultdict(dict) # type: dict 48 | 49 | if material_ids: 50 | if isinstance(material_ids, str): 51 | material_ids = [material_ids] 52 | 53 | query_params.update({"material_ids": ",".join(validate_ids(material_ids))}) 54 | 55 | if formula: 56 | if isinstance(formula, str): 57 | formula = [formula] 58 | 59 | query_params.update({"formula": ",".join(formula)}) 60 | 61 | if chemsys: 62 | if isinstance(chemsys, str): 63 | chemsys = [chemsys] 64 | 65 | query_params.update({"chemsys": ",".join(chemsys)}) 66 | 67 | if possible_species: 68 | query_params.update({"possible_species": ",".join(possible_species)}) 69 | 70 | query_params = { 71 | entry: query_params[entry] 72 | for entry in query_params 73 | if query_params[entry] is not None 74 | } 75 | 76 | return super()._search( 77 | num_chunks=num_chunks, 78 | chunk_size=chunk_size, 79 | all_fields=all_fields, 80 | fields=fields, 81 | **query_params, 82 | ) 83 | -------------------------------------------------------------------------------- /mp_api/client/routes/materials/piezo.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections import defaultdict 4 | 5 | from emmet.core.polar import PiezoelectricDoc 6 | 7 | from mp_api.client.core import BaseRester 8 | from mp_api.client.core.utils import validate_ids 9 | 10 | 11 | class PiezoRester(BaseRester[PiezoelectricDoc]): 12 | suffix = "materials/piezoelectric" 13 | document_model = PiezoelectricDoc # type: ignore 14 | primary_key = "material_id" 15 | 16 | def search( 17 | self, 18 | material_ids: str | list[str] | None = None, 19 | piezoelectric_modulus: tuple[float, float] | None = None, 20 | num_chunks: int | None = None, 21 | chunk_size: int = 1000, 22 | all_fields: bool = True, 23 | fields: list[str] | None = None, 24 | ) -> list[PiezoelectricDoc] | list[dict]: 25 | """Query piezoelectric data using a variety of search criteria. 26 | 27 | Arguments: 28 | material_ids (str, List[str]): A single Material ID string or list of strings 29 | (e.g., mp-149, [mp-149, mp-13]). 30 | piezoelectric_modulus (Tuple[float,float]): Minimum and maximum of the 31 | piezoelectric modulus in C/m² to consider. 32 | num_chunks (int): Maximum number of chunks of data to yield. None will yield all possible. 33 | chunk_size (int): Number of data entries per chunk. 34 | all_fields (bool): Whether to return all fields in the document. Defaults to True. 35 | fields (List[str]): List of fields in PiezoDoc to return data for. 36 | Default is material_id and last_updated if all_fields is False. 37 | 38 | Returns: 39 | ([PiezoDoc], [dict]) List of piezoelectric documents 40 | """ 41 | query_params = defaultdict(dict) # type: dict 42 | 43 | if material_ids: 44 | if isinstance(material_ids, str): 45 | material_ids = [material_ids] 46 | 47 | query_params.update({"material_ids": ",".join(validate_ids(material_ids))}) 48 | 49 | if piezoelectric_modulus: 50 | query_params.update( 51 | { 52 | "piezo_modulus_min": piezoelectric_modulus[0], 53 | "piezo_modulus_max": piezoelectric_modulus[1], 54 | } 55 | ) 56 | 57 | query_params = { 58 | entry: query_params[entry] 59 | for entry in query_params 60 | if query_params[entry] is not None 61 | } 62 | 63 | return super()._search( 64 | num_chunks=num_chunks, 65 | chunk_size=chunk_size, 66 | all_fields=all_fields, 67 | fields=fields, 68 | **query_params, 69 | ) 70 | -------------------------------------------------------------------------------- /mp_api/client/routes/materials/provenance.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from emmet.core.provenance import ProvenanceDoc 4 | 5 | from mp_api.client.core import BaseRester 6 | from mp_api.client.core.utils import validate_ids 7 | 8 | 9 | class ProvenanceRester(BaseRester[ProvenanceDoc]): 10 | suffix = "materials/provenance" 11 | document_model = ProvenanceDoc # type: ignore 12 | primary_key = "material_id" 13 | 14 | def search( 15 | self, 16 | material_ids: str | list[str] | None = None, 17 | deprecated: bool | None = False, 18 | num_chunks: int | None = None, 19 | chunk_size: int = 1000, 20 | all_fields: bool = True, 21 | fields: list[str] | None = None, 22 | ) -> list[ProvenanceDoc] | list[dict]: 23 | """Query provenance docs using a variety of search criteria. 24 | 25 | Arguments: 26 | material_ids (str, List[str]): A single Material ID string or list of strings 27 | (e.g., mp-149, [mp-149, mp-13]). 28 | deprecated (bool): Whether the material is tagged as deprecated. 29 | num_chunks (int): Maximum number of chunks of data to yield. None will yield all possible. 30 | chunk_size (int): Number of data entries per chunk. 31 | all_fields (bool): Whether to return all fields in the document. Defaults to True. 32 | fields (List[str]): List of fields in ProvenanceDoc to return data for. 33 | Default is material_id, last_updated, and formula_pretty if all_fields is False. 34 | 35 | Returns: 36 | ([ProvenanceDoc], [dict]) List of provenance documents or dictionaries. 37 | """ 38 | query_params = {"deprecated": deprecated} # type: dict 39 | 40 | if material_ids: 41 | if isinstance(material_ids, str): 42 | material_ids = [material_ids] 43 | 44 | query_params.update({"material_ids": ",".join(validate_ids(material_ids))}) 45 | 46 | query_params = { 47 | entry: query_params[entry] 48 | for entry in query_params 49 | if query_params[entry] is not None 50 | } 51 | return super()._search( 52 | num_chunks=num_chunks, 53 | chunk_size=chunk_size, 54 | all_fields=all_fields, 55 | fields=fields, 56 | **query_params, 57 | ) 58 | -------------------------------------------------------------------------------- /mp_api/client/routes/materials/robocrys.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from emmet.core.robocrys import RobocrystallogapherDoc 4 | 5 | from mp_api.client.core import BaseRester, MPRestError 6 | from mp_api.client.core.utils import validate_ids 7 | 8 | 9 | class RobocrysRester(BaseRester[RobocrystallogapherDoc]): 10 | suffix = "materials/robocrys" 11 | document_model = RobocrystallogapherDoc # type: ignore 12 | primary_key = "material_id" 13 | 14 | def search( 15 | self, 16 | keywords: list[str], 17 | num_chunks: int | None = None, 18 | chunk_size: int | None = 100, 19 | ): 20 | """Search text generated from Robocrystallographer. 21 | 22 | Arguments: 23 | keywords (List[str]): List of search keywords 24 | num_chunks (Optional[int]): Maximum number of chunks of data to yield. None will yield all possible. 25 | chunk_size (Optional[int]): Number of data entries per chunk. 26 | 27 | Returns: 28 | robocrys_docs (List[RobocrystallogapherDoc]): List of robocrystallographer documents 29 | """ 30 | keyword_string = ",".join(keywords) 31 | 32 | robocrys_docs = self._query_resource( 33 | criteria={"keywords": keyword_string, "_limit": chunk_size}, 34 | suburl="text_search", 35 | use_document_model=True, 36 | chunk_size=chunk_size, 37 | num_chunks=num_chunks, 38 | ).get("data", None) 39 | 40 | if robocrys_docs is None: 41 | raise MPRestError("Cannot find any matches.") 42 | 43 | return robocrys_docs 44 | 45 | def search_docs( 46 | self, 47 | material_ids: str | list[str] | None = None, 48 | num_chunks: int | None = None, 49 | chunk_size: int = 1000, 50 | all_fields: bool = True, 51 | fields: list[str] | None = None, 52 | ) -> list[RobocrystallogapherDoc] | list[dict]: 53 | """Query robocrystallographer docs using a variety of search criteria. 54 | 55 | Arguments: 56 | material_ids (str, List[str]): A single Material ID string or list of strings 57 | (e.g., mp-149, [mp-149, mp-13]). 58 | num_chunks (int): Maximum number of chunks of data to yield. None will yield all possible. 59 | chunk_size (int): Number of data entries per chunk. 60 | all_fields (bool): Whether to return all fields in the document. Defaults to True. 61 | fields (List[str]): List of fields in RobocrystallogapherDoc to return data for. 62 | Default is material_id, last_updated, and formula_pretty if all_fields is False. 63 | 64 | Returns: 65 | ([RobocrystallogapherDoc], [dict]) List of robocrystallographer documents or dictionaries. 66 | """ 67 | query_params = {} # type: dict 68 | 69 | if material_ids: 70 | if isinstance(material_ids, str): 71 | material_ids = [material_ids] 72 | 73 | query_params.update({"material_ids": ",".join(validate_ids(material_ids))}) 74 | 75 | query_params = { 76 | entry: query_params[entry] 77 | for entry in query_params 78 | if query_params[entry] is not None 79 | } 80 | return super()._search( 81 | num_chunks=num_chunks, 82 | chunk_size=chunk_size, 83 | all_fields=all_fields, 84 | fields=fields, 85 | **query_params, 86 | ) 87 | -------------------------------------------------------------------------------- /mp_api/client/routes/materials/similarity.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from emmet.core.similarity import SimilarityDoc 4 | 5 | from mp_api.client.core import BaseRester 6 | from mp_api.client.core.utils import validate_ids 7 | 8 | 9 | class SimilarityRester(BaseRester[SimilarityDoc]): 10 | suffix = "materials/similarity" 11 | document_model = SimilarityDoc # type: ignore 12 | primary_key = "material_id" 13 | 14 | def search( 15 | self, 16 | material_ids: str | list[str] | None = None, 17 | num_chunks: int | None = None, 18 | chunk_size: int = 1000, 19 | all_fields: bool = True, 20 | fields: list[str] | None = None, 21 | ) -> list[SimilarityDoc] | list[dict]: 22 | """Query similarity docs using a variety of search criteria. 23 | 24 | Arguments: 25 | material_ids (str, List[str]): A single Material ID string or list of strings 26 | (e.g., mp-149, [mp-149, mp-13]). 27 | num_chunks (int): Maximum number of chunks of data to yield. None will yield all possible. 28 | chunk_size (int): Number of data entries per chunk. 29 | all_fields (bool): Whether to return all fields in the document. Defaults to True. 30 | fields (List[str]): List of fields in SimilarityDoc to return data for. 31 | Default is material_id, last_updated, and formula_pretty if all_fields is False. 32 | 33 | Returns: 34 | ([SimilarityDoc], [dict]) List of similarity documents or dictionaries. 35 | """ 36 | query_params = {} # type: dict 37 | 38 | if material_ids: 39 | if isinstance(material_ids, str): 40 | material_ids = [material_ids] 41 | 42 | query_params.update({"material_ids": ",".join(validate_ids(material_ids))}) 43 | 44 | query_params = { 45 | entry: query_params[entry] 46 | for entry in query_params 47 | if query_params[entry] is not None 48 | } 49 | return super()._search( 50 | num_chunks=num_chunks, 51 | chunk_size=chunk_size, 52 | all_fields=all_fields, 53 | fields=fields, 54 | **query_params, 55 | ) 56 | -------------------------------------------------------------------------------- /mp_api/client/routes/materials/substrates.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections import defaultdict 4 | 5 | from emmet.core.substrates import SubstratesDoc 6 | 7 | from mp_api.client.core import BaseRester 8 | 9 | 10 | class SubstratesRester(BaseRester[SubstratesDoc]): 11 | suffix = "materials/substrates" 12 | document_model = SubstratesDoc # type: ignore 13 | primary_key = "film_id" 14 | 15 | def search( 16 | self, 17 | area: tuple[float, float] | None = None, 18 | energy: tuple[float, float] | None = None, 19 | film_id: str | None = None, 20 | film_orientation: list[int] | None = None, 21 | substrate_id: str | None = None, 22 | substrate_formula: str | None = None, 23 | substrate_orientation: list[int] | None = None, 24 | num_chunks: int | None = None, 25 | chunk_size: int = 1000, 26 | all_fields: bool = True, 27 | fields: list[str] | None = None, 28 | ) -> list[SubstratesDoc] | list[dict]: 29 | """Query substrate docs using a variety of search criteria. 30 | 31 | Arguments: 32 | area (Tuple[float,float]): Minimum and maximum volume in Ų to consider for the minimum coincident 33 | interface area range. 34 | energy (Tuple[float,float]): Minimum and maximum energy in meV to consider for the elastic energy range. 35 | film_id (str): Materials Project ID of the film material. 36 | film_orientation (List[int]): Vector indicating the surface orientation of the film material. 37 | substrate_id (str): Materials Project ID of the substrate material. 38 | substrate_formula (str): Reduced formula of the substrate material. 39 | substrate_orientation (List[int]): Vector indicating the surface orientation of the substrate material. 40 | num_chunks (int): Maximum number of chunks of data to yield. None will yield all possible. 41 | chunk_size (int): Number of data entries per chunk. 42 | all_fields (bool): Whether to return all fields in the document. Defaults to True. 43 | fields (List[str]): List of fields in SubstratesDoc to return data for. 44 | Default is the film_id and substrate_id only if all_fields is False. 45 | 46 | Returns: 47 | ([SubstratesDoc], [dict]) List of substrate documents or dictionaries. 48 | """ 49 | query_params = defaultdict(dict) # type: dict 50 | 51 | if film_id: 52 | query_params.update({"film_id": film_id}) 53 | 54 | if substrate_id: 55 | query_params.update({"sub_id": substrate_id}) 56 | 57 | if substrate_formula: 58 | query_params.update({"sub_form": substrate_formula}) 59 | 60 | if film_orientation: 61 | query_params.update( 62 | {"film_orientation": ",".join([str(i) for i in film_orientation])} 63 | ) 64 | 65 | if substrate_orientation: 66 | query_params.update( 67 | { 68 | "substrate_orientation": ",".join( 69 | [str(i) for i in substrate_orientation] 70 | ) 71 | } 72 | ) 73 | 74 | if area: 75 | query_params.update({"area_min": area[0], "area_max": area[1]}) 76 | 77 | if energy: 78 | query_params.update({"energy_min": energy[0], "energy_max": energy[1]}) 79 | 80 | query_params = { 81 | entry: query_params[entry] 82 | for entry in query_params 83 | if query_params[entry] is not None 84 | } 85 | 86 | return super()._search( 87 | **query_params, 88 | num_chunks=num_chunks, 89 | chunk_size=chunk_size, 90 | all_fields=all_fields, 91 | fields=fields, 92 | ) 93 | -------------------------------------------------------------------------------- /mp_api/client/routes/materials/surface_properties.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections import defaultdict 4 | 5 | from emmet.core.surface_properties import SurfacePropDoc 6 | 7 | from mp_api.client.core import BaseRester 8 | from mp_api.client.core.utils import validate_ids 9 | 10 | 11 | class SurfacePropertiesRester(BaseRester[SurfacePropDoc]): 12 | suffix = "materials/surface_properties" 13 | document_model = SurfacePropDoc # type: ignore 14 | primary_key = "material_id" 15 | 16 | def search( 17 | self, 18 | material_ids: str | list[str] | None = None, 19 | has_reconstructed: bool | None = None, 20 | shape_factor: tuple[float, float] | None = None, 21 | surface_energy_anisotropy: tuple[float, float] | None = None, 22 | weighted_surface_energy: tuple[float, float] | None = None, 23 | weighted_work_function: tuple[float, float] | None = None, 24 | num_chunks: int | None = None, 25 | chunk_size: int = 1000, 26 | all_fields: bool = True, 27 | fields: list[str] | None = None, 28 | ) -> list[SurfacePropDoc] | list[dict]: 29 | """Query surface properties docs using a variety of search criteria. 30 | 31 | Arguments: 32 | material_ids (str, List[str]): A single Material ID string or list of strings 33 | (e.g., mp-149, [mp-149, mp-13]). 34 | has_reconstructed (bool): Whether the entry has any reconstructed surfaces. 35 | shape_factor (Tuple[float,float]): Minimum and maximum shape factor values to consider. 36 | surface_energy_anisotropy (Tuple[float,float]): Minimum and maximum surface energy anisotropy values to 37 | consider. 38 | weighted_surface_energy (Tuple[float,float]): Minimum and maximum weighted surface energy in J/m² to 39 | consider. 40 | weighted_work_function (Tuple[float,float]): Minimum and maximum weighted work function in eV to consider. 41 | num_chunks (int): Maximum number of chunks of data to yield. None will yield all possible. 42 | chunk_size (int): Number of data entries per chunk. 43 | all_fields (bool): Whether to return all fields in the document. Defaults to True. 44 | fields (List[str]): List of fields in SurfacePropDoc to return data for. 45 | Default is material_id only if all_fields is False. 46 | 47 | Returns: 48 | ([SurfacePropDoc], [dict]) List of surface properties documents 49 | """ 50 | query_params = defaultdict(dict) # type: dict 51 | 52 | if material_ids: 53 | if isinstance(material_ids, str): 54 | material_ids = [material_ids] 55 | 56 | query_params.update({"material_ids": ",".join(validate_ids(material_ids))}) 57 | 58 | if weighted_surface_energy: 59 | query_params.update( 60 | { 61 | "weighted_surface_energy_min": weighted_surface_energy[0], 62 | "weighted_surface_energy_max": weighted_surface_energy[1], 63 | } 64 | ) 65 | 66 | if weighted_work_function: 67 | query_params.update( 68 | { 69 | "weighted_work_function_min": weighted_work_function[0], 70 | "weighted_work_function_max": weighted_work_function[1], 71 | } 72 | ) 73 | 74 | if surface_energy_anisotropy: 75 | query_params.update( 76 | { 77 | "surface_anisotropy_min": surface_energy_anisotropy[0], 78 | "surface_anisotropy_max": surface_energy_anisotropy[1], 79 | } 80 | ) 81 | 82 | if shape_factor: 83 | query_params.update( 84 | { 85 | "shape_factor_min": shape_factor[0], 86 | "shape_factor_max": shape_factor[1], 87 | } 88 | ) 89 | 90 | if has_reconstructed is not None: 91 | query_params.update({"has_reconstructed": has_reconstructed}) 92 | 93 | query_params = { 94 | entry: query_params[entry] 95 | for entry in query_params 96 | if query_params[entry] is not None 97 | } 98 | 99 | return super()._search( 100 | num_chunks=num_chunks, 101 | chunk_size=chunk_size, 102 | all_fields=all_fields, 103 | fields=fields, 104 | **query_params, 105 | ) 106 | -------------------------------------------------------------------------------- /mp_api/client/routes/materials/synthesis.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from emmet.core.synthesis import ( 4 | OperationTypeEnum, 5 | SynthesisSearchResultModel, 6 | SynthesisTypeEnum, 7 | ) 8 | 9 | from mp_api.client.core import BaseRester, MPRestError 10 | 11 | 12 | class SynthesisRester(BaseRester[SynthesisSearchResultModel]): 13 | suffix = "materials/synthesis" 14 | document_model = SynthesisSearchResultModel # type: ignore 15 | 16 | def search( 17 | self, 18 | keywords: list[str] | None = None, 19 | synthesis_type: list[SynthesisTypeEnum] | None = None, 20 | target_formula: str | None = None, 21 | precursor_formula: str | None = None, 22 | operations: list[OperationTypeEnum] | None = None, 23 | condition_heating_temperature_min: float | None = None, 24 | condition_heating_temperature_max: float | None = None, 25 | condition_heating_time_min: float | None = None, 26 | condition_heating_time_max: float | None = None, 27 | condition_heating_atmosphere: list[str] | None = None, 28 | condition_mixing_device: list[str] | None = None, 29 | condition_mixing_media: list[str] | None = None, 30 | num_chunks: int | None = None, 31 | chunk_size: int | None = 10, 32 | ) -> list[SynthesisSearchResultModel] | list[dict]: 33 | """Search synthesis recipe text. 34 | 35 | Arguments: 36 | keywords (Optional[List[str]]): List of string keywords to search synthesis paragraph text with 37 | synthesis_type (Optional[List[SynthesisTypeEnum]]): Type of synthesis to include 38 | target_formula (Optional[str]): Chemical formula of the target material 39 | precursor_formula (Optional[str]): Chemical formula of the precursor material 40 | operations (Optional[List[OperationTypeEnum]]): List of operations that syntheses must have 41 | condition_heating_temperature_min (Optional[float]): Minimal heating temperature 42 | condition_heating_temperature_max (Optional[float]): Maximal heating temperature 43 | condition_heating_time_min (Optional[float]): Minimal heating time 44 | condition_heating_time_max (Optional[float]): Maximal heating time 45 | condition_heating_atmosphere (Optional[List[str]]): Required heating atmosphere, such as "air", "argon" 46 | condition_mixing_device (Optional[List[str]]): Required mixing device, such as "zirconia", "Al2O3". 47 | condition_mixing_media (Optional[List[str]]): Required mixing media, such as "alcohol", "water" 48 | num_chunks (Optional[int]): Maximum number of chunks of data to yield. None will yield all possible. 49 | chunk_size (Optional[int]): Number of data entries per chunk. 50 | 51 | 52 | Returns: 53 | ([SynthesisSearchResultModel], [dict]): List of synthesis documents or dictionaries. 54 | """ 55 | # Turn None and empty list into None 56 | keywords = keywords or None 57 | synthesis_type = synthesis_type or None 58 | operations = operations or None 59 | condition_heating_atmosphere = condition_heating_atmosphere or None 60 | condition_mixing_device = condition_mixing_device or None 61 | condition_mixing_media = condition_mixing_media or None 62 | 63 | if keywords: 64 | keywords = ",".join([word.strip() for word in keywords]) # type: ignore 65 | 66 | synthesis_docs = self._query_resource( 67 | criteria={ 68 | "keywords": keywords, 69 | "synthesis_type": synthesis_type, 70 | "target_formula": target_formula, 71 | "precursor_formula": precursor_formula, 72 | "operations": operations, 73 | "condition_heating_temperature_min": condition_heating_temperature_min, 74 | "condition_heating_temperature_max": condition_heating_temperature_max, 75 | "condition_heating_time_min": condition_heating_time_min, 76 | "condition_heating_time_max": condition_heating_time_max, 77 | "condition_heating_atmosphere": condition_heating_atmosphere, 78 | "condition_mixing_device": condition_mixing_device, 79 | "condition_mixing_media": condition_mixing_media, 80 | "_limit": chunk_size, 81 | }, 82 | chunk_size=chunk_size, 83 | num_chunks=num_chunks, 84 | ).get("data", None) 85 | 86 | if synthesis_docs is None: 87 | raise MPRestError("Cannot find any matches.") 88 | 89 | return synthesis_docs 90 | -------------------------------------------------------------------------------- /mp_api/client/routes/materials/tasks.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from datetime import datetime 4 | 5 | from emmet.core.tasks import TaskDoc 6 | 7 | from mp_api.client.core import BaseRester, MPRestError 8 | from mp_api.client.core.utils import validate_ids 9 | 10 | 11 | class TaskRester(BaseRester[TaskDoc]): 12 | suffix = "materials/tasks" 13 | document_model = TaskDoc # type: ignore 14 | primary_key = "task_id" 15 | 16 | def get_trajectory(self, task_id): 17 | """Returns a Trajectory object containing the geometry of the 18 | material throughout a calculation. This is most useful for 19 | observing how a material relaxes during a geometry optimization. 20 | 21 | Args: 22 | task_id (str): Task ID 23 | 24 | """ 25 | traj_data = self._query_resource_data( 26 | {"task_ids": [task_id]}, suburl="trajectory/", use_document_model=False 27 | )[0].get( 28 | "trajectories", None 29 | ) # type: ignore 30 | 31 | if traj_data is None: 32 | raise MPRestError(f"No trajectory data for {task_id} found") 33 | 34 | return traj_data 35 | 36 | def search( 37 | self, 38 | task_ids: str | list[str] | None = None, 39 | elements: list[str] | None = None, 40 | exclude_elements: list[str] | None = None, 41 | formula: str | list[str] | None = None, 42 | last_updated: tuple[datetime, datetime] | None = None, 43 | num_chunks: int | None = None, 44 | chunk_size: int = 1000, 45 | all_fields: bool = True, 46 | fields: list[str] | None = None, 47 | ) -> list[TaskDoc] | list[dict]: 48 | """Query core task docs using a variety of search criteria. 49 | 50 | Arguments: 51 | task_ids (str, List[str]): List of Materials Project IDs to return data for. 52 | elements (List[str]): A list of elements. 53 | exclude_elements (List[str]): A list of elements to exclude. 54 | formula (str, List[str]): A formula including anonymized formula 55 | or wild cards (e.g., Fe2O3, ABO3, Si*). A list of chemical formulas can also be passed 56 | (e.g., [Fe2O3, ABO3]). 57 | last_updated (tuple[datetime, datetime]): A tuple of min and max UTC formatted datetimes. 58 | num_chunks (int): Maximum number of chunks of data to yield. None will yield all possible. 59 | chunk_size (int): Number of data entries per chunk. Max size is 100. 60 | all_fields (bool): Whether to return all fields in the document. Defaults to True. 61 | fields (List[str]): List of fields in TaskDoc to return data for. 62 | Default is material_id, last_updated, and formula_pretty if all_fields is False. 63 | 64 | Returns: 65 | ([TaskDoc], [dict]) List of task documents or dictionaries. 66 | """ 67 | query_params = {} # type: dict 68 | 69 | if task_ids: 70 | if isinstance(task_ids, str): 71 | task_ids = [task_ids] 72 | 73 | query_params.update({"task_ids": ",".join(validate_ids(task_ids))}) 74 | 75 | if formula: 76 | query_params.update({"formula": formula}) 77 | 78 | if elements: 79 | query_params.update({"elements": ",".join(elements)}) 80 | 81 | if exclude_elements: 82 | query_params.update({"exclude_elements": ",".join(exclude_elements)}) 83 | 84 | if last_updated: 85 | query_params.update( 86 | { 87 | "last_updated_min": last_updated[0], 88 | "last_updated_max": last_updated[1], 89 | } 90 | ) 91 | 92 | return super()._search( 93 | num_chunks=num_chunks, 94 | chunk_size=chunk_size, 95 | all_fields=all_fields, 96 | fields=fields, 97 | **query_params, 98 | ) 99 | -------------------------------------------------------------------------------- /mp_api/client/routes/materials/xas.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from emmet.core.xas import Edge, Type, XASDoc 4 | from pymatgen.core.periodic_table import Element 5 | 6 | from mp_api.client.core import BaseRester 7 | from mp_api.client.core.utils import validate_ids 8 | 9 | 10 | class XASRester(BaseRester[XASDoc]): 11 | suffix = "materials/xas" 12 | document_model = XASDoc # type: ignore 13 | primary_key = "spectrum_id" 14 | 15 | def search( 16 | self, 17 | edge: Edge | None = None, 18 | absorbing_element: Element | None = None, 19 | formula: str | None = None, 20 | chemsys: str | list[str] | None = None, 21 | elements: list[str] | None = None, 22 | material_ids: list[str] | None = None, 23 | spectrum_type: Type | None = None, 24 | spectrum_ids: str | list[str] | None = None, 25 | num_chunks: int | None = None, 26 | chunk_size: int = 1000, 27 | all_fields: bool = True, 28 | fields: list[str] | None = None, 29 | ): 30 | """Query core XAS docs using a variety of search criteria. 31 | 32 | Arguments: 33 | edge (Edge): The absorption edge (e.g. K, L2, L3, L2,3). 34 | absorbing_element (Element): The absorbing element. 35 | formula (str): A formula including anonymized formula 36 | or wild cards (e.g., Fe2O3, ABO3, Si*). 37 | chemsys (str, List[str]): A chemical system or list of chemical systems 38 | (e.g., Li-Fe-O, Si-*, [Si-O, Li-Fe-P]). 39 | elements (List[str]): A list of elements. 40 | material_ids (str, List[str]): A single Material ID string or list of strings 41 | (e.g., mp-149, [mp-149, mp-13]). 42 | spectrum_type (Type): Spectrum type (e.g. EXAFS, XAFS, or XANES). 43 | spectrum_ids (str, List[str]): A single Spectrum ID string or list of strings 44 | (e.g., mp-149-XANES-Li-K, [mp-149-XANES-Li-K, mp-13-XANES-Li-K]). 45 | num_chunks (int): Maximum number of chunks of data to yield. None will yield all possible. 46 | chunk_size (int): Number of data entries per chunk. 47 | all_fields (bool): Whether to return all fields in the document. Defaults to True. 48 | fields (List[str]): List of fields in MaterialsCoreDoc to return data for. 49 | Default is material_id, last_updated, and formula_pretty if all_fields is False. 50 | 51 | Returns: 52 | ([MaterialsDoc]) List of material documents 53 | """ 54 | query_params = {} 55 | 56 | if edge: 57 | query_params.update({"edge": edge}) 58 | 59 | if absorbing_element: 60 | query_params.update( 61 | { 62 | "absorbing_element": str(absorbing_element.symbol) 63 | if type(absorbing_element) == Element 64 | else absorbing_element 65 | } 66 | ) 67 | 68 | if spectrum_type: 69 | query_params.update({"spectrum_type": spectrum_type}) 70 | 71 | if formula: 72 | query_params.update({"formula": formula}) 73 | 74 | if chemsys: 75 | if isinstance(chemsys, str): 76 | chemsys = [chemsys] 77 | 78 | query_params.update({"chemsys": ",".join(chemsys)}) 79 | 80 | if elements: 81 | query_params.update({"elements": ",".join(elements)}) 82 | 83 | if material_ids: 84 | if isinstance(material_ids, str): 85 | material_ids = [material_ids] 86 | 87 | query_params.update({"material_ids": ",".join(validate_ids(material_ids))}) 88 | 89 | if spectrum_ids: 90 | if isinstance(spectrum_ids, str): 91 | spectrum_ids = [spectrum_ids] 92 | 93 | query_params.update({"spectrum_ids": ",".join(spectrum_ids)}) 94 | 95 | query_params = { 96 | entry: query_params[entry] 97 | for entry in query_params 98 | if query_params[entry] is not None 99 | } 100 | 101 | return super()._search( 102 | num_chunks=num_chunks, 103 | chunk_size=chunk_size, 104 | all_fields=all_fields, 105 | fields=fields, 106 | **query_params, 107 | ) 108 | -------------------------------------------------------------------------------- /mp_api/client/routes/molecules/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from .bonds import MoleculesBondRester 4 | from .jcesr import JcesrMoleculesRester 5 | from .molecules import AssociatedMoleculeRester, MoleculeRester 6 | from .orbitals import MoleculesOrbitalsRester 7 | from .partial_charges import MoleculesPartialChargesRester 8 | from .partial_spins import MoleculesPartialSpinsRester 9 | from .redox import MoleculesRedoxRester 10 | from .summary import MoleculesSummaryRester 11 | from .tasks import MoleculesTaskRester 12 | from .thermo import MoleculesThermoRester 13 | from .vibrations import MoleculesVibrationRester 14 | -------------------------------------------------------------------------------- /mp_api/client/routes/molecules/bonds.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from emmet.core.molecules.bonds import MoleculeBondingDoc 4 | from emmet.core.mpid import MPculeID 5 | 6 | from mp_api.client.core import BaseRester 7 | 8 | 9 | class MoleculesBondRester(BaseRester[MoleculeBondingDoc]): 10 | suffix = "molecules/bonding" 11 | document_model = MoleculeBondingDoc 12 | primary_key = "property_id" 13 | 14 | def search( 15 | self, 16 | molecule_ids: MPculeID | list[MPculeID] | None = None, 17 | property_ids: str | list[str] | None = None, 18 | method: str | None = None, 19 | bond_type: str | None = None, 20 | max_bond_length: float | None = None, 21 | min_bond_length: float | None = None, 22 | charge: int | None = None, 23 | spin_multiplicity: int | None = None, 24 | level_of_theory: str | None = None, 25 | solvent: str | None = None, 26 | lot_solvent: str | None = None, 27 | formula: str | list[str] | None = None, 28 | elements: list[str] | None = None, 29 | exclude_elements: list[str] | None = None, 30 | chemsys: str | list[str] | None = None, 31 | num_chunks: int | None = None, 32 | chunk_size: int = 1000, 33 | all_fields: bool = True, 34 | fields: list[str] | None = None, 35 | ): 36 | """Query molecules bonding docs using a variety of search criteria. 37 | 38 | Arguments: 39 | molecule_ids (MPculeID, List[MPculeID]): List of Materials Project Molecule IDs (MPculeIDs) to return data 40 | for. 41 | property_ids (str, List[str]): List of property IDs to return data for. 42 | method (str): Method used to generate bonding data 43 | (e.g. "nbo", "critic2", or "OpenBabelNN + metal_edge_extender") 44 | bond_type (str): Bond type of interest (e.g. "C-O" for carbon-oxygen bonds) 45 | max_bond_length (float): Maximum bond length in the molecule of the specified type 46 | min_bond_length (float): Minimum bond length in the molecule of the specified type 47 | charge (Tuple[int, int]): Minimum and maximum charge for the molecule. 48 | spin_multiplicity (Tuple[int, int]): Minimum and maximum spin for the molecule. 49 | level_of_theory (str): Desired level of theory (e.g. "wB97X-V/def2-TZVPPD/SMD") 50 | solvent (str): Desired solvent (e.g. "SOLVENT=WATER") 51 | lot_solvent (str): Desired combination of level of theory and solvent 52 | (e.g. "wB97X-V/def2-TZVPPD/SMD(SOLVENT=THF)") 53 | formula (str, List[str]): An alphabetical formula or list of formulas 54 | (e.g. "C2 Li2 O4", ["C2 H4", "C2 H6"]). 55 | elements (List[str]): A list of elements. 56 | exclude_elements (List(str)): List of elements to exclude. 57 | chemsys (str, List[str]): A chemical system, list of chemical systems 58 | (e.g., Li-C-O, [C-O-H-N, Li-N]). 59 | num_chunks (int): Maximum number of chunks of data to yield. None will yield all possible. 60 | chunk_size (int): Number of data entries per chunk. 61 | all_fields (bool): Whether to return all fields in the document. Defaults to True. 62 | fields (List[str]): List of fields in MoleculeDoc to return data for. 63 | Default is "molecule_id", "property_id", "solvent", "method", "last_updated" 64 | if all_fields is False. 65 | 66 | Returns: 67 | ([MoleculeBondingDoc]) List of bonding documents 68 | """ 69 | query_params = {} # type: dict 70 | 71 | if molecule_ids: 72 | if isinstance(molecule_ids, str): 73 | molecule_ids = [molecule_ids] 74 | 75 | query_params.update({"molecule_ids": ",".join(molecule_ids)}) 76 | 77 | if property_ids: 78 | if isinstance(property_ids, str): 79 | property_ids = [property_ids] 80 | 81 | query_params.update({"property_ids": ",".join(property_ids)}) 82 | 83 | if method: 84 | query_params.update({"method": method}) 85 | 86 | if bond_type: 87 | query_params.update({"bond_type": bond_type}) 88 | 89 | if max_bond_length: 90 | query_params.update({"max_bond_length": max_bond_length}) 91 | 92 | if min_bond_length: 93 | query_params.update({"min_bond_length": max_bond_length}) 94 | 95 | if charge: 96 | query_params.update({"charge": charge}) 97 | 98 | if spin_multiplicity: 99 | query_params.update({"spin_multiplicity": spin_multiplicity}) 100 | 101 | if level_of_theory: 102 | query_params.update({"level_of_theory": level_of_theory}) 103 | 104 | if solvent: 105 | query_params.update({"solvent": solvent}) 106 | 107 | if lot_solvent: 108 | query_params.update({"lot_solvent": lot_solvent}) 109 | 110 | if formula: 111 | if isinstance(formula, str): 112 | formula = [formula] 113 | 114 | query_params.update({"formula": ",".join(formula)}) 115 | 116 | if chemsys: 117 | if isinstance(chemsys, str): 118 | chemsys = [chemsys] 119 | 120 | query_params.update({"chemsys": ",".join(chemsys)}) 121 | 122 | if elements: 123 | query_params.update({"elements": ",".join(elements)}) 124 | 125 | if exclude_elements: 126 | query_params.update({"exclude_elements": ",".join(exclude_elements)}) 127 | 128 | return super()._search( 129 | num_chunks=num_chunks, 130 | chunk_size=chunk_size, 131 | all_fields=all_fields, 132 | fields=fields, 133 | **query_params, 134 | ) 135 | -------------------------------------------------------------------------------- /mp_api/client/routes/molecules/jcesr.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections import defaultdict 4 | 5 | from emmet.core.molecules_jcesr import MoleculesDoc 6 | from pymatgen.core.periodic_table import Element 7 | 8 | from mp_api.client.core import BaseRester 9 | from mp_api.client.core.utils import validate_ids 10 | 11 | 12 | class JcesrMoleculesRester(BaseRester[MoleculesDoc]): 13 | suffix = "molecules/jcesr" 14 | document_model = MoleculesDoc # type: ignore 15 | primary_key = "task_id" 16 | 17 | def search( 18 | self, 19 | task_ids: str | list[str] | None = None, 20 | charge: tuple[float, float] | None = None, 21 | elements: list[Element] | None = None, 22 | EA: tuple[float, float] | None = None, 23 | IE: tuple[float, float] | None = None, 24 | nelements: tuple[float, float] | None = None, 25 | pointgroup: str | None = None, 26 | smiles: str | None = None, 27 | num_chunks: int | None = None, 28 | chunk_size: int = 1000, 29 | all_fields: bool = True, 30 | fields: list[str] | None = None, 31 | ): 32 | """Query equations of state docs using a variety of search criteria. 33 | 34 | Arguments: 35 | task_ids (str, List[str]): A single molecule task ID string or list of strings. 36 | (e.g., mol-45004, [mol-45004, mol-45228]). 37 | charge (Tuple[float,float]): Minimum and maximum value of the charge in +e to consider. 38 | elements (List[Element]): A list of elements. 39 | film_orientation (List[Elements]): List of elements that are in the molecule. 40 | EA (Tuple[float,float]): Minimum and maximum value of the electron affinity in eV to consider. 41 | IE (Tuple[float,float]): Minimum and maximum value of the ionization energy in eV to consider. 42 | nelements (Tuple[float,float]): Minimum and maximum number of elements in the molecule to consider. 43 | pointgroup (str): Point group of the molecule in Schoenflies notation. 44 | smiles (str): The simplified molecular input line-entry system (SMILES) representation of the molecule. 45 | num_chunks (int): Maximum number of chunks of data to yield. None will yield all possible. 46 | chunk_size (int): Number of data entries per chunk. 47 | all_fields (bool): Whether to return all fields in the document. Defaults to True. 48 | fields (List[str]): List of fields in MoleculesDoc to return data for. 49 | Default is the material_id only if all_fields is False. 50 | 51 | Returns: 52 | ([MoleculesDoc]) List of molecule documents 53 | """ 54 | query_params = defaultdict(dict) # type: dict 55 | 56 | if task_ids: 57 | if isinstance(task_ids, str): 58 | task_ids = [task_ids] 59 | 60 | query_params.update({"task_ids": ",".join(validate_ids(task_ids))}) 61 | 62 | if elements: 63 | query_params.update({"elements": ",".join([str(ele) for ele in elements])}) 64 | 65 | if pointgroup: 66 | query_params.update({"pointgroup": pointgroup}) 67 | 68 | if smiles: 69 | query_params.update({"smiles": smiles}) 70 | 71 | if nelements: 72 | query_params.update( 73 | {"nelements_min": nelements[0], "nelements_max": nelements[1]} 74 | ) 75 | 76 | if EA: 77 | query_params.update({"EA_min": EA[0], "EA_max": EA[1]}) 78 | 79 | if IE: 80 | query_params.update({"IE_min": IE[0], "IE_max": IE[1]}) 81 | 82 | if charge: 83 | query_params.update({"charge_min": charge[0], "charge_max": charge[1]}) 84 | 85 | query_params = { 86 | entry: query_params[entry] 87 | for entry in query_params 88 | if query_params[entry] is not None 89 | } 90 | 91 | return super()._search( 92 | num_chunks=num_chunks, 93 | chunk_size=chunk_size, 94 | all_fields=all_fields, 95 | fields=fields, 96 | **query_params, 97 | ) 98 | -------------------------------------------------------------------------------- /mp_api/client/routes/molecules/partial_charges.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from emmet.core.molecules.atomic import PartialChargesDoc 4 | from emmet.core.mpid import MPculeID 5 | 6 | from mp_api.client.core import BaseRester 7 | 8 | 9 | class MoleculesPartialChargesRester(BaseRester[PartialChargesDoc]): 10 | suffix = "molecules/partial_charges" 11 | document_model = PartialChargesDoc 12 | primary_key = "property_id" 13 | 14 | def search( 15 | self, 16 | molecule_ids: MPculeID | list[MPculeID] | None = None, 17 | property_ids: str | list[str] | None = None, 18 | method: str | None = None, 19 | charge: int | None = None, 20 | spin_multiplicity: int | None = None, 21 | level_of_theory: str | None = None, 22 | solvent: str | None = None, 23 | lot_solvent: str | None = None, 24 | formula: str | list[str] | None = None, 25 | elements: list[str] | None = None, 26 | exclude_elements: list[str] | None = None, 27 | chemsys: str | list[str] | None = None, 28 | num_chunks: int | None = None, 29 | chunk_size: int = 1000, 30 | all_fields: bool = True, 31 | fields: list[str] | None = None, 32 | ): 33 | """Query molecules partial charges docs using a variety of search criteria. 34 | 35 | Arguments: 36 | molecule_ids (MPculeID, List[MPculeID]): List of Materials Project Molecule IDs (MPculeIDs) to return data 37 | for. 38 | property_ids (str, List[str]): List of property IDs to return data for. 39 | method (str): Method used to generate bonding data 40 | (e.g. "nbo", "critic2", "mulliken", or "resp") 41 | charge (Tuple[int, int]): Minimum and maximum charge for the molecule. 42 | spin_multiplicity (Tuple[int, int]): Minimum and maximum spin for the molecule. 43 | level_of_theory (str): Desired level of theory (e.g. "wB97X-V/def2-TZVPPD/SMD") 44 | solvent (str): Desired solvent (e.g. "SOLVENT=WATER") 45 | lot_solvent (str): Desired combination of level of theory and solvent 46 | (e.g. "wB97X-V/def2-TZVPPD/SMD(SOLVENT=THF)") 47 | formula (str, List[str]): An alphabetical formula or list of formulas 48 | (e.g. "C2 Li2 O4", ["C2 H4", "C2 H6"]). 49 | elements (List[str]): A list of elements. 50 | exclude_elements (List(str)): List of elements to exclude. 51 | chemsys (str, List[str]): A chemical system, list of chemical systems 52 | (e.g., Li-C-O, [C-O-H-N, Li-N]). 53 | num_chunks (int): Maximum number of chunks of data to yield. None will yield all possible. 54 | chunk_size (int): Number of data entries per chunk. 55 | all_fields (bool): Whether to return all fields in the document. Defaults to True. 56 | fields (List[str]): List of fields in MoleculeDoc to return data for. 57 | Default is "molecule_id", "property_id", "solvent", "method", "last_updated" 58 | if all_fields is False. 59 | 60 | Returns: 61 | ([PartialChargesDoc]) List of partial charges documents 62 | """ 63 | query_params = {} # type: dict 64 | 65 | if molecule_ids: 66 | if isinstance(molecule_ids, str): 67 | molecule_ids = [molecule_ids] 68 | 69 | query_params.update({"molecule_ids": ",".join(molecule_ids)}) 70 | 71 | if property_ids: 72 | if isinstance(property_ids, str): 73 | property_ids = [property_ids] 74 | 75 | query_params.update({"property_ids": ",".join(property_ids)}) 76 | 77 | if method: 78 | query_params.update({"method": method}) 79 | 80 | if charge: 81 | query_params.update({"charge": charge}) 82 | 83 | if spin_multiplicity: 84 | query_params.update({"spin_multiplicity": spin_multiplicity}) 85 | 86 | if level_of_theory: 87 | query_params.update({"level_of_theory": level_of_theory}) 88 | 89 | if solvent: 90 | query_params.update({"solvent": solvent}) 91 | 92 | if lot_solvent: 93 | query_params.update({"lot_solvent": lot_solvent}) 94 | 95 | if formula: 96 | if isinstance(formula, str): 97 | formula = [formula] 98 | 99 | query_params.update({"formula": ",".join(formula)}) 100 | 101 | if chemsys: 102 | if isinstance(chemsys, str): 103 | chemsys = [chemsys] 104 | 105 | query_params.update({"chemsys": ",".join(chemsys)}) 106 | 107 | if elements: 108 | query_params.update({"elements": ",".join(elements)}) 109 | 110 | if exclude_elements: 111 | query_params.update({"exclude_elements": ",".join(exclude_elements)}) 112 | 113 | return super()._search( 114 | num_chunks=num_chunks, 115 | chunk_size=chunk_size, 116 | all_fields=all_fields, 117 | fields=fields, 118 | **query_params, 119 | ) 120 | -------------------------------------------------------------------------------- /mp_api/client/routes/molecules/partial_spins.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from emmet.core.molecules.atomic import PartialSpinsDoc 4 | from emmet.core.mpid import MPculeID 5 | 6 | from mp_api.client.core import BaseRester 7 | 8 | 9 | class MoleculesPartialSpinsRester(BaseRester[PartialSpinsDoc]): 10 | suffix = "molecules/partial_spins" 11 | document_model = PartialSpinsDoc 12 | primary_key = "property_id" 13 | 14 | def search( 15 | self, 16 | molecule_ids: MPculeID | list[MPculeID] | None = None, 17 | property_ids: str | list[str] | None = None, 18 | method: str | None = None, 19 | charge: int | None = None, 20 | spin_multiplicity: int | None = None, 21 | level_of_theory: str | None = None, 22 | solvent: str | None = None, 23 | lot_solvent: str | None = None, 24 | formula: str | list[str] | None = None, 25 | elements: list[str] | None = None, 26 | exclude_elements: list[str] | None = None, 27 | chemsys: str | list[str] | None = None, 28 | num_chunks: int | None = None, 29 | chunk_size: int = 1000, 30 | all_fields: bool = True, 31 | fields: list[str] | None = None, 32 | ): 33 | """Query molecules partial spins docs using a variety of search criteria. 34 | 35 | Arguments: 36 | molecule_ids (MPculeID, List[MPculeID]): List of Materials Project Molecule IDs (MPculeIDs) to return data 37 | for. 38 | property_ids (str, List[str]): List of property IDs to return data for. 39 | method (str): Method used to generate bonding data 40 | (e.g. "nbo" or "mulliken") 41 | charge (Tuple[int, int]): Minimum and maximum charge for the molecule. 42 | spin_multiplicity (Tuple[int, int]): Minimum and maximum spin for the molecule. 43 | level_of_theory (str): Desired level of theory (e.g. "wB97X-V/def2-TZVPPD/SMD") 44 | solvent (str): Desired solvent (e.g. "SOLVENT=WATER") 45 | lot_solvent (str): Desired combination of level of theory and solvent 46 | (e.g. "wB97X-V/def2-TZVPPD/SMD(SOLVENT=THF)") 47 | formula (str, List[str]): An alphabetical formula or list of formulas 48 | (e.g. "C2 Li2 O4", ["C2 H4", "C2 H6"]). 49 | elements (List[str]): A list of elements. 50 | exclude_elements (List(str)): List of elements to exclude. 51 | chemsys (str, List[str]): A chemical system, list of chemical systems 52 | (e.g., Li-C-O, [C-O-H-N, Li-N]). 53 | num_chunks (int): Maximum number of chunks of data to yield. None will yield all possible. 54 | chunk_size (int): Number of data entries per chunk. 55 | all_fields (bool): Whether to return all fields in the document. Defaults to True. 56 | fields (List[str]): List of fields in MoleculeDoc to return data for. 57 | Default is "molecule_id", "property_id", "solvent", "method", "last_updated" 58 | if all_fields is False. 59 | 60 | Returns: 61 | ([PartialSpinsDoc]) List of partial charges documents 62 | """ 63 | query_params = {} # type: dict 64 | 65 | if molecule_ids: 66 | if isinstance(molecule_ids, str): 67 | molecule_ids = [molecule_ids] 68 | 69 | query_params.update({"molecule_ids": ",".join(molecule_ids)}) 70 | 71 | if property_ids: 72 | if isinstance(property_ids, str): 73 | property_ids = [property_ids] 74 | 75 | query_params.update({"property_ids": ",".join(property_ids)}) 76 | 77 | if method: 78 | query_params.update({"method": method}) 79 | 80 | if charge: 81 | query_params.update({"charge": charge}) 82 | 83 | if spin_multiplicity: 84 | query_params.update({"spin_multiplicity": spin_multiplicity}) 85 | 86 | if level_of_theory: 87 | query_params.update({"level_of_theory": level_of_theory}) 88 | 89 | if solvent: 90 | query_params.update({"solvent": solvent}) 91 | 92 | if lot_solvent: 93 | query_params.update({"lot_solvent": lot_solvent}) 94 | 95 | if formula: 96 | if isinstance(formula, str): 97 | formula = [formula] 98 | 99 | query_params.update({"formula": ",".join(formula)}) 100 | 101 | if chemsys: 102 | if isinstance(chemsys, str): 103 | chemsys = [chemsys] 104 | 105 | query_params.update({"chemsys": ",".join(chemsys)}) 106 | 107 | if elements: 108 | query_params.update({"elements": ",".join(elements)}) 109 | 110 | if exclude_elements: 111 | query_params.update({"exclude_elements": ",".join(exclude_elements)}) 112 | 113 | return super()._search( 114 | num_chunks=num_chunks, 115 | chunk_size=chunk_size, 116 | all_fields=all_fields, 117 | fields=fields, 118 | **query_params, 119 | ) 120 | -------------------------------------------------------------------------------- /mp_api/client/routes/molecules/summary.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections import defaultdict 4 | 5 | from emmet.core.molecules.summary import HasProps, MoleculeSummaryDoc 6 | from emmet.core.mpid import MPculeID 7 | 8 | from mp_api.client.core import BaseRester 9 | 10 | 11 | class MoleculesSummaryRester(BaseRester[MoleculeSummaryDoc]): 12 | suffix = "molecules/summary" 13 | document_model = MoleculeSummaryDoc # type: ignore 14 | primary_key = "molecule_id" 15 | 16 | def search( 17 | self, 18 | charge: int | None = None, 19 | spin_multiplicity: int | None = None, 20 | nelements: tuple[int, int] | None = None, 21 | chemsys: str | list[str] | None = None, 22 | # deprecated: bool | None = None, 23 | elements: list[str] | None = None, 24 | exclude_elements: list[str] | None = None, 25 | formula: str | list[str] | None = None, 26 | has_props: list[HasProps] | None = None, 27 | molecule_ids: list[MPculeID] | None = None, 28 | # has_solvent: Optional[Union[str, List[str]]] = None, 29 | # has_level_of_theory: Optional[Union[str, List[str]]] = None, 30 | # has_lot_solvent: Optional[Union[str, List[str]]] = None, 31 | # with_solvent: Optional[str] = None, 32 | # num_sites: Optional[Tuple[int, int]] = None, 33 | num_chunks: int | None = None, 34 | chunk_size: int = 1000, 35 | all_fields: bool = True, 36 | fields: list[str] | None = None, 37 | ): 38 | """Query core data using a variety of search criteria. 39 | 40 | Arguments: 41 | charge (int): Minimum and maximum charge for the molecule. 42 | spin_multiplicity (int): Minimum and maximum spin for the molecule. 43 | nelements (Tuple[int, int]): Minimum and maximum number of elements 44 | # has_solvent (str, List[str]): Whether the molecule has properties calculated in 45 | # solvents (e.g., "SOLVENT=THF", ["SOLVENT=WATER", "VACUUM"]) 46 | # has_level_of_theory (str, List[str]): Whether the molecule has properties calculated 47 | # using a particular level of theory (e.g. "wB97M-V/def2-SVPD/SMD", 48 | # ["wB97X-V/def2-TZVPPD/SMD", "wB97M-V/def2-QZVPPD/SMD"]) 49 | # has_lot_solvent (str, List[str]): Whether the molecule has properties calculated 50 | # using a particular combination of level of theory and solvent (e.g. 51 | # "wB97X-V/def2-SVPD/SMD(SOLVENT=THF)", 52 | # ["wB97X-V/def2-TZVPPD/SMD(VACUUM)", "wB97M-V/def2-QZVPPD/SMD(SOLVENT=WATER)"]) 53 | chemsys (str, List[str]): A chemical system, list of chemical systems 54 | (e.g., Li-C-O, [C-O-H-N, Li-N]). 55 | #deprecated (bool): Whether the material is tagged as deprecated. 56 | elements (List[str]): A list of elements. 57 | exclude_elements (List(str)): List of elements to exclude. 58 | formula (str, List[str]): An alphabetical formula or list of formulas 59 | (e.g. "C2 Li2 O4", ["C2 H4", "C2 H6"]). 60 | has_props: (List[HasProps]): The calculated properties available for the material. 61 | molecule_ids (List[MPculeID]): List of Materials Project Molecule IDs (MPculeIDs) to return data for. 62 | num_chunks (int): Maximum number of chunks of data to yield. None will yield all possible. 63 | chunk_size (int): Number of data entries per chunk. 64 | all_fields (bool): Whether to return all fields in the document. Defaults to True. 65 | fields (List[str]): List of fields in SearchDoc to return data for. 66 | Default is material_id if all_fields is False. 67 | 68 | Returns: 69 | ([MoleculeSummaryDoc]) List of molecules summary documents 70 | """ 71 | query_params = defaultdict(dict) # type: dict 72 | 73 | min_max = [ 74 | "nelements", 75 | "ionization_energy", 76 | "electron_affinity", 77 | "reduction_free_energy", 78 | "oxidation_free_energy", 79 | ] 80 | 81 | for param, value in locals().items(): 82 | if param in min_max and value: 83 | if isinstance(value, (int, float)): 84 | value = (value, value) 85 | query_params.update( 86 | { 87 | f"{param}_min": value[0], 88 | f"{param}_max": value[1], 89 | } 90 | ) 91 | 92 | if molecule_ids: 93 | query_params.update({"molecule_ids": ",".join(molecule_ids)}) 94 | 95 | if charge: 96 | query_params.update({"charge": charge}) 97 | 98 | if spin_multiplicity: 99 | query_params.update({"spin_multiplicity": spin_multiplicity}) 100 | 101 | # if deprecated is not None: 102 | # query_params.update({"deprecated": deprecated}) 103 | 104 | if formula: 105 | if isinstance(formula, str): 106 | formula = [formula] 107 | 108 | query_params.update({"formula": ",".join(formula)}) 109 | 110 | if chemsys: 111 | if isinstance(chemsys, str): 112 | chemsys = [chemsys] 113 | 114 | query_params.update({"chemsys": ",".join(chemsys)}) 115 | 116 | if elements: 117 | query_params.update({"elements": ",".join(elements)}) 118 | 119 | if exclude_elements is not None: 120 | query_params.update({"exclude_elements": ",".join(exclude_elements)}) 121 | 122 | if has_props: 123 | query_params.update({"has_props": ",".join([i.value for i in has_props])}) 124 | 125 | query_params = { 126 | entry: query_params[entry] 127 | for entry in query_params 128 | if query_params[entry] is not None 129 | } 130 | 131 | return super()._search( 132 | num_chunks=num_chunks, 133 | chunk_size=chunk_size, 134 | all_fields=all_fields, 135 | fields=fields, 136 | **query_params, 137 | ) 138 | -------------------------------------------------------------------------------- /mp_api/client/routes/molecules/tasks.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from emmet.core.qchem.task import TaskDocument 4 | 5 | from mp_api.client.core import BaseRester 6 | from mp_api.client.core.utils import validate_ids 7 | 8 | 9 | class MoleculesTaskRester(BaseRester[TaskDocument]): 10 | suffix = "molecules/tasks" 11 | document_model = TaskDocument 12 | primary_key = "task_id" 13 | 14 | # TODO: get_trajectory method (once PR in pymatgen) 15 | 16 | def search( 17 | self, 18 | task_ids: list[str] | None = None, 19 | chemsys: str | list[str] | None = None, 20 | elements: list[str] | None = None, 21 | exclude_elements: list[str] | None = None, 22 | formula: str | list[str] | None = None, 23 | num_chunks: int | None = None, 24 | chunk_size: int = 1000, 25 | all_fields: bool = True, 26 | fields: list[str] | None = None, 27 | ): 28 | """Query core task docs using a variety of search criteria. 29 | 30 | Arguments: 31 | task_ids (List[str]): List of Materials Project IDs to return data for. 32 | chemsys (str, List[str]): A chemical system or list of chemical systems 33 | (e.g., C-H-O, [C-Li-O, Li-O]). 34 | elements (List[str]): A list of elements. 35 | exclude_elements (List[str]): A list of elements to exclude. 36 | formula (str, List[str]): An alphabetical formula (e.g. "C1 Li2 O3" or ["C2 H4", "C2 H6"]). 37 | num_chunks (int): Maximum number of chunks of data to yield. None will yield all possible. 38 | chunk_size (int): Number of data entries per chunk. Max size is 100. 39 | all_fields (bool): Whether to return all fields in the document. Defaults to True. 40 | fields (List[str]): List of fields in TaskDocument to return data for. 41 | Default is material_id, last_updated, and formula_pretty if all_fields is False. 42 | 43 | Returns: 44 | ([TaskDocument]) List of task documents 45 | """ 46 | query_params = {} # type: dict 47 | 48 | if task_ids: 49 | query_params.update({"task_ids": ",".join(validate_ids(task_ids))}) 50 | 51 | if formula: 52 | query_params.update({"formula": formula}) 53 | 54 | if elements: 55 | query_params.update({"elements": ",".join(elements)}) 56 | 57 | if exclude_elements: 58 | query_params.update({"exclude_elements": ",".join(exclude_elements)}) 59 | 60 | if chemsys: 61 | if isinstance(chemsys, str): 62 | chemsys = [chemsys] 63 | 64 | query_params.update({"chemsys": ",".join(chemsys)}) 65 | 66 | return super()._search( 67 | num_chunks=num_chunks, 68 | chunk_size=chunk_size, 69 | all_fields=all_fields, 70 | fields=fields, 71 | **query_params, 72 | ) 73 | -------------------------------------------------------------------------------- /mp_api/client/routes/molecules/vibrations.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from emmet.core.molecules.vibration import VibrationDoc 4 | from emmet.core.mpid import MPculeID 5 | 6 | from mp_api.client.core import BaseRester 7 | 8 | 9 | class MoleculesVibrationRester(BaseRester[VibrationDoc]): 10 | suffix = "molecules/vibrations" 11 | document_model = VibrationDoc 12 | primary_key = "property_id" 13 | 14 | def search( 15 | self, 16 | molecule_ids: MPculeID | list[MPculeID] | None = None, 17 | property_ids: str | list[str] | None = None, 18 | charge: int | None = None, 19 | spin_multiplicity: int | None = None, 20 | level_of_theory: str | None = None, 21 | solvent: str | None = None, 22 | lot_solvent: str | None = None, 23 | formula: str | list[str] | None = None, 24 | elements: list[str] | None = None, 25 | exclude_elements: list[str] | None = None, 26 | chemsys: str | list[str] | None = None, 27 | num_chunks: int | None = None, 28 | chunk_size: int = 1000, 29 | all_fields: bool = True, 30 | fields: list[str] | None = None, 31 | ): 32 | """Query molecules vibration docs using a variety of search criteria. 33 | 34 | Arguments: 35 | molecule_ids (MPculeID, List[MPculeID]): List of Materials Project Molecule IDs (MPculeIDs) to return data 36 | for. 37 | property_ids (str, List[str]): List of property IDs to return data for. 38 | charge (Tuple[int, int]): Minimum and maximum charge for the molecule. 39 | spin_multiplicity (Tuple[int, int]): Minimum and maximum spin for the molecule. 40 | level_of_theory (str): Desired level of theory (e.g. "wB97X-V/def2-TZVPPD/SMD") 41 | solvent (str): Desired solvent (e.g. "SOLVENT=WATER") 42 | lot_solvent (str): Desired combination of level of theory and solvent 43 | (e.g. "wB97X-V/def2-TZVPPD/SMD(SOLVENT=THF)") 44 | correction_level_of_theory (str): Desired correction level of theory (e.g. "wB97X-V/def2-TZVPPD/SMD") 45 | correction_solvent (str): Desired correction solvent (e.g. "SOLVENT=WATER") 46 | correction_lot_solvent (str): Desired correction combination of level of theory and solvent 47 | (e.g. "wB97X-V/def2-TZVPPD/SMD(SOLVENT=THF)") 48 | combined_lot_solvent (str): Desired combination of level of theory and solvent including both main 49 | thermo calculation and single-point energy correction 50 | (e.g. "wB97X-D/def2-SVPD/VACUUM//wB97X-V/def2-TZVPPD/SMD(SOLVENT=THF)") 51 | electronic_energy (Tuple[float, float]): Minimum and maximum electronic energy 52 | zero_point_energy (Tuple[float, float]): Minimum and maximum zero-point energy 53 | total_enthalpy (Tuple[float, float]): Minimum and maximum total enthalpy 54 | total_entropy (Tuple[float, float]): Minimum and maximum total entropy 55 | translational_enthalpy (Tuple[float, float]): Minimum and maximum translational enthalpy 56 | rotational_enthalpy (Tuple[float, float]): Minimum and maximum rotational enthalpy 57 | vibrational_enthalpy (Tuple[float, float]): Minimum and maximum vibrational enthalpy 58 | translational_entropy (Tuple[float, float]): Minimum and maximum translational enthalpy 59 | rotational_entropy (Tuple[float, float]): Minimum and maximum rotational enthalpy 60 | vibrational_entropy (Tuple[float, float]): Minimum and maximum vibrational enthalpy 61 | free_energy (Tuple[float, float]): Minimum and maximum free energy 62 | formula (str, List[str]): An alphabetical formula or list of formulas 63 | (e.g. "C2 Li2 O4", ["C2 H4", "C2 H6"]). 64 | elements (List[str]): A list of elements. 65 | exclude_elements (List(str)): List of elements to exclude. 66 | chemsys (str, List[str]): A chemical system, list of chemical systems 67 | (e.g., Li-C-O, [C-O-H-N, Li-N]). 68 | num_chunks (int): Maximum number of chunks of data to yield. None will yield all possible. 69 | chunk_size (int): Number of data entries per chunk. 70 | all_fields (bool): Whether to return all fields in the document. Defaults to True. 71 | fields (List[str]): List of fields in MoleculeDoc to return data for. 72 | Default is "molecule_id", "property_id", "solvent", "method", "last_updated" 73 | if all_fields is False. 74 | 75 | Returns: 76 | ([VibrationDoc]) List of molecule vibration documents 77 | """ 78 | query_params = {} # type: dict 79 | 80 | if molecule_ids: 81 | if isinstance(molecule_ids, str): 82 | molecule_ids = [molecule_ids] 83 | 84 | query_params.update({"molecule_ids": ",".join(molecule_ids)}) 85 | 86 | if property_ids: 87 | if isinstance(property_ids, str): 88 | property_ids = [property_ids] 89 | 90 | query_params.update({"property_ids": ",".join(property_ids)}) 91 | 92 | if charge: 93 | query_params.update({"charge": charge}) 94 | 95 | if spin_multiplicity: 96 | query_params.update({"spin_multiplicity": spin_multiplicity}) 97 | 98 | if level_of_theory: 99 | query_params.update({"level_of_theory": level_of_theory}) 100 | 101 | if solvent: 102 | query_params.update({"solvent": solvent}) 103 | 104 | if lot_solvent: 105 | query_params.update({"lot_solvent": lot_solvent}) 106 | 107 | if formula: 108 | if isinstance(formula, str): 109 | formula = [formula] 110 | 111 | query_params.update({"formula": ",".join(formula)}) 112 | 113 | if chemsys: 114 | if isinstance(chemsys, str): 115 | chemsys = [chemsys] 116 | 117 | query_params.update({"chemsys": ",".join(chemsys)}) 118 | 119 | if elements: 120 | query_params.update({"elements": ",".join(elements)}) 121 | 122 | if exclude_elements: 123 | query_params.update({"exclude_elements": ",".join(exclude_elements)}) 124 | 125 | return super()._search( 126 | num_chunks=num_chunks, 127 | chunk_size=chunk_size, 128 | all_fields=all_fields, 129 | fields=fields, 130 | **query_params, 131 | ) 132 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "setuptools-scm"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "mp-api" 7 | authors = [ 8 | { name = "The Materials Project", email = "feedback@materialsproject.org" }, 9 | ] 10 | description = "API Client for the Materials Project" 11 | readme = "README.md" 12 | requires-python = ">=3.9" 13 | license = { text = "modified BSD" } 14 | classifiers = [ 15 | "Programming Language :: Python :: 3", 16 | "Development Status :: 4 - Beta", 17 | "Intended Audience :: Science/Research", 18 | "Intended Audience :: Information Technology", 19 | "Operating System :: OS Independent", 20 | "Topic :: Scientific/Engineering", 21 | ] 22 | dependencies = [ 23 | "setuptools", 24 | "msgpack", 25 | "maggma>=0.57.1", 26 | "pymatgen>=2022.3.7,!=2024.2.20", 27 | "typing-extensions>=3.7.4.1", 28 | "requests>=2.23.0", 29 | "monty>=2024.12.10", 30 | "emmet-core>=0.84.3rc6", 31 | "smart_open", 32 | ] 33 | dynamic = ["version"] 34 | 35 | [project.optional-dependencies] 36 | all = ["emmet-core[all]>=0.69.1", "custodian", "mpcontribs-client", "boto3"] 37 | test = [ 38 | "pre-commit", 39 | "pytest", 40 | "pytest-asyncio", 41 | "pytest-cov", 42 | "pytest-mock", 43 | "flake8", 44 | "pycodestyle", 45 | "mypy", 46 | "mypy-extensions", 47 | "typing-extensions", 48 | "types-setuptools", 49 | "types-requests", 50 | ] 51 | 52 | [tool.setuptools.packages.find] 53 | include = ["mp_api*"] 54 | namespaces = true 55 | 56 | [tool.setuptools_scm] 57 | 58 | [tool.pytest.ini_options] 59 | minversion = "6.0" 60 | addopts = ["-ra", "--showlocals", "--strict-markers", "--strict-config"] 61 | xfail_strict = true 62 | filterwarnings = ["default"] 63 | log_cli_level = "info" 64 | testpaths = ["tests"] 65 | 66 | [tool.ruff] 67 | target-version = "py38" 68 | select = [ 69 | "B", # flake8-bugbear 70 | "D", # pydocstyle 71 | "E", # pycodestyle 72 | "F", # pyflakes 73 | "I", # isort 74 | "PLE", # pylint error 75 | "PLW", # pylint warning 76 | "PYI", # flakes8-pyi 77 | "Q", # flake8-quotes 78 | "SIM", # flake8-simplify 79 | "TID", # tidy imports 80 | "UP", # pyupgrade 81 | "W", # pycodestyle 82 | "YTT", # flake8-2020 83 | ] 84 | ignore = [ 85 | "B019", # Use of functools.lru_cache or functools.cache on methods can lead to memory leaks 86 | "B028", # No explicit stacklevel keyword argument found 87 | "B904", # Within an except clause, raise exceptions with raise ... from err 88 | "D100", # Missing docstring in public module 89 | "D104", # Missing docstring in public package 90 | "D101", # Missing docstring in public class 91 | "D102", # Missing docstring in public method 92 | "D105", # Missing docstring in magic method 93 | "D106", # Missing docstring in public nested class 94 | "D205", # 1 blank line required between summary line and description 95 | "PLW2901", # Outer for loop variable overwritten by inner assignment target 96 | "SIM105", # Use contextlib.suppress(FileNotFoundError) instead of try-except-pass 97 | "SIM115", # Use context handler for opening files, 98 | "E501", 99 | ] 100 | exclude = ["docs", ".ruff_cache", "requirements", "test*", "settings.py"] 101 | pydocstyle.convention = "google" 102 | flake8-unused-arguments.ignore-variadic-names = true 103 | isort.required-imports = ["from __future__ import annotations"] 104 | 105 | [tool.ruff.per-file-ignores] 106 | "*/__init__.py" = ["F401"] # F401: imported but unused 107 | -------------------------------------------------------------------------------- /requirements/requirements-ubuntu-latest_py3.11.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.11 3 | # by the following command: 4 | # 5 | # pip-compile --output-file=requirements/requirements-ubuntu-latest_py3.11.txt pyproject.toml 6 | # 7 | aioitertools==0.12.0 8 | # via maggma 9 | annotated-types==0.7.0 10 | # via pydantic 11 | attrs==25.3.0 12 | # via 13 | # jsonlines 14 | # jsonschema 15 | # referencing 16 | bcrypt==4.3.0 17 | # via paramiko 18 | bibtexparser==1.4.3 19 | # via pymatgen 20 | boto3==1.38.31 21 | # via maggma 22 | botocore==1.38.31 23 | # via 24 | # boto3 25 | # s3transfer 26 | certifi==2025.4.26 27 | # via requests 28 | cffi==1.17.1 29 | # via 30 | # cryptography 31 | # pynacl 32 | charset-normalizer==3.4.2 33 | # via requests 34 | contourpy==1.3.2 35 | # via matplotlib 36 | cryptography==45.0.3 37 | # via paramiko 38 | cycler==0.12.1 39 | # via matplotlib 40 | dnspython==2.7.0 41 | # via 42 | # maggma 43 | # pymongo 44 | emmet-core==0.84.7rc3 45 | # via mp-api (pyproject.toml) 46 | fonttools==4.58.1 47 | # via matplotlib 48 | idna==3.10 49 | # via requests 50 | jmespath==1.0.1 51 | # via 52 | # boto3 53 | # botocore 54 | joblib==1.5.1 55 | # via pymatgen 56 | jsonlines==4.0.0 57 | # via maggma 58 | jsonschema==4.24.0 59 | # via maggma 60 | jsonschema-specifications==2025.4.1 61 | # via jsonschema 62 | kiwisolver==1.4.8 63 | # via matplotlib 64 | latexcodec==3.0.0 65 | # via pybtex 66 | maggma==0.71.5 67 | # via mp-api (pyproject.toml) 68 | matplotlib==3.10.3 69 | # via pymatgen 70 | mongomock==4.3.0 71 | # via maggma 72 | monty==2025.3.3 73 | # via 74 | # emmet-core 75 | # maggma 76 | # mp-api (pyproject.toml) 77 | # pymatgen 78 | mpmath==1.3.0 79 | # via sympy 80 | msgpack==1.1.0 81 | # via 82 | # maggma 83 | # mp-api (pyproject.toml) 84 | narwhals==1.41.0 85 | # via plotly 86 | networkx==3.5 87 | # via pymatgen 88 | numpy==2.2.6 89 | # via 90 | # contourpy 91 | # maggma 92 | # matplotlib 93 | # monty 94 | # pandas 95 | # pymatgen 96 | # scipy 97 | # spglib 98 | orjson==3.10.18 99 | # via 100 | # maggma 101 | # pymatgen 102 | packaging==25.0 103 | # via 104 | # matplotlib 105 | # mongomock 106 | # plotly 107 | palettable==3.3.3 108 | # via pymatgen 109 | pandas==2.3.0 110 | # via 111 | # maggma 112 | # pymatgen 113 | paramiko==3.5.1 114 | # via sshtunnel 115 | pillow==11.2.1 116 | # via matplotlib 117 | plotly==6.1.2 118 | # via pymatgen 119 | pybtex==0.24.0 120 | # via emmet-core 121 | pycparser==2.22 122 | # via cffi 123 | pydantic==2.11.5 124 | # via 125 | # emmet-core 126 | # maggma 127 | # pydantic-settings 128 | pydantic-core==2.33.2 129 | # via pydantic 130 | pydantic-settings==2.9.1 131 | # via 132 | # emmet-core 133 | # maggma 134 | pydash==8.0.5 135 | # via maggma 136 | pymatgen==2025.5.28 137 | # via 138 | # emmet-core 139 | # mp-api (pyproject.toml) 140 | pymongo==4.10.1 141 | # via maggma 142 | pynacl==1.5.0 143 | # via paramiko 144 | pyparsing==3.2.3 145 | # via 146 | # bibtexparser 147 | # matplotlib 148 | python-dateutil==2.9.0.post0 149 | # via 150 | # botocore 151 | # maggma 152 | # matplotlib 153 | # pandas 154 | python-dotenv==1.1.0 155 | # via pydantic-settings 156 | pytz==2025.2 157 | # via 158 | # mongomock 159 | # pandas 160 | pyyaml==6.0.2 161 | # via pybtex 162 | pyzmq==26.4.0 163 | # via maggma 164 | referencing==0.36.2 165 | # via 166 | # jsonschema 167 | # jsonschema-specifications 168 | requests==2.32.3 169 | # via 170 | # mp-api (pyproject.toml) 171 | # pymatgen 172 | rpds-py==0.25.1 173 | # via 174 | # jsonschema 175 | # referencing 176 | ruamel-yaml==0.18.12 177 | # via 178 | # maggma 179 | # monty 180 | # pymatgen 181 | ruamel-yaml-clib==0.2.12 182 | # via ruamel-yaml 183 | s3transfer==0.13.0 184 | # via boto3 185 | scipy==1.15.3 186 | # via pymatgen 187 | sentinels==1.0.0 188 | # via mongomock 189 | six==1.17.0 190 | # via 191 | # pybtex 192 | # python-dateutil 193 | smart-open==7.1.0 194 | # via mp-api (pyproject.toml) 195 | spglib==2.6.0 196 | # via pymatgen 197 | sshtunnel==0.4.0 198 | # via maggma 199 | sympy==1.14.0 200 | # via pymatgen 201 | tabulate==0.9.0 202 | # via pymatgen 203 | tqdm==4.67.1 204 | # via 205 | # maggma 206 | # pymatgen 207 | typing-extensions==4.14.0 208 | # via 209 | # emmet-core 210 | # mp-api (pyproject.toml) 211 | # pydantic 212 | # pydantic-core 213 | # pydash 214 | # referencing 215 | # spglib 216 | # typing-inspection 217 | typing-inspection==0.4.1 218 | # via 219 | # pydantic 220 | # pydantic-settings 221 | tzdata==2025.2 222 | # via pandas 223 | uncertainties==3.2.3 224 | # via pymatgen 225 | urllib3==2.4.0 226 | # via 227 | # botocore 228 | # requests 229 | wrapt==1.17.2 230 | # via smart-open 231 | 232 | # The following packages are considered to be unsafe in a requirements file: 233 | # setuptools 234 | -------------------------------------------------------------------------------- /requirements/requirements-ubuntu-latest_py3.12.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.12 3 | # by the following command: 4 | # 5 | # pip-compile --output-file=requirements/requirements-ubuntu-latest_py3.12.txt pyproject.toml 6 | # 7 | aioitertools==0.12.0 8 | # via maggma 9 | annotated-types==0.7.0 10 | # via pydantic 11 | attrs==25.3.0 12 | # via 13 | # jsonlines 14 | # jsonschema 15 | # referencing 16 | bcrypt==4.3.0 17 | # via paramiko 18 | bibtexparser==1.4.3 19 | # via pymatgen 20 | boto3==1.38.31 21 | # via maggma 22 | botocore==1.38.31 23 | # via 24 | # boto3 25 | # s3transfer 26 | certifi==2025.4.26 27 | # via requests 28 | cffi==1.17.1 29 | # via 30 | # cryptography 31 | # pynacl 32 | charset-normalizer==3.4.2 33 | # via requests 34 | contourpy==1.3.2 35 | # via matplotlib 36 | cryptography==45.0.3 37 | # via paramiko 38 | cycler==0.12.1 39 | # via matplotlib 40 | dnspython==2.7.0 41 | # via 42 | # maggma 43 | # pymongo 44 | emmet-core==0.84.7rc3 45 | # via mp-api (pyproject.toml) 46 | fonttools==4.58.1 47 | # via matplotlib 48 | idna==3.10 49 | # via requests 50 | jmespath==1.0.1 51 | # via 52 | # boto3 53 | # botocore 54 | joblib==1.5.1 55 | # via pymatgen 56 | jsonlines==4.0.0 57 | # via maggma 58 | jsonschema==4.24.0 59 | # via maggma 60 | jsonschema-specifications==2025.4.1 61 | # via jsonschema 62 | kiwisolver==1.4.8 63 | # via matplotlib 64 | latexcodec==3.0.0 65 | # via pybtex 66 | maggma==0.71.5 67 | # via mp-api (pyproject.toml) 68 | matplotlib==3.10.3 69 | # via pymatgen 70 | mongomock==4.3.0 71 | # via maggma 72 | monty==2025.3.3 73 | # via 74 | # emmet-core 75 | # maggma 76 | # mp-api (pyproject.toml) 77 | # pymatgen 78 | mpmath==1.3.0 79 | # via sympy 80 | msgpack==1.1.0 81 | # via 82 | # maggma 83 | # mp-api (pyproject.toml) 84 | narwhals==1.41.0 85 | # via plotly 86 | networkx==3.5 87 | # via pymatgen 88 | numpy==2.2.6 89 | # via 90 | # contourpy 91 | # maggma 92 | # matplotlib 93 | # monty 94 | # pandas 95 | # pymatgen 96 | # scipy 97 | # spglib 98 | orjson==3.10.18 99 | # via 100 | # maggma 101 | # pymatgen 102 | packaging==25.0 103 | # via 104 | # matplotlib 105 | # mongomock 106 | # plotly 107 | palettable==3.3.3 108 | # via pymatgen 109 | pandas==2.3.0 110 | # via 111 | # maggma 112 | # pymatgen 113 | paramiko==3.5.1 114 | # via sshtunnel 115 | pillow==11.2.1 116 | # via matplotlib 117 | plotly==6.1.2 118 | # via pymatgen 119 | pybtex==0.24.0 120 | # via emmet-core 121 | pycparser==2.22 122 | # via cffi 123 | pydantic==2.11.5 124 | # via 125 | # emmet-core 126 | # maggma 127 | # pydantic-settings 128 | pydantic-core==2.33.2 129 | # via pydantic 130 | pydantic-settings==2.9.1 131 | # via 132 | # emmet-core 133 | # maggma 134 | pydash==8.0.5 135 | # via maggma 136 | pymatgen==2025.5.28 137 | # via 138 | # emmet-core 139 | # mp-api (pyproject.toml) 140 | pymongo==4.10.1 141 | # via maggma 142 | pynacl==1.5.0 143 | # via paramiko 144 | pyparsing==3.2.3 145 | # via 146 | # bibtexparser 147 | # matplotlib 148 | python-dateutil==2.9.0.post0 149 | # via 150 | # botocore 151 | # maggma 152 | # matplotlib 153 | # pandas 154 | python-dotenv==1.1.0 155 | # via pydantic-settings 156 | pytz==2025.2 157 | # via 158 | # mongomock 159 | # pandas 160 | pyyaml==6.0.2 161 | # via pybtex 162 | pyzmq==26.4.0 163 | # via maggma 164 | referencing==0.36.2 165 | # via 166 | # jsonschema 167 | # jsonschema-specifications 168 | requests==2.32.3 169 | # via 170 | # mp-api (pyproject.toml) 171 | # pymatgen 172 | rpds-py==0.25.1 173 | # via 174 | # jsonschema 175 | # referencing 176 | ruamel-yaml==0.18.12 177 | # via 178 | # maggma 179 | # monty 180 | # pymatgen 181 | ruamel-yaml-clib==0.2.12 182 | # via ruamel-yaml 183 | s3transfer==0.13.0 184 | # via boto3 185 | scipy==1.15.3 186 | # via pymatgen 187 | sentinels==1.0.0 188 | # via mongomock 189 | six==1.17.0 190 | # via 191 | # pybtex 192 | # python-dateutil 193 | smart-open==7.1.0 194 | # via mp-api (pyproject.toml) 195 | spglib==2.6.0 196 | # via pymatgen 197 | sshtunnel==0.4.0 198 | # via maggma 199 | sympy==1.14.0 200 | # via pymatgen 201 | tabulate==0.9.0 202 | # via pymatgen 203 | tqdm==4.67.1 204 | # via 205 | # maggma 206 | # pymatgen 207 | typing-extensions==4.14.0 208 | # via 209 | # emmet-core 210 | # mp-api (pyproject.toml) 211 | # pydantic 212 | # pydantic-core 213 | # pydash 214 | # referencing 215 | # spglib 216 | # typing-inspection 217 | typing-inspection==0.4.1 218 | # via 219 | # pydantic 220 | # pydantic-settings 221 | tzdata==2025.2 222 | # via pandas 223 | uncertainties==3.2.3 224 | # via pymatgen 225 | urllib3==2.4.0 226 | # via 227 | # botocore 228 | # requests 229 | wrapt==1.17.2 230 | # via smart-open 231 | 232 | # The following packages are considered to be unsafe in a requirements file: 233 | # setuptools 234 | -------------------------------------------------------------------------------- /test_files/Si_mp_149.cif: -------------------------------------------------------------------------------- 1 | # generated using pymatgen 2 | data_Si 3 | _symmetry_space_group_name_H-M 'P 1' 4 | _cell_length_a 5.46872800 5 | _cell_length_b 5.46872800 6 | _cell_length_c 5.46872800 7 | _cell_angle_alpha 90.00000000 8 | _cell_angle_beta 90.00000000 9 | _cell_angle_gamma 90.00000000 10 | _symmetry_Int_Tables_number 1 11 | _chemical_formula_structural Si 12 | _chemical_formula_sum Si8 13 | _cell_volume 163.55317139 14 | _cell_formula_units_Z 8 15 | loop_ 16 | _symmetry_equiv_pos_site_id 17 | _symmetry_equiv_pos_as_xyz 18 | 1 'x, y, z' 19 | loop_ 20 | _atom_site_type_symbol 21 | _atom_site_label 22 | _atom_site_symmetry_multiplicity 23 | _atom_site_fract_x 24 | _atom_site_fract_y 25 | _atom_site_fract_z 26 | _atom_site_occupancy 27 | Si Si0 1 0.25000000 0.75000000 0.25000000 1.0 28 | Si Si1 1 0.00000000 0.00000000 0.50000000 1.0 29 | Si Si2 1 0.25000000 0.25000000 0.75000000 1.0 30 | Si Si3 1 0.00000000 0.50000000 0.00000000 1.0 31 | Si Si4 1 0.75000000 0.75000000 0.75000000 1.0 32 | Si Si5 1 0.50000000 0.00000000 0.00000000 1.0 33 | Si Si6 1 0.75000000 0.25000000 0.25000000 1.0 34 | Si Si7 1 0.50000000 0.50000000 0.50000000 1.0 35 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/api/2b4626bbe73b9b69f3630c8931ea47657de40402/tests/__init__.py -------------------------------------------------------------------------------- /tests/materials/core_function.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import Callable, Any 3 | 4 | 5 | def client_search_testing( 6 | search_method: Callable, 7 | excluded_params: list[str], 8 | alt_name_dict: dict[str, str], 9 | custom_field_tests: dict[str, Any], 10 | sub_doc_fields: list[str], 11 | ): 12 | """ 13 | Function to test a client using its search method. 14 | Each parameter is used to query for data, which is then checked. 15 | 16 | Args: 17 | search_method (Callable): Client search method 18 | excluded_params (list[str]): List of parameters to exclude from testing 19 | alt_name_dict (dict[str, str]): Alternative names for parameters used in the projection and subsequent data checking 20 | custom_field_tests (dict[str, Any]): Custom queries for specific fields. 21 | sub_doc_fields (list[str]): Prefixes for fields to check in resulting data. Useful when data to be tested is nested. 22 | """ 23 | if search_method is not None: 24 | # Get list of parameters 25 | param_tuples = list(search_method.__annotations__.items()) 26 | 27 | # Query API for each numeric and boolean parameter and check if returned 28 | for entry in param_tuples: 29 | param = entry[0] 30 | 31 | if param not in excluded_params + ["return"]: 32 | param_type = entry[1] 33 | q = None 34 | 35 | if "tuple[int, int]" in param_type: 36 | project_field = alt_name_dict.get(param, None) 37 | q = { 38 | param: (-100, 100), 39 | "chunk_size": 1, 40 | "num_chunks": 1, 41 | } 42 | elif "tuple[float, float]" in param_type: 43 | project_field = alt_name_dict.get(param, None) 44 | q = { 45 | param: (-100.12, 100.12), 46 | "chunk_size": 1, 47 | "num_chunks": 1, 48 | } 49 | elif "bool" in param_type: 50 | project_field = alt_name_dict.get(param, None) 51 | q = { 52 | param: False, 53 | "chunk_size": 1, 54 | "num_chunks": 1, 55 | } 56 | elif param in custom_field_tests: 57 | project_field = alt_name_dict.get(param, None) 58 | q = { 59 | param: custom_field_tests[param], 60 | "chunk_size": 1, 61 | "num_chunks": 1, 62 | } 63 | 64 | if q is None: 65 | raise ValueError( 66 | f"Parameter '{param}' with type '{param_type}' was not " 67 | "properly identified in the generic search method test." 68 | ) 69 | 70 | doc = search_method(**q)[0].model_dump() 71 | 72 | for sub_field in sub_doc_fields: 73 | if sub_field in doc: 74 | doc = doc[sub_field] 75 | 76 | assert ( 77 | doc[project_field if project_field is not None else param] 78 | is not None 79 | ) 80 | -------------------------------------------------------------------------------- /tests/materials/test_bonds.py: -------------------------------------------------------------------------------- 1 | import os 2 | from core_function import client_search_testing 3 | import pytest 4 | from mp_api.client.routes.materials.bonds import BondsRester 5 | 6 | 7 | @pytest.fixture 8 | def rester(): 9 | rester = BondsRester() 10 | yield rester 11 | rester.session.close() 12 | 13 | 14 | excluded_params = [ 15 | "sort_fields", 16 | "chunk_size", 17 | "num_chunks", 18 | "all_fields", 19 | "fields", 20 | ] 21 | 22 | sub_doc_fields = [] # type: list 23 | 24 | alt_name_dict = { 25 | "material_ids": "material_id", 26 | "max_bond_length": "bond_length_stats", 27 | "min_bond_length": "bond_length_stats", 28 | "mean_bond_length": "bond_length_stats", 29 | } # type: dict 30 | 31 | custom_field_tests = { 32 | "material_ids": ["mp-149"], 33 | "coordination_envs": ["Mo-S(6)"], 34 | "coordination_envs_anonymous": ["A-B(6)"], 35 | } # type: dict 36 | 37 | 38 | @pytest.mark.skipif(os.getenv("MP_API_KEY", None) is None, reason="No API key found.") 39 | def test_client(rester): 40 | search_method = rester.search 41 | 42 | client_search_testing( 43 | search_method=search_method, 44 | excluded_params=excluded_params, 45 | alt_name_dict=alt_name_dict, 46 | custom_field_tests=custom_field_tests, 47 | sub_doc_fields=sub_doc_fields, 48 | ) 49 | -------------------------------------------------------------------------------- /tests/materials/test_chemenv.py: -------------------------------------------------------------------------------- 1 | import os 2 | from core_function import client_search_testing 3 | 4 | import pytest 5 | 6 | from mp_api.client.routes.materials.chemenv import ChemenvRester 7 | 8 | 9 | @pytest.fixture 10 | def rester(): 11 | rester = ChemenvRester() 12 | yield rester 13 | rester.session.close() 14 | 15 | 16 | excluded_params = [ 17 | "sort_fields", 18 | "chunk_size", 19 | "num_chunks", 20 | "all_fields", 21 | "fields", 22 | "volume", 23 | "csm", # temp until timeout fix 24 | ] 25 | 26 | sub_doc_fields = [] # type: list 27 | 28 | alt_name_dict = { 29 | "material_ids": "material_id", 30 | "exclude_elements": "material_id", 31 | "num_elements": "nelements", 32 | "num_sites": "nsites", 33 | } # type: dict 34 | 35 | custom_field_tests = { 36 | "material_ids": ["mp-22526"], 37 | "elements": ["Si", "O"], 38 | "exclude_elements": ["Si", "O"], 39 | "chemenv_symbol": ["S:1"], 40 | "chemenv_iupac": ["IC-12"], 41 | "chemenv_iucr": ["[2l]"], 42 | "chemenv_name": ["Octahedron"], 43 | "species": ["Cu2+"], 44 | } # type: dict 45 | 46 | 47 | @pytest.mark.skipif(os.getenv("MP_API_KEY", None) is None, reason="No API key found.") 48 | def test_client(rester): 49 | search_method = rester.search 50 | 51 | client_search_testing( 52 | search_method=search_method, 53 | excluded_params=excluded_params, 54 | alt_name_dict=alt_name_dict, 55 | custom_field_tests=custom_field_tests, 56 | sub_doc_fields=sub_doc_fields, 57 | ) 58 | -------------------------------------------------------------------------------- /tests/materials/test_dielectric.py: -------------------------------------------------------------------------------- 1 | import os 2 | from core_function import client_search_testing 3 | 4 | import pytest 5 | 6 | from mp_api.client.routes.materials.dielectric import DielectricRester 7 | 8 | 9 | @pytest.fixture 10 | def rester(): 11 | rester = DielectricRester() 12 | yield rester 13 | rester.session.close() 14 | 15 | 16 | excluded_params = [ 17 | "sort_fields", 18 | "chunk_size", 19 | "num_chunks", 20 | "all_fields", 21 | "fields", 22 | ] 23 | 24 | sub_doc_fields = [] # type: list 25 | 26 | alt_name_dict = { 27 | "e_static": "e_ionic", 28 | "material_ids": "material_id", 29 | } # type: dict 30 | 31 | custom_field_tests = { 32 | "material_ids": ["mp-149"], 33 | } # type: dict 34 | 35 | 36 | @pytest.mark.skipif(os.getenv("MP_API_KEY", None) is None, reason="No API key found.") 37 | def test_client(rester): 38 | search_method = rester.search 39 | 40 | client_search_testing( 41 | search_method=search_method, 42 | excluded_params=excluded_params, 43 | alt_name_dict=alt_name_dict, 44 | custom_field_tests=custom_field_tests, 45 | sub_doc_fields=sub_doc_fields, 46 | ) 47 | -------------------------------------------------------------------------------- /tests/materials/test_elasticity.py: -------------------------------------------------------------------------------- 1 | import os 2 | from core_function import client_search_testing 3 | 4 | import pytest 5 | 6 | from mp_api.client.routes.materials.elasticity import ElasticityRester 7 | 8 | 9 | @pytest.fixture 10 | def rester(): 11 | rester = ElasticityRester() 12 | yield rester 13 | rester.session.close() 14 | 15 | 16 | excluded_params = [ 17 | "sort_fields", 18 | "chunk_size", 19 | "num_chunks", 20 | "all_fields", 21 | "fields", 22 | ] 23 | 24 | sub_doc_fields = [] # type: list 25 | 26 | alt_name_dict = { 27 | "material_ids": "material_id", 28 | "elastic_anisotropy": "universal_anisotropy", 29 | "poisson_ratio": "homogeneous_poisson", 30 | "g_voigt": "bulk_modulus", 31 | "g_reuss": "bulk_modulus", 32 | "g_vrh": "bulk_modulus", 33 | "k_voigt": "shear_modulus", 34 | "k_reuss": "shear_modulus", 35 | "k_vrh": "shear_modulus", 36 | } # type: dict 37 | 38 | custom_field_tests = {"material_ids": ["mp-149"]} # type: dict 39 | 40 | 41 | @pytest.mark.skipif(os.getenv("MP_API_KEY", None) is None, reason="No API key found.") 42 | def test_client(rester): 43 | search_method = rester.search 44 | 45 | client_search_testing( 46 | search_method=search_method, 47 | excluded_params=excluded_params, 48 | alt_name_dict=alt_name_dict, 49 | custom_field_tests=custom_field_tests, 50 | sub_doc_fields=sub_doc_fields, 51 | ) 52 | -------------------------------------------------------------------------------- /tests/materials/test_electrodes.py: -------------------------------------------------------------------------------- 1 | import os 2 | from core_function import client_search_testing 3 | 4 | import pytest 5 | from pymatgen.core.periodic_table import Element 6 | 7 | from mp_api.client.routes.materials.electrodes import ElectrodeRester 8 | 9 | 10 | @pytest.fixture 11 | def rester(): 12 | rester = ElectrodeRester() 13 | yield rester 14 | rester.session.close() 15 | 16 | 17 | excluded_params = [ 18 | "sort_fields", 19 | "chunk_size", 20 | "num_chunks", 21 | "all_fields", 22 | "fields", 23 | ] 24 | 25 | sub_doc_fields = [] # type: list 26 | 27 | alt_name_dict = { 28 | "battery_ids": "battery_id", 29 | "formula": "battery_id", 30 | "exclude_elements": "battery_id", 31 | "num_elements": "nelements", 32 | "num_sites": "nsites", 33 | } # type: dict 34 | 35 | custom_field_tests = { 36 | "material_ids": ["mp-16722"], 37 | "battery_ids": ["mp-16722_Al"], 38 | "working_ion": Element("Li"), 39 | "formula": "CoO2", 40 | "chemsys": "Co-O", 41 | "elements": ["Co", "O"], 42 | "exclude_elements": ["Co"], 43 | } # type: dict 44 | 45 | 46 | @pytest.mark.skipif(os.getenv("MP_API_KEY", None) is None, reason="No API key found.") 47 | def test_client(rester): 48 | search_method = rester.search 49 | 50 | client_search_testing( 51 | search_method=search_method, 52 | excluded_params=excluded_params, 53 | alt_name_dict=alt_name_dict, 54 | custom_field_tests=custom_field_tests, 55 | sub_doc_fields=sub_doc_fields, 56 | ) 57 | -------------------------------------------------------------------------------- /tests/materials/test_electronic_structure.py: -------------------------------------------------------------------------------- 1 | import os 2 | from core_function import client_search_testing 3 | 4 | import pytest 5 | from pymatgen.analysis.magnetism import Ordering 6 | 7 | from mp_api.client.routes.materials.electronic_structure import ( 8 | BandStructureRester, 9 | DosRester, 10 | ElectronicStructureRester, 11 | ) 12 | 13 | 14 | @pytest.fixture 15 | def es_rester(): 16 | rester = ElectronicStructureRester() 17 | yield rester 18 | rester.session.close() 19 | 20 | 21 | es_excluded_params = [ 22 | "sort_fields", 23 | "chunk_size", 24 | "num_chunks", 25 | "all_fields", 26 | "fields", 27 | ] 28 | 29 | sub_doc_fields = [] # type: list 30 | 31 | es_alt_name_dict = { 32 | "material_ids": "material_id", 33 | "exclude_elements": "material_id", 34 | "formula": "material_id", 35 | "num_elements": "nelements", 36 | "num_sites": "nsites", 37 | } # type: dict 38 | 39 | es_custom_field_tests = { 40 | "material_ids": ["mp-149"], 41 | "magnetic_ordering": Ordering.FM, 42 | "formula": "CoO2", 43 | "chemsys": "Co-O", 44 | "elements": ["Co", "O"], 45 | "exclude_elements": ["Co"], 46 | } # type: dict 47 | 48 | 49 | @pytest.mark.skipif(os.getenv("MP_API_KEY", None) is None, reason="No API key found.") 50 | @pytest.mark.skip(reason="magnetic ordering fields not build correctly") 51 | def test_es_client(es_rester): 52 | search_method = es_rester.search 53 | 54 | client_search_testing( 55 | search_method=search_method, 56 | excluded_params=es_excluded_params, 57 | alt_name_dict=es_alt_name_dict, 58 | custom_field_tests=es_custom_field_tests, 59 | sub_doc_fields=sub_doc_fields, 60 | ) 61 | 62 | 63 | bs_custom_field_tests = { 64 | "magnetic_ordering": Ordering.FM, 65 | "is_metal": True, 66 | "is_gap_direct": True, 67 | "efermi": (0, 100), 68 | "band_gap": (0, 5), 69 | } 70 | 71 | bs_sub_doc_fields = ["bandstructure"] 72 | 73 | bs_alt_name_dict = {} # type: dict 74 | 75 | 76 | @pytest.fixture 77 | def bs_rester(): 78 | rester = BandStructureRester() 79 | yield rester 80 | rester.session.close() 81 | 82 | 83 | @pytest.mark.skipif(os.getenv("MP_API_KEY", None) is None, reason="No API key found.") 84 | @pytest.mark.skip(reason="magnetic ordering fields not build correctly") 85 | def test_bs_client(bs_rester): 86 | # Get specific search method 87 | search_method = bs_rester.search 88 | 89 | # Query fields 90 | for param in bs_custom_field_tests: 91 | project_field = bs_alt_name_dict.get(param, None) 92 | q = { 93 | param: bs_custom_field_tests[param], 94 | "chunk_size": 1, 95 | "num_chunks": 1, 96 | } 97 | doc = search_method(**q)[0].model_dump() 98 | 99 | for sub_field in bs_sub_doc_fields: 100 | if sub_field in doc: 101 | doc = doc[sub_field] 102 | 103 | if param != "path_type": 104 | doc = doc["setyawan_curtarolo"] 105 | 106 | assert doc[project_field if project_field is not None else param] is not None 107 | 108 | 109 | dos_custom_field_tests = { 110 | "magnetic_ordering": Ordering.FM, 111 | "efermi": (0, 100), 112 | "band_gap": (0, 5), 113 | } 114 | 115 | dos_excluded_params = ["orbital", "element"] 116 | 117 | dos_sub_doc_fields = ["dos"] 118 | 119 | dos_alt_name_dict = {} # type: dict 120 | 121 | 122 | @pytest.fixture 123 | def dos_rester(): 124 | rester = DosRester() 125 | yield rester 126 | rester.session.close() 127 | 128 | 129 | @pytest.mark.skipif(os.getenv("MP_API_KEY", None) is None, reason="No API key found.") 130 | @pytest.mark.skip(reason="magnetic ordering fields not build correctly") 131 | def test_dos_client(dos_rester): 132 | search_method = dos_rester.search 133 | 134 | # Query fields 135 | for param in dos_custom_field_tests: 136 | if param not in dos_excluded_params: 137 | project_field = dos_alt_name_dict.get(param, None) 138 | q = { 139 | param: dos_custom_field_tests[param], 140 | "chunk_size": 1, 141 | "num_chunks": 1, 142 | } 143 | doc = search_method(**q)[0].model_dump() 144 | for sub_field in dos_sub_doc_fields: 145 | if sub_field in doc: 146 | doc = doc[sub_field] 147 | 148 | if param != "projection_type" and param != "magnetic_ordering": 149 | doc = doc["total"]["1"] 150 | 151 | assert ( 152 | doc[project_field if project_field is not None else param] is not None 153 | ) 154 | -------------------------------------------------------------------------------- /tests/materials/test_eos.py: -------------------------------------------------------------------------------- 1 | import os 2 | from core_function import client_search_testing 3 | 4 | import pytest 5 | 6 | from mp_api.client.routes.materials.eos import EOSRester 7 | 8 | 9 | @pytest.fixture 10 | def rester(): 11 | rester = EOSRester() 12 | yield rester 13 | rester.session.close() 14 | 15 | 16 | excluded_params = [ 17 | "energies", 18 | "volumes", 19 | "sort_fields", 20 | "chunk_size", 21 | "num_chunks", 22 | "all_fields", 23 | "fields", 24 | ] 25 | 26 | sub_doc_fields = [] # type: list 27 | 28 | alt_name_dict = {"material_ids": "material_id"} # type: dict 29 | 30 | custom_field_tests = { # type: dict 31 | "material_ids": ["mp-149"] 32 | } 33 | 34 | 35 | @pytest.mark.skipif(os.getenv("MP_API_KEY", None) is None, reason="No API key found.") 36 | def test_client(rester): 37 | search_method = rester.search 38 | 39 | client_search_testing( 40 | search_method=search_method, 41 | excluded_params=excluded_params, 42 | alt_name_dict=alt_name_dict, 43 | custom_field_tests=custom_field_tests, 44 | sub_doc_fields=sub_doc_fields, 45 | ) 46 | -------------------------------------------------------------------------------- /tests/materials/test_grain_boundary.py: -------------------------------------------------------------------------------- 1 | import os 2 | from core_function import client_search_testing 3 | 4 | import pytest 5 | from emmet.core.grain_boundary import GBTypeEnum 6 | 7 | from mp_api.client.routes.materials.grain_boundaries import GrainBoundaryRester 8 | 9 | 10 | @pytest.fixture 11 | def rester(): 12 | rester = GrainBoundaryRester() 13 | yield rester 14 | rester.session.close() 15 | 16 | 17 | excluded_params = [ 18 | "sort_fields", 19 | "chunk_size", 20 | "num_chunks", 21 | "all_fields", 22 | "fields", 23 | ] 24 | 25 | sub_doc_fields = [] # type: list 26 | 27 | alt_name_dict = { 28 | "material_ids": "material_id", 29 | "separation_energy": "w_sep", 30 | } # type: dict 31 | 32 | custom_field_tests = { 33 | "material_ids": ["mp-81"], 34 | "gb_plane": ["1", "0", "0"], 35 | "rotation_axis": ["1", "0", "0"], 36 | "sigma": 5, 37 | "type": GBTypeEnum.twist, 38 | "chemsys": "Mo", 39 | "pretty_formula": "Mo", 40 | } # type: dict 41 | 42 | 43 | @pytest.mark.skipif(os.getenv("MP_API_KEY", None) is None, reason="No API key found.") 44 | def test_client(rester): 45 | search_method = rester.search 46 | 47 | client_search_testing( 48 | search_method=search_method, 49 | excluded_params=excluded_params, 50 | alt_name_dict=alt_name_dict, 51 | custom_field_tests=custom_field_tests, 52 | sub_doc_fields=sub_doc_fields, 53 | ) 54 | -------------------------------------------------------------------------------- /tests/materials/test_magnetism.py: -------------------------------------------------------------------------------- 1 | import os 2 | from core_function import client_search_testing 3 | 4 | import pytest 5 | from pymatgen.analysis.magnetism import Ordering 6 | 7 | from mp_api.client.routes.materials.magnetism import MagnetismRester 8 | 9 | 10 | @pytest.fixture 11 | def rester(): 12 | rester = MagnetismRester() 13 | yield rester 14 | rester.session.close() 15 | 16 | 17 | excluded_params = [ 18 | "sort_fields", 19 | "chunk_size", 20 | "num_chunks", 21 | "all_fields", 22 | "fields", 23 | ] 24 | 25 | sub_doc_fields = [] # type: list 26 | 27 | alt_name_dict = {"material_ids": "material_id"} # type: dict 28 | 29 | custom_field_tests = {"material_ids": ["mp-149"], "ordering": Ordering.FM} # type: dict 30 | 31 | 32 | @pytest.mark.skipif(os.getenv("MP_API_KEY", None) is None, reason="No API key found.") 33 | def test_client(rester): 34 | search_method = rester.search 35 | 36 | client_search_testing( 37 | search_method=search_method, 38 | excluded_params=excluded_params, 39 | alt_name_dict=alt_name_dict, 40 | custom_field_tests=custom_field_tests, 41 | sub_doc_fields=sub_doc_fields, 42 | ) 43 | -------------------------------------------------------------------------------- /tests/materials/test_materials.py: -------------------------------------------------------------------------------- 1 | import os 2 | from core_function import client_search_testing 3 | 4 | import pytest 5 | from emmet.core.symmetry import CrystalSystem 6 | 7 | from mp_api.client.routes.materials.materials import MaterialsRester 8 | 9 | 10 | @pytest.fixture 11 | def rester(): 12 | rester = MaterialsRester() 13 | yield rester 14 | rester.session.close() 15 | 16 | 17 | excluded_params = [ 18 | "sort_fields", 19 | "chunk_size", 20 | "num_chunks", 21 | "all_fields", 22 | "fields", 23 | "exclude_elements", # temp until server timeout increase 24 | "num_elements", # temp until server timeout increase 25 | "num_sites", # temp until server timeout increase 26 | "density", # temp until server timeout increase 27 | ] 28 | 29 | sub_doc_fields = [] # type: list 30 | 31 | alt_name_dict = { 32 | "material_ids": "material_id", 33 | "formula": "material_id", 34 | "crystal_system": "symmetry", 35 | "spacegroup_number": "symmetry", 36 | "spacegroup_symbol": "symmetry", 37 | "exclude_elements": "material_id", 38 | "num_elements": "nelements", 39 | "num_sites": "nsites", 40 | } # type: dict 41 | 42 | custom_field_tests = { 43 | "material_ids": ["mp-149"], 44 | "formula": "Si", 45 | "chemsys": "Si-P", 46 | "elements": ["Si", "P"], 47 | "task_ids": ["mp-149"], 48 | "crystal_system": CrystalSystem.cubic, 49 | "spacegroup_number": 38, 50 | "spacegroup_symbol": "Amm2", 51 | } # type: dict 52 | 53 | 54 | @pytest.mark.skipif(os.getenv("MP_API_KEY") is None, reason="No API key found.") 55 | def test_client(rester): 56 | search_method = rester.search 57 | 58 | client_search_testing( 59 | search_method=search_method, 60 | excluded_params=excluded_params, 61 | alt_name_dict=alt_name_dict, 62 | custom_field_tests=custom_field_tests, 63 | sub_doc_fields=sub_doc_fields, 64 | ) 65 | -------------------------------------------------------------------------------- /tests/materials/test_oxidation_states.py: -------------------------------------------------------------------------------- 1 | import os 2 | from core_function import client_search_testing 3 | 4 | import pytest 5 | 6 | from mp_api.client.routes.materials.oxidation_states import OxidationStatesRester 7 | 8 | 9 | @pytest.fixture 10 | def rester(): 11 | rester = OxidationStatesRester() 12 | yield rester 13 | rester.session.close() 14 | 15 | 16 | excluded_params = [ 17 | "sort_fields", 18 | "chunk_size", 19 | "num_chunks", 20 | "all_fields", 21 | "fields", 22 | ] 23 | 24 | sub_doc_fields = [] # type: list 25 | 26 | alt_name_dict = {"formula": "material_id", "material_ids": "material_id"} # type: dict 27 | 28 | custom_field_tests = { 29 | "material_ids": ["mp-149"], 30 | "formula": "Si", 31 | "chemsys": "Si-O", 32 | "possible_species": ["Cr2+", "O2-"], 33 | } # type: dict 34 | 35 | 36 | @pytest.mark.skipif(os.getenv("MP_API_KEY", None) is None, reason="No API key found.") 37 | def test_client(rester): 38 | search_method = rester.search 39 | 40 | client_search_testing( 41 | search_method=search_method, 42 | excluded_params=excluded_params, 43 | alt_name_dict=alt_name_dict, 44 | custom_field_tests=custom_field_tests, 45 | sub_doc_fields=sub_doc_fields, 46 | ) 47 | -------------------------------------------------------------------------------- /tests/materials/test_piezo.py: -------------------------------------------------------------------------------- 1 | import os 2 | from core_function import client_search_testing 3 | 4 | import pytest 5 | 6 | from mp_api.client.routes.materials.piezo import PiezoRester 7 | 8 | 9 | @pytest.fixture 10 | def rester(): 11 | rester = PiezoRester() 12 | yield rester 13 | rester.session.close() 14 | 15 | 16 | excluded_params = [ 17 | "sort_fields", 18 | "chunk_size", 19 | "num_chunks", 20 | "all_fields", 21 | "fields", 22 | ] 23 | 24 | sub_doc_fields = [] # type: list 25 | 26 | alt_name_dict = { 27 | "piezoelectric_modulus": "e_ij_max", 28 | "material_ids": "material_id", 29 | } # type: dict 30 | 31 | custom_field_tests = {"material_ids": ["mp-9900"]} # type: dict 32 | 33 | 34 | @pytest.mark.skipif(os.getenv("MP_API_KEY", None) is None, reason="No API key found.") 35 | def test_client(rester): 36 | search_method = rester.search 37 | 38 | client_search_testing( 39 | search_method=search_method, 40 | excluded_params=excluded_params, 41 | alt_name_dict=alt_name_dict, 42 | custom_field_tests=custom_field_tests, 43 | sub_doc_fields=sub_doc_fields, 44 | ) 45 | -------------------------------------------------------------------------------- /tests/materials/test_provenance.py: -------------------------------------------------------------------------------- 1 | import os 2 | from core_function import client_search_testing 3 | 4 | import pytest 5 | 6 | from mp_api.client.routes.materials.provenance import ProvenanceRester 7 | 8 | 9 | @pytest.fixture 10 | def rester(): 11 | rester = ProvenanceRester() 12 | yield rester 13 | rester.session.close() 14 | 15 | 16 | excluded_params = [ 17 | "sort_fields", 18 | "chunk_size", 19 | "num_chunks", 20 | "all_fields", 21 | "fields", 22 | ] 23 | 24 | sub_doc_fields = [] # type: list 25 | 26 | alt_name_dict = {"material_ids": "material_id"} # type: dict 27 | 28 | custom_field_tests = {"material_ids": ["mp-149"]} # type: dict 29 | 30 | 31 | @pytest.mark.skipif(os.getenv("MP_API_KEY", None) is None, reason="No API key found.") 32 | def test_client(rester): 33 | search_method = rester.search 34 | 35 | client_search_testing( 36 | search_method=search_method, 37 | excluded_params=excluded_params, 38 | alt_name_dict=alt_name_dict, 39 | custom_field_tests=custom_field_tests, 40 | sub_doc_fields=sub_doc_fields, 41 | ) 42 | -------------------------------------------------------------------------------- /tests/materials/test_robocrys.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | from mp_api.client.routes.materials.robocrys import RobocrysRester 6 | 7 | 8 | @pytest.fixture 9 | def rester(): 10 | rester = RobocrysRester() 11 | yield rester 12 | rester.session.close() 13 | 14 | 15 | @pytest.mark.skipif(os.getenv("MP_API_KEY", None) is None, reason="No API key found.") 16 | def test_client(rester): 17 | search_method = rester.search 18 | 19 | if search_method is not None: 20 | q = {"keywords": ["silicon"], "num_chunks": 1} 21 | 22 | doc = search_method(**q)[0] 23 | 24 | assert doc.description is not None 25 | assert doc.condensed_structure is not None 26 | -------------------------------------------------------------------------------- /tests/materials/test_substrates.py: -------------------------------------------------------------------------------- 1 | import os 2 | from core_function import client_search_testing 3 | 4 | import pytest 5 | 6 | from mp_api.client.routes.materials.substrates import SubstratesRester 7 | 8 | 9 | @pytest.fixture 10 | def rester(): 11 | rester = SubstratesRester() 12 | yield rester 13 | rester.session.close() 14 | 15 | 16 | excluded_params = [ 17 | "sort_fields", 18 | "chunk_size", 19 | "num_chunks", 20 | "all_fields", 21 | "fields", 22 | ] 23 | 24 | sub_doc_fields = [] # type: list 25 | 26 | alt_name_dict = { 27 | "substrate_id": "sub_id", 28 | "substrate_formula": "sub_form", 29 | "film_orientation": "film_orient", 30 | "substrate_orientation": "orient", 31 | } # type: dict 32 | 33 | custom_field_tests = { 34 | "film_id": "mp-1046", 35 | "substrate_id": "mp-804", 36 | "substrate_formula": "GaN", 37 | "film_orientation": [1, 0, 0], 38 | "substrate_orientation": [1, 0, 0], 39 | } # type: dict 40 | 41 | 42 | @pytest.mark.skipif(os.getenv("MP_API_KEY", None) is None, reason="No API key found.") 43 | def test_client(rester): 44 | search_method = rester.search 45 | 46 | client_search_testing( 47 | search_method=search_method, 48 | excluded_params=excluded_params, 49 | alt_name_dict=alt_name_dict, 50 | custom_field_tests=custom_field_tests, 51 | sub_doc_fields=sub_doc_fields, 52 | ) 53 | -------------------------------------------------------------------------------- /tests/materials/test_summary.py: -------------------------------------------------------------------------------- 1 | import os 2 | from core_function import client_search_testing 3 | 4 | import pytest 5 | from emmet.core.summary import HasProps 6 | from emmet.core.symmetry import CrystalSystem 7 | from pymatgen.analysis.magnetism import Ordering 8 | 9 | from mp_api.client.routes.materials.summary import SummaryRester 10 | 11 | excluded_params = [ 12 | "include_gnome", 13 | "sort_fields", 14 | "chunk_size", 15 | "num_chunks", 16 | "all_fields", 17 | "fields", 18 | ] 19 | 20 | alt_name_dict = { 21 | "material_ids": "material_id", 22 | "formula": "formula_pretty", 23 | "exclude_elements": "formula_pretty", 24 | "piezoelectric_modulus": "e_ij_max", 25 | "crystal_system": "symmetry", 26 | "spacegroup_symbol": "symmetry", 27 | "spacegroup_number": "symmetry", 28 | "total_energy": "energy_per_atom", 29 | "formation_energy": "formation_energy_per_atom", 30 | "uncorrected_energy": "uncorrected_energy_per_atom", 31 | "equilibrium_reaction_energy": "equilibrium_reaction_energy_per_atom", 32 | "magnetic_ordering": "ordering", 33 | "elastic_anisotropy": "universal_anisotropy", 34 | "poisson_ratio": "homogeneous_poisson", 35 | "g_reuss": "bulk_modulus", 36 | "g_vrh": "bulk_modulus", 37 | "g_voigt": "bulk_modulus", 38 | "k_reuss": "shear_modulus", 39 | "k_vrh": "shear_modulus", 40 | "k_voigt": "shear_modulus", 41 | "num_sites": "nsites", 42 | "num_elements": "nelements", 43 | "surface_energy_anisotropy": "surface_anisotropy", 44 | } # type: dict 45 | 46 | custom_field_tests = { 47 | "material_ids": ["mp-149"], 48 | "formula": "SiO2", 49 | "chemsys": "Si-O", 50 | "elements": ["Si", "O"], 51 | "exclude_elements": ["Si", "O"], 52 | "possible_species": ["O2-"], 53 | "crystal_system": CrystalSystem.cubic, 54 | "spacegroup_number": 38, 55 | "spacegroup_symbol": "Amm2", 56 | "has_props": [HasProps.dielectric], 57 | "theoretical": True, 58 | "has_reconstructed": False, 59 | "magnetic_ordering": Ordering.FM, 60 | "nelements": (8, 9), 61 | } # type: dict 62 | 63 | 64 | @pytest.mark.skipif(os.getenv("MP_API_KEY") is None, reason="No API key found.") 65 | def test_client(): 66 | search_method = SummaryRester().search 67 | 68 | client_search_testing( 69 | search_method=search_method, 70 | excluded_params=excluded_params, 71 | alt_name_dict=alt_name_dict, 72 | custom_field_tests=custom_field_tests, 73 | sub_doc_fields=[], 74 | ) 75 | -------------------------------------------------------------------------------- /tests/materials/test_surface_properties.py: -------------------------------------------------------------------------------- 1 | import os 2 | from core_function import client_search_testing 3 | 4 | import pytest 5 | 6 | from mp_api.client.routes.materials.surface_properties import SurfacePropertiesRester 7 | 8 | 9 | @pytest.fixture 10 | def rester(): 11 | rester = SurfacePropertiesRester() 12 | yield rester 13 | rester.session.close() 14 | 15 | 16 | excluded_params = [ 17 | "sort_fields", 18 | "chunk_size", 19 | "num_chunks", 20 | "all_fields", 21 | "fields", 22 | ] 23 | 24 | sub_doc_fields = [] # type: list 25 | 26 | alt_name_dict = { 27 | "surface_energy_anisotropy": "surface_anisotropy", 28 | "material_ids": "material_id", 29 | } # type: dict 30 | 31 | custom_field_tests = {"material_ids": ["mp-23152"]} # type: dict 32 | 33 | 34 | @pytest.mark.skipif(os.getenv("MP_API_KEY", None) is None, reason="No API key found.") 35 | def test_client(rester): 36 | search_method = rester.search 37 | 38 | client_search_testing( 39 | search_method=search_method, 40 | excluded_params=excluded_params, 41 | alt_name_dict=alt_name_dict, 42 | custom_field_tests=custom_field_tests, 43 | sub_doc_fields=sub_doc_fields, 44 | ) 45 | -------------------------------------------------------------------------------- /tests/materials/test_synthesis.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import List 3 | 4 | import pytest 5 | from emmet.core.synthesis import SynthesisRecipe, SynthesisTypeEnum 6 | 7 | from mp_api.client.routes.materials.synthesis import SynthesisRester 8 | 9 | 10 | @pytest.fixture 11 | def rester(): 12 | rester = SynthesisRester() 13 | yield rester 14 | rester.session.close() 15 | 16 | 17 | @pytest.mark.skipif(os.getenv("MP_API_KEY", None) is None, reason="No API key found.") 18 | def test_client(rester): 19 | search_method = rester.search 20 | 21 | if search_method is not None: 22 | q = {"keywords": ["silicon"]} 23 | 24 | doc = search_method(**q)[0] 25 | 26 | assert doc.doi is not None 27 | assert doc.paragraph_string is not None 28 | assert doc.synthesis_type is not None 29 | 30 | 31 | @pytest.mark.skipif(os.getenv("MP_API_KEY", None) is None, reason="No API key found.") 32 | def test_filters_keywords(rester): 33 | search_method = rester.search 34 | 35 | if search_method is not None: 36 | doc = search_method(keywords=["silicon"])[0] 37 | 38 | assert isinstance(doc.search_score, float) 39 | highlighted = sum((x["texts"] for x in doc.highlights), []) 40 | assert "silicon" in " ".join([x["value"] for x in highlighted]).lower() 41 | 42 | 43 | @pytest.mark.skipif(os.getenv("MP_API_KEY", None) is None, reason="No API key found.") 44 | def test_filters_synthesis_type(rester): 45 | search_method = rester.search 46 | 47 | if search_method is not None: 48 | doc = search_method( 49 | synthesis_type=[SynthesisTypeEnum.solid_state], num_chunks=1 50 | ) 51 | assert all(x.synthesis_type == SynthesisTypeEnum.solid_state for x in doc) 52 | 53 | doc = search_method(synthesis_type=[SynthesisTypeEnum.sol_gel], num_chunks=1) 54 | assert all(x.synthesis_type == SynthesisTypeEnum.sol_gel for x in doc) 55 | 56 | 57 | @pytest.mark.skipif(os.getenv("MP_API_KEY", None) is None, reason="No API key found.") 58 | @pytest.mark.xfail # Needs fixing 59 | def test_filters_temperature_range(rester): 60 | search_method = rester.search 61 | 62 | if search_method is not None: 63 | docs: List[SynthesisRecipe] = search_method( 64 | condition_heating_temperature_min=700, 65 | condition_heating_temperature_max=1000, 66 | num_chunks=5, 67 | ) 68 | for doc in docs: 69 | for op in doc.operations: 70 | for temp in op.conditions.heating_temperature: 71 | for val in temp.values: 72 | assert 700 <= val <= 1000 73 | 74 | 75 | @pytest.mark.skipif(os.getenv("MP_API_KEY", None) is None, reason="No API key found.") 76 | @pytest.mark.xfail # Needs fixing 77 | def test_filters_time_range(rester): 78 | search_method = rester.search 79 | 80 | if search_method is not None: 81 | docs: List[SynthesisRecipe] = search_method( 82 | condition_heating_time_min=7, condition_heating_time_max=11, num_chunks=5 83 | ) 84 | for doc in docs: 85 | for op in doc.operations: 86 | for temp in op.conditions.heating_time: 87 | for val in temp.values: 88 | assert 7 <= val <= 11 89 | 90 | 91 | @pytest.mark.skipif(os.getenv("MP_API_KEY", None) is None, reason="No API key found.") 92 | def test_filters_atmosphere(rester): 93 | search_method = rester.search 94 | 95 | if search_method is not None: 96 | docs: List[SynthesisRecipe] = search_method( 97 | condition_heating_atmosphere=["air", "O2"], 98 | num_chunks=5, 99 | ) 100 | for doc in docs: 101 | found = False 102 | for op in doc.operations: 103 | for atm in op.conditions.heating_atmosphere: 104 | if atm in ["air", "O2"]: 105 | found = True 106 | assert found 107 | 108 | 109 | @pytest.mark.skipif(os.getenv("MP_API_KEY", None) is None, reason="No API key found.") 110 | def test_filters_mixing_device(rester): 111 | search_method = rester.search 112 | 113 | if search_method is not None: 114 | docs: List[SynthesisRecipe] = search_method( 115 | condition_mixing_device=["zirconia", "Al2O3"], 116 | num_chunks=5, 117 | ) 118 | for doc in docs: 119 | found = False 120 | for op in doc.operations: 121 | if op.conditions.mixing_device in ["zirconia", "Al2O3"]: 122 | found = True 123 | assert found 124 | 125 | 126 | @pytest.mark.skipif(os.getenv("MP_API_KEY", None) is None, reason="No API key found.") 127 | def test_filters_mixing_media(rester): 128 | search_method = rester.search 129 | 130 | if search_method is not None: 131 | docs: List[SynthesisRecipe] = search_method( 132 | condition_mixing_media=["water", "alcohol"], 133 | num_chunks=5, 134 | ) 135 | for doc in docs: 136 | found = False 137 | for op in doc.operations: 138 | if op.conditions.mixing_media in ["water", "alcohol"]: 139 | found = True 140 | assert found 141 | -------------------------------------------------------------------------------- /tests/materials/test_tasks.py: -------------------------------------------------------------------------------- 1 | import os 2 | from core_function import client_search_testing 3 | from datetime import datetime 4 | import pytest 5 | 6 | from mp_api.client.routes.materials.tasks import TaskRester 7 | 8 | 9 | @pytest.fixture 10 | def rester(): 11 | rester = TaskRester(monty_decode=False) 12 | yield rester 13 | rester.session.close() 14 | 15 | 16 | excluded_params = [ 17 | "sort_fields", 18 | "chunk_size", 19 | "num_chunks", 20 | "all_fields", 21 | "fields", 22 | "formula", 23 | "elements", 24 | "exclude_elements", 25 | ] 26 | 27 | sub_doc_fields = [] # type: list 28 | 29 | alt_name_dict = { 30 | "formula": "task_id", 31 | "task_ids": "task_id", 32 | "exclude_elements": "task_id", 33 | } # type: dict 34 | 35 | custom_field_tests = { 36 | "chemsys": "Si-O", 37 | "last_updated": (None, datetime.utcnow()), 38 | "task_ids": ["mp-149"], 39 | } # type: dict 40 | 41 | 42 | @pytest.mark.skipif(os.getenv("MP_API_KEY") is None, reason="No API key found.") 43 | def test_client(rester): 44 | search_method = rester.search 45 | 46 | client_search_testing( 47 | search_method=search_method, 48 | excluded_params=excluded_params, 49 | alt_name_dict=alt_name_dict, 50 | custom_field_tests=custom_field_tests, 51 | sub_doc_fields=sub_doc_fields, 52 | ) 53 | 54 | 55 | def test_get_trajectories(rester): 56 | trajectories = [traj for traj in rester.get_trajectory("mp-149")] 57 | 58 | for traj in trajectories: 59 | assert ("@module", "pymatgen.core.trajectory") in traj.items() 60 | -------------------------------------------------------------------------------- /tests/materials/test_thermo.py: -------------------------------------------------------------------------------- 1 | import os 2 | from core_function import client_search_testing 3 | 4 | import pytest 5 | from emmet.core.thermo import ThermoType 6 | from pymatgen.analysis.phase_diagram import PhaseDiagram 7 | 8 | from mp_api.client.routes.materials.thermo import ThermoRester 9 | 10 | 11 | @pytest.fixture 12 | def rester(): 13 | rester = ThermoRester() 14 | yield rester 15 | rester.session.close() 16 | 17 | 18 | excluded_params = [ 19 | "sort_fields", 20 | "chunk_size", 21 | "num_chunks", 22 | "all_fields", 23 | "fields", 24 | "equilibrium_reaction_energy", 25 | ] 26 | 27 | sub_doc_fields = [] # type: list 28 | 29 | alt_name_dict = { 30 | "formula": "formula_pretty", 31 | "material_ids": "material_id", 32 | "thermo_ids": "thermo_id", 33 | "thermo_types": "thermo_type", 34 | "total_energy": "energy_per_atom", 35 | "formation_energy": "formation_energy_per_atom", 36 | "uncorrected_energy": "uncorrected_energy_per_atom", 37 | "equilibrium_reaction_energy": "equilibrium_reaction_energy_per_atom", 38 | "num_elements": "nelements", 39 | "num_sites": "nsites", 40 | } # type: dict 41 | 42 | custom_field_tests = { 43 | "material_ids": ["mp-149"], 44 | "thermo_ids": ["mp-149_GGA_GGA+U"], 45 | "thermo_types": [ThermoType.GGA_GGA_U], 46 | "formula": "SiO2", 47 | "chemsys": "Si-O", 48 | } # type: dict 49 | 50 | 51 | @pytest.mark.skipif(os.getenv("MP_API_KEY") is None, reason="No API key found.") 52 | def test_client(rester): 53 | search_method = rester.search 54 | 55 | client_search_testing( 56 | search_method=search_method, 57 | excluded_params=excluded_params, 58 | alt_name_dict=alt_name_dict, 59 | custom_field_tests=custom_field_tests, 60 | sub_doc_fields=sub_doc_fields, 61 | ) 62 | 63 | 64 | def test_get_phase_diagram_from_chemsys(): 65 | # Test that a phase diagram is returned 66 | 67 | assert isinstance( 68 | ThermoRester().get_phase_diagram_from_chemsys("Hf-Pm", thermo_type="GGA_GGA+U"), 69 | PhaseDiagram, 70 | ) 71 | -------------------------------------------------------------------------------- /tests/materials/test_xas.py: -------------------------------------------------------------------------------- 1 | import os 2 | from core_function import client_search_testing 3 | 4 | import pytest 5 | from emmet.core.xas import Edge, Type 6 | from pymatgen.core.periodic_table import Element 7 | 8 | from mp_api.client.routes.materials.xas import XASRester 9 | 10 | 11 | @pytest.fixture 12 | def rester(): 13 | rester = XASRester() 14 | yield rester 15 | rester.session.close() 16 | 17 | 18 | excluded_params = [ 19 | "sort_fields", 20 | "chunk_size", 21 | "num_chunks", 22 | "all_fields", 23 | "fields", 24 | "material_ids", 25 | ] 26 | 27 | sub_doc_fields = [] # type: list 28 | 29 | alt_name_dict = { 30 | "required_elements": "elements", 31 | "formula": "formula_pretty", 32 | "exclude_elements": "material_id", 33 | } # type: dict 34 | 35 | custom_field_tests = { 36 | "edge": Edge.L2_3, 37 | "spectrum_type": Type.EXAFS, 38 | "absorbing_element": Element("Ce"), 39 | "required_elements": [Element("Ce")], 40 | "formula": "Ce(WO4)2", 41 | "chemsys": "Ce-O-W", 42 | "elements": ["Ce"], 43 | } # type: dict 44 | 45 | 46 | @pytest.mark.skip(reason="Temp skip until timeout update.") 47 | @pytest.mark.skipif(os.getenv("MP_API_KEY") is None, reason="No API key found.") 48 | def test_client(rester): 49 | search_method = rester.search 50 | 51 | client_search_testing( 52 | search_method=search_method, 53 | excluded_params=excluded_params, 54 | alt_name_dict=alt_name_dict, 55 | custom_field_tests=custom_field_tests, 56 | sub_doc_fields=sub_doc_fields, 57 | ) 58 | -------------------------------------------------------------------------------- /tests/molecules/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/api/2b4626bbe73b9b69f3630c8931ea47657de40402/tests/molecules/__init__.py -------------------------------------------------------------------------------- /tests/molecules/core_function.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import Callable, Any 3 | 4 | 5 | def client_search_testing( 6 | search_method: Callable, 7 | excluded_params: list[str], 8 | alt_name_dict: dict[str, str], 9 | custom_field_tests: dict[str, Any], 10 | sub_doc_fields: list[str], 11 | ): 12 | """ 13 | Function to test a client using its search method. 14 | Each parameter is used to query for data, which is then checked. 15 | 16 | Args: 17 | search_method (Callable): Client search method 18 | excluded_params (list[str]): List of parameters to exclude from testing 19 | alt_name_dict (dict[str, str]): Alternative names for parameters used in the projection and subsequent data checking 20 | custom_field_tests (dict[str, Any]): Custom queries for specific fields. 21 | sub_doc_fields (list[str]): Prefixes for fields to check in resulting data. Useful when data to be tested is nested. 22 | """ 23 | if search_method is not None: 24 | # Get list of parameters 25 | param_tuples = list(search_method.__annotations__.items()) 26 | # Query API for each numeric and boolean parameter and check if returned 27 | for entry in param_tuples: 28 | param = entry[0] 29 | if param not in excluded_params: 30 | param_type = entry[1] 31 | q = None 32 | 33 | if param in custom_field_tests: 34 | q = { 35 | param: custom_field_tests[param], 36 | "chunk_size": 1, 37 | "num_chunks": 1, 38 | } 39 | elif "tuple[int, int]" in param_type: 40 | q = { 41 | param: (-100, 100), 42 | "chunk_size": 1, 43 | "num_chunks": 1, 44 | } 45 | elif "tuple[float, float]" in param_type: 46 | q = { 47 | param: (-3000.12, 3000.12), 48 | "chunk_size": 1, 49 | "num_chunks": 1, 50 | } 51 | elif param_type is bool: 52 | q = { 53 | param: False, 54 | "chunk_size": 1, 55 | "num_chunks": 1, 56 | } 57 | 58 | docs = search_method(**q) 59 | 60 | if len(docs) > 0: 61 | doc = docs[0].model_dump() 62 | else: 63 | raise ValueError("No documents returned") 64 | 65 | assert doc[alt_name_dict.get(param, param)] is not None 66 | -------------------------------------------------------------------------------- /tests/molecules/test_jcesr.py: -------------------------------------------------------------------------------- 1 | import os 2 | from .core_function import client_search_testing 3 | 4 | import pytest 5 | from pymatgen.core.periodic_table import Element 6 | 7 | from mp_api.client.routes.molecules.jcesr import JcesrMoleculesRester 8 | 9 | 10 | @pytest.fixture 11 | def rester(): 12 | rester = JcesrMoleculesRester() 13 | yield rester 14 | rester.session.close() 15 | 16 | 17 | excluded_params = [ 18 | "sort_fields", 19 | "chunk_size", 20 | "num_chunks", 21 | "all_fields", 22 | "fields", 23 | "charge", 24 | ] 25 | 26 | sub_doc_fields = [] # type: list 27 | 28 | alt_name_dict = {"task_ids": "task_id"} # type: dict 29 | 30 | custom_field_tests = { 31 | "task_ids": ["mol-45228"], 32 | "elements": [Element("H")], 33 | "pointgroup": "C1", 34 | "smiles": "C#CC(=C)C.CNCCNC", 35 | } # type: dict 36 | 37 | 38 | @pytest.mark.skipif(os.getenv("MP_API_KEY") is None, reason="No API key found.") 39 | def test_client(rester): 40 | search_method = rester.search 41 | 42 | client_search_testing( 43 | search_method=search_method, 44 | excluded_params=excluded_params, 45 | alt_name_dict=alt_name_dict, 46 | custom_field_tests=custom_field_tests, 47 | sub_doc_fields=sub_doc_fields, 48 | ) 49 | -------------------------------------------------------------------------------- /tests/molecules/test_summary.py: -------------------------------------------------------------------------------- 1 | import os 2 | from .core_function import client_search_testing 3 | 4 | import pytest 5 | from emmet.core.molecules.summary import HasProps 6 | 7 | from mp_api.client.routes.molecules.summary import MoleculesSummaryRester 8 | 9 | excluded_params = [ 10 | "sort_fields", 11 | "chunk_size", 12 | "num_chunks", 13 | "all_fields", 14 | "fields", 15 | "exclude_elements", 16 | "has_props", 17 | ] 18 | 19 | alt_name_dict = {"formula": "formula_alphabetical", "molecule_ids": "molecule_id"} 20 | 21 | custom_field_tests = { 22 | "molecule_ids": ["351ef090ebd90b661a4e1205756f6957-C1Mg1N2O1S1-m2-1"], 23 | "formula": "C2 H4", 24 | "chemsys": "C-H", 25 | "elements": ["P"], 26 | "has_solvent": "DIELECTRIC=18,500;N=1,415;ALPHA=0,000;BETA=0,735;GAMMA=20,200;PHI=0,000;PSI=0,000", 27 | "has_level_of_theory": "wB97X-V/def2-TZVPPD/SMD", 28 | "has_lot_solvent": "wB97X-V/def2-TZVPPD/SMD(SOLVENT=THF)", 29 | "nelements": 2, 30 | "charge": 1, 31 | "spin_multiplicity": 1, 32 | "has_props": [HasProps.orbitals], 33 | } # type: dict 34 | 35 | 36 | @pytest.mark.skip(reason="Temporary until data adjustments") 37 | @pytest.mark.skipif(os.getenv("MP_API_KEY") is None, reason="No API key found.") 38 | def test_client(): 39 | search_method = MoleculesSummaryRester().search 40 | 41 | client_search_testing( 42 | search_method=search_method, 43 | excluded_params=excluded_params, 44 | alt_name_dict=alt_name_dict, 45 | custom_field_tests=custom_field_tests, 46 | sub_doc_fields=[], 47 | ) 48 | -------------------------------------------------------------------------------- /tests/test_client.py: -------------------------------------------------------------------------------- 1 | import os 2 | import warnings 3 | 4 | import pytest 5 | 6 | from mp_api.client import MPRester 7 | from mp_api.client.routes.materials import TaskRester, ProvenanceRester 8 | 9 | # -- Rester name data for generic tests 10 | 11 | key_only_resters = { 12 | "materials_phonon": "mp-11703", 13 | "materials_similarity": "mp-149", 14 | "doi": "mp-149", 15 | "materials_wulff": "mp-149", 16 | "materials_charge_density": "mp-1936745", 17 | "materials_provenance": "mp-149", 18 | "materials_robocrys": "mp-1025395", 19 | } 20 | 21 | search_only_resters = [ 22 | "materials_grain_boundaries", 23 | "materials_electronic_structure_bandstructure", 24 | "materials_electronic_structure_dos", 25 | "materials_substrates", 26 | "materials_synthesis", 27 | "materials_similarity", 28 | ] 29 | 30 | special_resters = ["materials_charge_density", "doi"] 31 | 32 | ignore_generic = [ 33 | "_user_settings", 34 | "_general_store", 35 | "_messages", 36 | # "tasks", 37 | # "bonds", 38 | "materials_xas", 39 | "materials_elasticity", 40 | "materials_fermi", 41 | "materials_alloys", 42 | # "summary", 43 | ] # temp 44 | 45 | 46 | mpr = MPRester() 47 | 48 | # Temporarily ignore molecules resters while molecules query operators are changed 49 | resters_to_test = [ 50 | rester for rester in mpr._all_resters if "molecule" not in rester.suffix 51 | ] 52 | 53 | 54 | @pytest.mark.skipif(os.getenv("MP_API_KEY") is None, reason="No API key found.") 55 | @pytest.mark.parametrize("rester", resters_to_test) 56 | def test_generic_get_methods(rester): 57 | # -- Test generic search and get_data_by_id methods 58 | name = rester.suffix.replace("/", "_") 59 | 60 | rester = rester( 61 | endpoint=mpr.endpoint, 62 | include_user_agent=True, 63 | session=mpr.session, 64 | # Disable monty decode on nested data which may give errors 65 | monty_decode=rester not in [TaskRester, ProvenanceRester], 66 | use_document_model=True, 67 | ) 68 | 69 | if name not in ignore_generic: 70 | key = rester.primary_key 71 | if name not in key_only_resters: 72 | if key not in rester.available_fields: 73 | key = rester.available_fields[0] 74 | 75 | doc = rester._query_resource_data({"_limit": 1}, fields=[key])[0] 76 | assert isinstance(doc, rester.document_model) 77 | 78 | if name not in search_only_resters: 79 | doc = rester.get_data_by_id(doc.model_dump()[key], fields=[key]) 80 | assert isinstance(doc, rester.document_model) 81 | 82 | elif name not in special_resters: 83 | doc = rester.get_data_by_id(key_only_resters[name], fields=[key]) 84 | assert isinstance(doc, rester.document_model) 85 | -------------------------------------------------------------------------------- /tests/test_core_client.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | from mp_api.client import MPRester 6 | from mp_api.client.core import BaseRester 7 | 8 | 9 | @pytest.fixture 10 | def rester(): 11 | rester = BaseRester() 12 | yield rester 13 | rester.session.close() 14 | 15 | 16 | @pytest.fixture() 17 | def mpr(): 18 | rester = MPRester() 19 | yield rester 20 | rester.session.close() 21 | 22 | 23 | @pytest.mark.skipif(os.getenv("MP_API_KEY", None) is None, reason="No API key found.") 24 | @pytest.mark.xfail 25 | def test_post_fail(rester): 26 | rester._post_resource({}, suburl="materials/find_structure") 27 | 28 | 29 | @pytest.mark.skipif(os.getenv("MP_API_KEY", None) is None, reason="No API key found.") 30 | def test_pagination(mpr): 31 | mpids = mpr.materials.search( 32 | all_fields=False, fields=["material_id"], num_chunks=2, chunk_size=1000 33 | ) 34 | assert len(mpids) > 1000 35 | 36 | 37 | @pytest.mark.skipif(os.getenv("MP_API_KEY", None) is None, reason="No API key found.") 38 | def test_count(mpr): 39 | count = mpr.materials.count( 40 | dict(task_ids="mp-149", _all_fields=False, _fields="material_id") 41 | ) 42 | assert count == 1 43 | 44 | 45 | def test_available_fields(rester, mpr): 46 | assert len(mpr.materials.available_fields) > 0 47 | assert rester.available_fields == ["Unknown fields."] 48 | --------------------------------------------------------------------------------