diff --git a/README.md b/README.md index a946e26..cbf601d 100644 --- a/README.md +++ b/README.md @@ -8,63 +8,144 @@ Python wrapper for FPBase.org GraphQL API. -See https://www.fpbase.org/graphql for full documentation on the graphql schema and an interactive playground. - -This library provides simple Python access to commonly-accessed data. +See for full documentation on the graphql schema and an interactive playground. ## Installation -``` +```sh pip install fpbase ``` -## Usage +## API + +See all response model types in `fpbase.models`. + +### Functions that return a single object + +* `fpbase.get_fluorophore` (can return either protein or dye) +* `fpbase.get_protein` (will only return proteins) +* `fpbase.get_microscope` +* `fpbase.get_filter` +* `fpbase.get_light_source` +* `fpbase.get_camera` + +### Functions that return a list of available object keys + +* `fpbase.list_fluorophores` (includes both proteins and dyes) +* `fpbase.list_proteins` +* `fpbase.list_dyes` +* `fpbase.list_microscopes` +* `fpbase.list_filters` +* `fpbase.list_light_sources` +* `fpbase.list_cameras` + +### Other + +* `fpbase.graphql_query` : Send generic GraphQL query to FPbase (see for full documentation on the graphql schema and an interactive playground) + +## Example Usage ```python -In [1]: from fpbase import get_fluorophore, get_microscope +import fpbase -In [2]: print(get_fluorophore("mCherry")) -Fluorophore( +fpbase.get_fluorophore("mCherry") +# NOTE: you can also use `get_protein()` if you want to ensure that the +# fluorophore is a protein and the response is an `fpbase.models.Protein` +``` + +
+ +output + +```python +Protein( name='mCherry', id='ZERB6', - states=[ - State( - id=336, - exMax=587.0, - emMax=610.0, - emhex='#f70000', - exhex='#ff4600', - extCoeff=72000.0, - qy=0.22, - spectra=[Spectrum(subtype='EX'), Spectrum(subtype='EM'), Spectrum(subtype='A_2P')], - lifetime=1.4 - ) - ], - defaultState=336 + default_state=State( + id=336, + name='mCherry', + exMax=587.0, + emMax=610.0, + emhex='#f70000', + exhex='#ff4600', + ext_coeff=72000.0, + qy=0.22, + spectra=[ + Spectrum(id=79, subtype='EX', owner_filter=None, owner_camera=None, owner_light=None), + Spectrum(id=80, subtype='EM', owner_filter=None, owner_camera=None, owner_light=None), + Spectrum(id=158, subtype='A_2P', owner_filter=None, owner_camera=None, owner_light=None) + ], + lifetime=1.4 + ), + seq='MVSKGEEDNMAIIKEFMRFKVHMEGSVNGHEFEIEGEGEGRPYEGTQTAKLKVTKGGPLPFAWDILSPQFMYGSKAYVKHPADIPDYLKLSFPEGFKWERVMNFEDGGVVTVTQDSSLQDGEFIYKVKLRGTNFPSDGPVMQKKTMGWEASSERMYPEDGALKGEIKQRLKLKDGGHYDAEVKTTYKAKKPVQLPGAYNVNIKLDITSHNEDYTIVEQYERAEGRHSTGGMDELYK', + pdb=['2H5Q'], + genbank='AAV52164', + uniprot='X5DSL3', + agg=, + switch_type=, + primary_reference=Reference(doi='10.1038/nbt1037', url='https://doi.org/10.1038/nbt1037'), + references=[ + Reference(doi='10.1038/nbt1037', url='https://doi.org/10.1038/nbt1037'), + Reference(doi='10.1021/bi060773l', url='https://doi.org/10.1021/bi060773l'), + Reference(doi='10.1038/nmeth1062', url='https://doi.org/10.1038/nmeth1062'), + Reference(doi='10.1038/nmeth.1596', url='https://doi.org/10.1038/nmeth.1596'), + Reference(doi='10.1038/nmeth.1955', url='https://doi.org/10.1038/nmeth.1955'), + Reference(doi='10.1091/mbc.e16-01-0063', url='https://doi.org/10.1091/mbc.e16-01-0063'), + Reference(doi='10.1038/s41598-017-12212-x', url='https://doi.org/10.1038/s41598-017-12212-x'), + Reference(doi='10.1038/s41598-018-28858-0', url='https://doi.org/10.1038/s41598-018-28858-0'), + Reference(doi='10.1002/pld3.112', url='https://doi.org/10.1002/pld3.112'), + Reference(doi='10.1371/journal.pone.0219886', url='https://doi.org/10.1371/journal.pone.0219886'), + Reference(doi='10.1073/pnas.2017379117', url='https://doi.org/10.1073/pnas.2017379117'), + Reference(doi='10.1038/s41467-023-40647-6', url='https://doi.org/10.1038/s41467-023-40647-6') + ] ) +``` + +
+ +```python +fpbase.get_fluorophore("DAPI") +``` -In [3]: print(get_fluorophore("DAPI")) +
+ +output + +```python Fluorophore( name='DAPI', id='15', - states=[ - State( - id=15, - exMax=359.0, - emMax=461.0, - emhex='', - exhex='', - extCoeff=None, - qy=None, - spectra=[Spectrum(subtype='AB'), Spectrum(subtype='EX'), Spectrum(subtype='EM')], - lifetime=None - ) - ], - defaultState=None + default_state=State( + id=15, + name='DAPI', + exMax=359.0, + emMax=461.0, + emhex='', + exhex='', + ext_coeff=None, + qy=None, + spectra=[ + Spectrum(id=7754, subtype='AB', owner_filter=None, owner_camera=None, owner_light=None), + Spectrum(id=222, subtype='EX', owner_filter=None, owner_camera=None, owner_light=None), + Spectrum(id=223, subtype='EM', owner_filter=None, owner_camera=None, owner_light=None) + ], + lifetime=None + ) ) +``` + +
+ +```python +# fetch info for +fpbase.get_microscope("i6WL2W") +``` -# fetch info for https://www.fpbase.org/microscope/i6WL2W/ -In [4]: print(get_microscope("i6WL2W")) +
+ +output + +```python Microscope( id='i6WL2WdgcDMgJYtPrpZcaJ', name='Example Widefield (Sedat)', @@ -72,102 +153,356 @@ Microscope( OpticalConfig( name='Widefield Blue', filters=[ - FilterPlacement(name='Chroma ET395/25x', spectrum=Spectrum(subtype='BX'), path='EX', reflects=False), - FilterPlacement(name='Chroma T425lpxr', spectrum=Spectrum(subtype='LP'), path='BS', reflects=False), - FilterPlacement(name='Chroma ET460/50m', spectrum=Spectrum(subtype='BM'), path='EM', reflects=False) + FilterPlacement( + path='EX', + filter=Filter( + id=52, + name='Chroma ET395/25x', + spectrum=Spectrum(id=375, subtype='BX', owner_filter=None, owner_camera=None, owner_light=None), + manufacturer='', + bandcenter=None, + bandwidth=None, + edge=None + ), + reflects=False + ), + FilterPlacement( + path='BS', + filter=Filter(id=10, name='Chroma T425lpxr', spectrum=Spectrum(id=333, subtype='LP', owner_filter=None, owner_camera=None, owner_light=None), manufacturer='', bandcenter=None, bandwidth=None, edge=None), + reflects=False + ), + FilterPlacement( + path='EM', + filter=Filter( + id=47, + name='Chroma ET460/50m', + spectrum=Spectrum(id=370, subtype='BM', owner_filter=None, owner_camera=None, owner_light=None), + manufacturer='', + bandcenter=None, + bandwidth=None, + edge=None + ), + reflects=False + ) ], - camera=SpectrumOwner(name='Andor Zyla 4.2 PLUS', spectrum=Spectrum(subtype='QE')), - light=SpectrumOwner(name='SOLA 395', spectrum=Spectrum(subtype='PD')), + camera=Camera(id=4, name='Andor Zyla 4.2 PLUS', spectrum=Spectrum(id=1328, subtype='QE', owner_filter=None, owner_camera=None, owner_light=None), manufacturer=''), + light=LightSource(id=9, name='SOLA 395', spectrum=Spectrum(id=394, subtype='PD', owner_filter=None, owner_camera=None, owner_light=None), manufacturer=''), laser=None ), OpticalConfig( name='Widefield Dual FRET', filters=[ - FilterPlacement(name='Lumencor 470/24x', spectrum=Spectrum(subtype='BX'), path='EX', reflects=False), - FilterPlacement(name='Chroma 59022bs', spectrum=Spectrum(subtype='BS'), path='BS', reflects=False), - FilterPlacement(name='Semrock FF02-641/75', spectrum=Spectrum(subtype='BP'), path='EM', reflects=False) + FilterPlacement( + path='EX', + filter=Filter( + id=63, + name='Lumencor 470/24x', + spectrum=Spectrum(id=399, subtype='BX', owner_filter=None, owner_camera=None, owner_light=None), + manufacturer='', + bandcenter=None, + bandwidth=None, + edge=None + ), + reflects=False + ), + FilterPlacement( + path='BS', + filter=Filter(id=62, name='Chroma 59022bs', spectrum=Spectrum(id=385, subtype='BS', owner_filter=None, owner_camera=None, owner_light=None), manufacturer='', bandcenter=None, bandwidth=None, edge=None), + reflects=False + ), + FilterPlacement( + path='EM', + filter=Filter( + id=689, + name='Semrock FF02-641/75', + spectrum=Spectrum(id=1025, subtype='BP', owner_filter=None, owner_camera=None, owner_light=None), + manufacturer='', + bandcenter=None, + bandwidth=None, + edge=None + ), + reflects=False + ) ], - camera=SpectrumOwner(name='Andor Zyla 4.2 PLUS', spectrum=Spectrum(subtype='QE')), - light=SpectrumOwner(name='SOLA 395', spectrum=Spectrum(subtype='PD')), + camera=Camera(id=4, name='Andor Zyla 4.2 PLUS', spectrum=Spectrum(id=1328, subtype='QE', owner_filter=None, owner_camera=None, owner_light=None), manufacturer=''), + light=LightSource(id=9, name='SOLA 395', spectrum=Spectrum(id=394, subtype='PD', owner_filter=None, owner_camera=None, owner_light=None), manufacturer=''), laser=None ), OpticalConfig( name='Widefield Dual Green', filters=[ - FilterPlacement(name='Lumencor 470/24x', spectrum=Spectrum(subtype='BX'), path='EX', reflects=False), - FilterPlacement(name='Chroma 59022bs', spectrum=Spectrum(subtype='BS'), path='BS', reflects=False), - FilterPlacement(name='Semrock FF03-525/50', spectrum=Spectrum(subtype='BP'), path='EM', reflects=False) + FilterPlacement( + path='EX', + filter=Filter( + id=63, + name='Lumencor 470/24x', + spectrum=Spectrum(id=399, subtype='BX', owner_filter=None, owner_camera=None, owner_light=None), + manufacturer='', + bandcenter=None, + bandwidth=None, + edge=None + ), + reflects=False + ), + FilterPlacement( + path='BS', + filter=Filter(id=62, name='Chroma 59022bs', spectrum=Spectrum(id=385, subtype='BS', owner_filter=None, owner_camera=None, owner_light=None), manufacturer='', bandcenter=None, bandwidth=None, edge=None), + reflects=False + ), + FilterPlacement( + path='EM', + filter=Filter( + id=569, + name='Semrock FF03-525/50', + spectrum=Spectrum(id=905, subtype='BP', owner_filter=None, owner_camera=None, owner_light=None), + manufacturer='', + bandcenter=None, + bandwidth=None, + edge=None + ), + reflects=False + ) ], - camera=SpectrumOwner(name='Andor Zyla 4.2 PLUS', spectrum=Spectrum(subtype='QE')), - light=SpectrumOwner(name='SOLA 395', spectrum=Spectrum(subtype='PD')), + camera=Camera(id=4, name='Andor Zyla 4.2 PLUS', spectrum=Spectrum(id=1328, subtype='QE', owner_filter=None, owner_camera=None, owner_light=None), manufacturer=''), + light=LightSource(id=9, name='SOLA 395', spectrum=Spectrum(id=394, subtype='PD', owner_filter=None, owner_camera=None, owner_light=None), manufacturer=''), laser=None ), OpticalConfig( name='Widefield Dual Red', filters=[ - FilterPlacement(name='Lumencor 575/25x', spectrum=Spectrum(subtype='BX'), path='EX', reflects=False), - FilterPlacement(name='Chroma 59022bs', spectrum=Spectrum(subtype='BS'), path='BS', reflects=False), - FilterPlacement(name='Semrock FF02-641/75', spectrum=Spectrum(subtype='BP'), path='EM', reflects=False) + FilterPlacement( + path='EX', + filter=Filter( + id=67, + name='Lumencor 575/25x', + spectrum=Spectrum(id=403, subtype='BX', owner_filter=None, owner_camera=None, owner_light=None), + manufacturer='', + bandcenter=None, + bandwidth=None, + edge=None + ), + reflects=False + ), + FilterPlacement( + path='BS', + filter=Filter(id=62, name='Chroma 59022bs', spectrum=Spectrum(id=385, subtype='BS', owner_filter=None, owner_camera=None, owner_light=None), manufacturer='', bandcenter=None, bandwidth=None, edge=None), + reflects=False + ), + FilterPlacement( + path='EM', + filter=Filter( + id=689, + name='Semrock FF02-641/75', + spectrum=Spectrum(id=1025, subtype='BP', owner_filter=None, owner_camera=None, owner_light=None), + manufacturer='', + bandcenter=None, + bandwidth=None, + edge=None + ), + reflects=False + ) ], - camera=SpectrumOwner(name='Andor Zyla 4.2 PLUS', spectrum=Spectrum(subtype='QE')), - light=SpectrumOwner(name='SOLA 395', spectrum=Spectrum(subtype='PD')), + camera=Camera(id=4, name='Andor Zyla 4.2 PLUS', spectrum=Spectrum(id=1328, subtype='QE', owner_filter=None, owner_camera=None, owner_light=None), manufacturer=''), + light=LightSource(id=9, name='SOLA 395', spectrum=Spectrum(id=394, subtype='PD', owner_filter=None, owner_camera=None, owner_light=None), manufacturer=''), laser=None ), OpticalConfig( name='Widefield Far-Red', filters=[ - FilterPlacement(name='Chroma ET640/30x', spectrum=Spectrum(subtype='BX'), path='EX', reflects=False), - FilterPlacement(name='Chroma T660lpxr', spectrum=Spectrum(subtype='LP'), path='BS', reflects=False), - FilterPlacement(name='Semrock FF01-698/70', spectrum=Spectrum(subtype='BP'), path='EM', reflects=False) + FilterPlacement( + path='EX', + filter=Filter( + id=445, + name='Chroma ET640/30x', + spectrum=Spectrum(id=781, subtype='BX', owner_filter=None, owner_camera=None, owner_light=None), + manufacturer='', + bandcenter=None, + bandwidth=None, + edge=None + ), + reflects=False + ), + FilterPlacement( + path='BS', + filter=Filter(id=6, name='Chroma T660lpxr', spectrum=Spectrum(id=329, subtype='LP', owner_filter=None, owner_camera=None, owner_light=None), manufacturer='', bandcenter=None, bandwidth=None, edge=None), + reflects=False + ), + FilterPlacement( + path='EM', + filter=Filter( + id=719, + name='Semrock FF01-698/70', + spectrum=Spectrum(id=1055, subtype='BP', owner_filter=None, owner_camera=None, owner_light=None), + manufacturer='', + bandcenter=None, + bandwidth=None, + edge=None + ), + reflects=False + ) ], - camera=SpectrumOwner(name='Andor Zyla 4.2 PLUS', spectrum=Spectrum(subtype='QE')), - light=SpectrumOwner(name='SOLA 395', spectrum=Spectrum(subtype='PD')), + camera=Camera(id=4, name='Andor Zyla 4.2 PLUS', spectrum=Spectrum(id=1328, subtype='QE', owner_filter=None, owner_camera=None, owner_light=None), manufacturer=''), + light=LightSource(id=9, name='SOLA 395', spectrum=Spectrum(id=394, subtype='PD', owner_filter=None, owner_camera=None, owner_light=None), manufacturer=''), laser=None ), OpticalConfig( name='Widefield Triple Cyan', filters=[ - FilterPlacement(name='Lumencor 440/20x', spectrum=Spectrum(subtype='BX'), path='EX', reflects=False), - FilterPlacement(name='Chroma 69008bs', spectrum=Spectrum(subtype='BS'), path='BS', reflects=False), - FilterPlacement(name='Chroma ET470/24m', spectrum=Spectrum(subtype='BM'), path='EM', reflects=False) + FilterPlacement( + path='EX', + filter=Filter( + id=79, + name='Lumencor 440/20x', + spectrum=Spectrum(id=415, subtype='BX', owner_filter=None, owner_camera=None, owner_light=None), + manufacturer='', + bandcenter=None, + bandwidth=None, + edge=None + ), + reflects=False + ), + FilterPlacement( + path='BS', + filter=Filter(id=60, name='Chroma 69008bs', spectrum=Spectrum(id=383, subtype='BS', owner_filter=None, owner_camera=None, owner_light=None), manufacturer='', bandcenter=None, bandwidth=None, edge=None), + reflects=False + ), + FilterPlacement( + path='EM', + filter=Filter( + id=46, + name='Chroma ET470/24m', + spectrum=Spectrum(id=369, subtype='BM', owner_filter=None, owner_camera=None, owner_light=None), + manufacturer='', + bandcenter=None, + bandwidth=None, + edge=None + ), + reflects=False + ) ], - camera=SpectrumOwner(name='Andor Zyla 4.2 PLUS', spectrum=Spectrum(subtype='QE')), - light=SpectrumOwner(name='SOLA 395', spectrum=Spectrum(subtype='PD')), + camera=Camera(id=4, name='Andor Zyla 4.2 PLUS', spectrum=Spectrum(id=1328, subtype='QE', owner_filter=None, owner_camera=None, owner_light=None), manufacturer=''), + light=LightSource(id=9, name='SOLA 395', spectrum=Spectrum(id=394, subtype='PD', owner_filter=None, owner_camera=None, owner_light=None), manufacturer=''), laser=None ), OpticalConfig( name='Widefield Triple FRET', filters=[ - FilterPlacement(name='Lumencor 440/20x', spectrum=Spectrum(subtype='BX'), path='EX', reflects=False), - FilterPlacement(name='Chroma 69008bs', spectrum=Spectrum(subtype='BS'), path='BS', reflects=False), - FilterPlacement(name='Chroma ET535/30m', spectrum=Spectrum(subtype='BM'), path='EM', reflects=False) + FilterPlacement( + path='EX', + filter=Filter( + id=79, + name='Lumencor 440/20x', + spectrum=Spectrum(id=415, subtype='BX', owner_filter=None, owner_camera=None, owner_light=None), + manufacturer='', + bandcenter=None, + bandwidth=None, + edge=None + ), + reflects=False + ), + FilterPlacement( + path='BS', + filter=Filter(id=60, name='Chroma 69008bs', spectrum=Spectrum(id=383, subtype='BS', owner_filter=None, owner_camera=None, owner_light=None), manufacturer='', bandcenter=None, bandwidth=None, edge=None), + reflects=False + ), + FilterPlacement( + path='EM', + filter=Filter( + id=36, + name='Chroma ET535/30m', + spectrum=Spectrum(id=359, subtype='BM', owner_filter=None, owner_camera=None, owner_light=None), + manufacturer='', + bandcenter=None, + bandwidth=None, + edge=None + ), + reflects=False + ) ], - camera=SpectrumOwner(name='Andor Zyla 4.2 PLUS', spectrum=Spectrum(subtype='QE')), - light=SpectrumOwner(name='SOLA 395', spectrum=Spectrum(subtype='PD')), + camera=Camera(id=4, name='Andor Zyla 4.2 PLUS', spectrum=Spectrum(id=1328, subtype='QE', owner_filter=None, owner_camera=None, owner_light=None), manufacturer=''), + light=LightSource(id=9, name='SOLA 395', spectrum=Spectrum(id=394, subtype='PD', owner_filter=None, owner_camera=None, owner_light=None), manufacturer=''), laser=None ), OpticalConfig( name='Widefield Triple Red', filters=[ - FilterPlacement(name='Lumencor 575/25x', spectrum=Spectrum(subtype='BX'), path='EX', reflects=False), - FilterPlacement(name='Chroma 69008bs', spectrum=Spectrum(subtype='BS'), path='BS', reflects=False), - FilterPlacement(name='Semrock FF02-641/75', spectrum=Spectrum(subtype='BP'), path='EM', reflects=False) + FilterPlacement( + path='EX', + filter=Filter( + id=67, + name='Lumencor 575/25x', + spectrum=Spectrum(id=403, subtype='BX', owner_filter=None, owner_camera=None, owner_light=None), + manufacturer='', + bandcenter=None, + bandwidth=None, + edge=None + ), + reflects=False + ), + FilterPlacement( + path='BS', + filter=Filter(id=60, name='Chroma 69008bs', spectrum=Spectrum(id=383, subtype='BS', owner_filter=None, owner_camera=None, owner_light=None), manufacturer='', bandcenter=None, bandwidth=None, edge=None), + reflects=False + ), + FilterPlacement( + path='EM', + filter=Filter( + id=689, + name='Semrock FF02-641/75', + spectrum=Spectrum(id=1025, subtype='BP', owner_filter=None, owner_camera=None, owner_light=None), + manufacturer='', + bandcenter=None, + bandwidth=None, + edge=None + ), + reflects=False + ) ], - camera=SpectrumOwner(name='Andor Zyla 4.2 PLUS', spectrum=Spectrum(subtype='QE')), - light=SpectrumOwner(name='SOLA 395', spectrum=Spectrum(subtype='PD')), + camera=Camera(id=4, name='Andor Zyla 4.2 PLUS', spectrum=Spectrum(id=1328, subtype='QE', owner_filter=None, owner_camera=None, owner_light=None), manufacturer=''), + light=LightSource(id=9, name='SOLA 395', spectrum=Spectrum(id=394, subtype='PD', owner_filter=None, owner_camera=None, owner_light=None), manufacturer=''), laser=None ), OpticalConfig( name='Widefield Triple Yellow', filters=[ - FilterPlacement(name='Chroma ET500/20x', spectrum=Spectrum(subtype='BX'), path='EX', reflects=False), - FilterPlacement(name='Chroma 69008bs', spectrum=Spectrum(subtype='BS'), path='BS', reflects=False), - FilterPlacement(name='Chroma ET535/30m', spectrum=Spectrum(subtype='BM'), path='EM', reflects=False) + FilterPlacement( + path='EX', + filter=Filter( + id=41, + name='Chroma ET500/20x', + spectrum=Spectrum(id=364, subtype='BX', owner_filter=None, owner_camera=None, owner_light=None), + manufacturer='', + bandcenter=None, + bandwidth=None, + edge=None + ), + reflects=False + ), + FilterPlacement( + path='BS', + filter=Filter(id=60, name='Chroma 69008bs', spectrum=Spectrum(id=383, subtype='BS', owner_filter=None, owner_camera=None, owner_light=None), manufacturer='', bandcenter=None, bandwidth=None, edge=None), + reflects=False + ), + FilterPlacement( + path='EM', + filter=Filter( + id=36, + name='Chroma ET535/30m', + spectrum=Spectrum(id=359, subtype='BM', owner_filter=None, owner_camera=None, owner_light=None), + manufacturer='', + bandcenter=None, + bandwidth=None, + edge=None + ), + reflects=False + ) ], - camera=SpectrumOwner(name='Andor Zyla 4.2 PLUS', spectrum=Spectrum(subtype='QE')), - light=SpectrumOwner(name='SOLA 395', spectrum=Spectrum(subtype='PD')), + camera=Camera(id=4, name='Andor Zyla 4.2 PLUS', spectrum=Spectrum(id=1328, subtype='QE', owner_filter=None, owner_camera=None, owner_light=None), manufacturer=''), + light=LightSource(id=9, name='SOLA 395', spectrum=Spectrum(id=394, subtype='PD', owner_filter=None, owner_camera=None, owner_light=None), manufacturer=''), laser=None ) ] ) ``` + +
diff --git a/src/fpbase/__init__.py b/src/fpbase/__init__.py index 1e1b4cf..913a232 100644 --- a/src/fpbase/__init__.py +++ b/src/fpbase/__init__.py @@ -4,18 +4,45 @@ try: __version__ = version("fpbasepy") -except PackageNotFoundError: +except PackageNotFoundError: # pragma: no cover __version__ = "uninstalled" __author__ = "Talley Lambert" __email__ = "talley.lambert@gmail.com" from . import models -from ._fetch import FPbaseClient, get_filter, get_fluorophore, get_microscope +from ._fetch import ( + FPbaseClient, + get_camera, + get_filter, + get_fluorophore, + get_light_source, + get_microscope, + get_protein, + graphql_query, + list_cameras, + list_dyes, + list_filters, + list_fluorophores, + list_light_sources, + list_microscopes, + list_proteins, +) __all__ = [ "FPbaseClient", + "get_camera", "get_filter", "get_fluorophore", + "get_light_source", "get_microscope", + "get_protein", + "graphql_query", + "list_cameras", + "list_dyes", + "list_filters", + "list_fluorophores", + "list_light_sources", + "list_microscopes", + "list_proteins", "models", ] diff --git a/src/fpbase/_fetch.py b/src/fpbase/_fetch.py index cfbc7f6..774735e 100644 --- a/src/fpbase/_fetch.py +++ b/src/fpbase/_fetch.py @@ -7,24 +7,31 @@ import threading from difflib import get_close_matches from functools import cached_property -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, Final import requests -from ._graphql import DYE_QUERY, FILTER_QUERY, MICROSCOPE_QUERY, PROTEIN_QUERY +from ._graphql import DYE_QUERY, MICROSCOPE_QUERY, PROTEIN_QUERY, SPECTRUM_QUERY from .models import ( + Camera, DyeResponse, Filter, - FilterSpectrumResponse, Fluorophore, + LightSource, Microscope, MicroscopeResponse, + Protein, ProteinResponse, + Spectrum, + SpectrumResponse, ) if TYPE_CHECKING: from collections.abc import Mapping +FPBASE_URL: Final = "https://www.fpbase.org/graphql/" +_HEADERS = {"Content-Type": "application/json", "User-Agent": "fpbase-py"} + class FPbaseClient: __instance: FPbaseClient | None = None @@ -38,12 +45,10 @@ def instance(cls) -> FPbaseClient: cls.__instance = cls() return cls.__instance - def __init__(self, base_url: str = "https://www.fpbase.org/graphql/"): + def __init__(self, base_url: str = FPBASE_URL): self.base_url = base_url self.session = requests.Session() - self.session.headers.update( - {"Content-Type": "application/json", "User-Agent": "fpbase-py"} - ) + self.session.headers.update(_HEADERS) self._cache: dict[str, bytes] = {} def get_microscope(self, id: str = "i6WL2W") -> Microscope: @@ -62,59 +67,117 @@ def get_fluorophore(self, name: str) -> Fluorophore: Examples -------- >>> get_fluorophore("mTurquoise2") - >>> get_fluorophore("mturquoise2") + >>> get_fluorophore("Alexa Fluor 488") """ - _ids = self._fluorophore_ids - if name in _ids: # direct hit - fluor_info = _ids[name] - else: - try: - fluor_info = _ids[name.lower()] - except KeyError as e: - if closest := get_close_matches(name, _ids, n=1, cutoff=0.5): - suggest = f" Did you mean {closest[0]!r}?" - else: - suggest = "" - raise ValueError(f"Fluorophore {name!r} not found.{suggest}") from e + fluor_info = _get_or_raise_suggestion( + name, self._fluorophore_ids, "Fluorophore" + ) if fluor_info["type"] == "d": return self._get_dye_by_id(fluor_info["id"]) elif fluor_info["type"] == "p": return self._get_protein_by_id(fluor_info["id"]) - raise ValueError(f"Invalid fluorophore type {fluor_info['type']!r}") + raise ValueError( # pragma: no cover + f"Invalid fluorophore type {fluor_info['type']!r}" + ) + + def get_protein(self, name: str) -> Protein: + """Fetch protein by name. + + Examples + -------- + >>> get_protein("EGFP") + """ + fluor_info = _get_or_raise_suggestion(name, self._fluorophore_ids, "Protein") + if fluor_info["type"] != "p": # pragma: no cover + raise ValueError(f"Protein {name!r} not found.") + return self._get_protein_by_id(fluor_info["id"]) + + def list_proteins(self) -> list[str]: + """List all available proteins.""" + return sorted( + { + info["name"] + for info in self._fluorophore_ids.values() + if info["type"] == "p" + } + ) + + def list_dyes(self) -> list[str]: + """List all available dyes.""" + return sorted( + { + info["name"] + for info in self._fluorophore_ids.values() + if info["type"] == "d" + } + ) + + def list_fluorophores(self) -> list[str]: + """List all available fluorophores.""" + return sorted({info["name"] for info in self._fluorophore_ids.values()}) + + def list_microscopes(self) -> list[str]: + """List all available microscopes.""" + resp = self._send_query("{ microscopes { id name } }") + return [item["name"] for item in json.loads(resp)["data"]["microscopes"]] + + def list_filters(self) -> list[str]: + """List all available filters.""" + return sorted(self._filter_spectrum_ids.keys()) + + def list_cameras(self) -> list[str]: + """List all available cameras.""" + return sorted(self._camera_spectrum_ids.keys()) + + def list_light_sources(self) -> list[str]: + """List all available lights.""" + return sorted(self._light_spectrum_ids.keys()) def get_filter(self, name: str) -> Filter: - """Fetch filter spectrum by name.""" + """Fetch filter by name.""" + spectrum = self._get_spectrum(name, "Filter") + if spectrum.owner_filter is None: # pragma: no cover + raise ValueError(f"Filter {name!r} not found.") + return spectrum.owner_filter + + def get_camera(self, name: str) -> Camera: + """Fetch camera spectrum by name.""" + spectrum = self._get_spectrum(name, "Camera") + if spectrum.owner_camera is None: # pragma: no cover + raise ValueError(f"Camera {name!r} not found.") + return spectrum.owner_camera + + def get_light_source(self, name: str) -> LightSource: + """Fetch light spectrum by name.""" + spectrum = self._get_spectrum(name, "Light") + if spectrum.owner_light is None: # pragma: no cover + raise ValueError(f"Light {name!r} not found.") + return spectrum.owner_light + + def _get_spectrum(self, name: str, type_: str) -> Spectrum: + possibilities: Mapping[str, int] = { + "Filter": self._filter_spectrum_ids, + "Light": self._light_spectrum_ids, + "Camera": self._camera_spectrum_ids, + }[type_] normed = _norm_name(name) - try: - filter_id = self._filter_spectrum_ids[normed] - except KeyError as e: - if closest := get_close_matches( - normed, self._filter_spectrum_ids, n=1, cutoff=0.5 - ): - suggest = f" Did you mean {closest[0]!r}?" - else: - suggest = "" - raise ValueError(f"Filter {name!r} not found.{suggest}") from e - - resp = self._send_query(FILTER_QUERY, {"id": int(filter_id)}) - return FilterSpectrumResponse.model_validate_json( - resp - ).data.spectrum.ownerFilter + filter_id = _get_or_raise_suggestion(normed, possibilities, type_) + + resp = self._send_query(SPECTRUM_QUERY, {"id": int(filter_id)}) + return SpectrumResponse.model_validate_json(resp).data.spectrum # ----------------------------------------------------------- def _send_query(self, query: str, variables: dict | None = None) -> bytes: - payload = {"query": query, "variables": variables or {}} - payload_str = json.dumps(payload, sort_keys=True) # Convert to JSON string # Create a hash - hashkey = hashlib.md5(payload_str.encode("utf-8")).hexdigest() - if hashkey not in self._cache: + if (key := _hashargs(self.base_url, query, variables)) not in self._cache: + payload = {"query": query, "variables": variables or {}} data = json.dumps(payload).encode("utf-8") response = self.session.post(self.base_url, data=data) response.raise_for_status() - self._cache[hashkey] = response.content - return self._cache[hashkey] + self._cache[key] = response.content + return self._cache[key] @cached_property def _fluorophore_ids(self) -> dict[str, dict[str, str]]: @@ -124,23 +187,35 @@ def _fluorophore_ids(self) -> dict[str, dict[str, str]]: lookup: dict[str, dict[str, str]] = {} for key in ["dyes", "proteins"]: for item in data[key]: - lookup[item["name"].lower()] = {"id": item["id"], "type": key[0]} - lookup[item["slug"]] = {"id": item["id"], "type": key[0]} + lookup[item["name"].lower()] = {**item, "type": key[0]} + lookup[item["slug"]] = {**item, "type": key[0]} if key == "proteins": - lookup[item["id"]] = {"id": item["id"], "type": key[0]} + lookup[item["id"].lower()] = {**item, "type": key[0]} return lookup @cached_property def _filter_spectrum_ids(self) -> Mapping[str, int]: - resp = self._send_query('{ spectra(category:"F") { id owner { name } } }') - data: dict = json.loads(resp)["data"]["spectra"] + return self._get_spectrum_ids("F") + + @cached_property + def _light_spectrum_ids(self) -> Mapping[str, int]: + return self._get_spectrum_ids("L") + + @cached_property + def _camera_spectrum_ids(self) -> Mapping[str, int]: + return self._get_spectrum_ids("C") + + def _get_spectrum_ids(self, key: str) -> dict[str, int]: + query = f'{{ spectra(category: "{key}") {{ id owner {{ name }} }} }}' + resp = self._send_query(query) + data = json.loads(resp)["data"]["spectra"] return {_norm_name(item["owner"]["name"]): int(item["id"]) for item in data} def _get_dye_by_id(self, id: str | int) -> Fluorophore: resp = self._send_query(DYE_QUERY, {"id": int(id)}) return DyeResponse.model_validate_json(resp).data.dye - def _get_protein_by_id(self, id: str) -> Fluorophore: + def _get_protein_by_id(self, id: str) -> Protein: resp = self._send_query(PROTEIN_QUERY, {"id": id}) return ProteinResponse.model_validate_json(resp).data.protein @@ -159,3 +234,128 @@ def get_fluorophore(name: str) -> Fluorophore: def get_filter(name: str) -> Filter: return FPbaseClient.instance().get_filter(name) + + +def get_camera(name: str) -> Camera: + return FPbaseClient.instance().get_camera(name) + + +def get_light_source(name: str) -> LightSource: + return FPbaseClient.instance().get_light_source(name) + + +def get_protein(name: str) -> Protein: + return FPbaseClient.instance().get_protein(name) + + +def list_proteins() -> list[str]: + return FPbaseClient.instance().list_proteins() + + +def list_dyes() -> list[str]: + return FPbaseClient.instance().list_dyes() + + +def list_fluorophores() -> list[str]: + return FPbaseClient.instance().list_fluorophores() + + +def list_microscopes() -> list[str]: + return FPbaseClient.instance().list_microscopes() + + +def list_filters() -> list[str]: + return FPbaseClient.instance().list_filters() + + +def list_cameras() -> list[str]: + return FPbaseClient.instance().list_cameras() + + +def list_light_sources() -> list[str]: + return FPbaseClient.instance().list_light_sources() + + +def _get_or_raise_suggestion( + query: str, possibilities: Mapping[str, Any], type_: str +) -> Any: + """Raise a ValueError with a suggestion if a close match is found.""" + try: + return possibilities[query.lower()] + except KeyError as e: + if closest := get_close_matches(query, possibilities, n=1, cutoff=0.5): + suggest = f" Did you mean {closest[0]!r}?" + else: # pragma: no cover + suggest = "" + raise ValueError(f"{type_} {query!r} not found.{suggest}") from e + + +_RESPONSE_CACHE: dict[str, dict] = {} + + +def graphql_query( + query: str, + variables: dict | None = None, + *, + session: requests.Session | None = None, +) -> dict[str, Any]: + """Send a generic GraphQL query to the FPbase API. + + See docs and test out queries at https://www.fpbase.org/graphql/ + + Parameters + ---------- + query : str + A graphql query string. For example, "{ proteins { name } }" + variables : dict | None, optional + If the query requires variables, pass them here, by default None + session : requests.Session | None, optional + Optionally pass a requests session, by default, will create a new session. + + Returns + ------- + dict[str, Any] + JSON response from the API deserialized into a Python dictionary. + + Examples + -------- + # get all protein names and sequences + >>> data = fpbase.graphql_query("{proteins { name seq } }") + + # get specific fields for a specific protein + # note that single/double quotes are NOT interchangeable here + >>> data = fpbase.graphql_query('{protein(id: "R9NL8") { name seq } }') + + # get optical configs for a microscope, using a variable + >>> q = "query getScope($id: String!){ microscope(id: $id){ name opticalConfigs {name} } }" + >>> data = fpbase.graphql_query(q, {"id": "i6WL2WdgcDMgJYtPrpZcaJ"}) + """ # noqa: E501 + url = FPBASE_URL + if (key := _hashargs(url, query, variables)) not in _RESPONSE_CACHE: + data_bytes = _fetch_query(query, variables, session=session, url=url) + _RESPONSE_CACHE[key] = json.loads(data_bytes) + return _RESPONSE_CACHE[key] + + +def _hashargs(*args: str | dict | None | tuple) -> str: + hasher = hashlib.md5() + for arg in args: + if isinstance(arg, dict): + arg = tuple(sorted(arg.items())) + hasher.update(str(arg).encode("utf-8")) + return hasher.hexdigest() + + +def _fetch_query( + query: str, + variables: dict | None = None, + *, + session: requests.Session | None = None, + url: str = FPBASE_URL, +) -> bytes: + payload = {"query": query, "variables": variables or {}} + data = json.dumps(payload).encode("utf-8") + post = requests.post if session is None else session.post + response = post(url, data=data, headers=_HEADERS) + response.raise_for_status() + return response.content diff --git a/src/fpbase/_graphql.py b/src/fpbase/_graphql.py index 68f5aa8..01ef909 100644 --- a/src/fpbase/_graphql.py +++ b/src/fpbase/_graphql.py @@ -6,13 +6,16 @@ opticalConfigs { name filters { - name path reflects - spectrum { subtype data } + filter { + id + name + spectrum { id subtype data } + } } - camera { name spectrum { subtype data } } - light { name spectrum { subtype data } } + camera { id name spectrum { id subtype data } } + light { id name spectrum { id subtype data } } laser } } @@ -28,7 +31,7 @@ emMax extCoeff qy - spectra { subtype data } + spectra { id subtype data } } } """ @@ -38,6 +41,15 @@ protein(id: $id) { name id + seq + genbank + pdb + uniprot + mw + agg + switchType + primaryReference { doi } + references { doi } states { id name @@ -48,26 +60,50 @@ extCoeff qy lifetime - spectra { subtype data } + spectra { id subtype data } } defaultState { id - } + name + exMax + emMax + emhex + exhex + extCoeff + qy + lifetime + spectra { id subtype data } + } } } """ -FILTER_QUERY = """ +SPECTRUM_QUERY = """ query getSpectrum($id: Int!) { spectrum(id: $id) { + id subtype data ownerFilter { + id name manufacturer bandcenter bandwidth edge + spectrum { id subtype data } + } + ownerCamera { + id + name + manufacturer + spectrum { id subtype data } + } + ownerLight { + id + name + manufacturer + spectrum { id subtype data } } } } diff --git a/src/fpbase/models.py b/src/fpbase/models.py index 2b555cd..bdc7925 100644 --- a/src/fpbase/models.py +++ b/src/fpbase/models.py @@ -1,14 +1,14 @@ """Main fetching logic.""" +from collections.abc import Iterable from enum import Enum -from typing import Any, Literal, Optional +from typing import Any, Optional -from pydantic import BaseModel, Field, field_validator, model_validator +from pydantic import BaseModel, Field, computed_field, model_validator __all__ = [ "Filter", "FilterPlacement", - "FilterSpectrum", "Fluorophore", "Microscope", "OpticalConfig", @@ -34,7 +34,7 @@ class SpectrumType(str, Enum): QE = "QE" AB = "AB" - def __str__(self) -> str: + def __str__(self) -> str: # pragma: no cover """Return the string representation of the enum.""" return self.value @@ -43,54 +43,104 @@ def __repr__(self) -> str: return repr(self.value) +class FilterPath(str, Enum): + """Placement of a filter in an optical config.""" + + EX = "EX" + EM = "EM" + BS = "BS" + + def __str__(self) -> str: # pragma: no cover + """Return the string representation of the enum.""" + return self.value + + def __repr__(self) -> str: + """Return the repr of the enum.""" + return repr(self.value) + + +class Olig(str, Enum): + MONOMER = "M" + DIMER = "D" + TANDEM_DIMER = "TD" + WEAK_DIMER = "WD" + TETRAMER = "T" + + def __str__(self) -> str: # pragma: no cover + """Return the string representation of the enum.""" + return self.value + + +class SwitchType(str, Enum): + BASIC = "B" + PHOTOACTIVATABLE = "PA" + PHOTOSWITCHABLE = "PS" + PHOTOCONVERTIBLE = "PC" + MULTIPHOTOCHROMIC = "MP" + TIMER = "T" + OTHER = "O" + + def __str__(self) -> str: # pragma: no cover + """Return the string representation of the enum.""" + return self.value + + class Spectrum(BaseModel): """Spectrum with data.""" + id: int subtype: SpectrumType data: list[tuple[float, float]] = Field(..., repr=False) + owner_filter: Optional["Filter"] = Field(None, alias="ownerFilter") + owner_camera: Optional["Camera"] = Field(None, alias="ownerCamera") + owner_light: Optional["LightSource"] = Field(None, alias="ownerLight") -class Filter(BaseModel): - """A filter with its properties.""" +class SpectrumOwner(BaseModel): + """Something that can own a spectrum.""" + + id: int name: str - manufacturer: str - bandcenter: Optional[float] - bandwidth: Optional[float] - edge: Optional[float] + spectrum: Spectrum -class FilterSpectrum(Spectrum): - """Spectrum owned by a filter.""" +class Filter(SpectrumOwner): + """A filter with its properties.""" - ownerFilter: Filter + manufacturer: str = "" + bandcenter: Optional[float] = None + bandwidth: Optional[float] = None + edge: Optional[float] = None -class SpectrumOwner(BaseModel): - """Something that can own a spectrum.""" +class Camera(SpectrumOwner): + manufacturer: str = "" - name: str - spectrum: Spectrum + +class LightSource(SpectrumOwner): + manufacturer: str = "" class State(BaseModel): """Fluorophore state.""" id: int - exMax: float # nanometers - emMax: float # nanometers + name: str + exMax: Optional[float] = None # nanometers + emMax: Optional[float] = None # nanometers emhex: str = "" exhex: str = "" - extCoeff: Optional[float] = None # M^-1 cm^-1 + ext_coeff: Optional[float] = Field(None, alias="extCoeff") # M^-1 cm^-1 qy: Optional[float] = None - spectra: list[Spectrum] + spectra: list[Spectrum] = Field(default_factory=list) lifetime: Optional[float] = None # ns @property def excitation_spectrum(self) -> Optional[Spectrum]: """Return the excitation spectrum, absorption spectrum, or None.""" spect = next((s for s in self.spectra if s.subtype == "EX"), None) - if not spect: + if not spect: # pragma: no cover spect = next((s for s in self.spectra if s.subtype == "AB"), None) return spect @@ -105,8 +155,8 @@ class Fluorophore(BaseModel): name: str id: str + default_state: Optional[State] = Field(None, alias="defaultState") states: list[State] = Field(default_factory=list) - defaultState: Optional[int] = None @model_validator(mode="before") @classmethod @@ -114,30 +164,49 @@ def _v_model(cls, v: Any) -> Any: if isinstance(v, dict): out = dict(v) if "states" not in v and "exMax" in v: - out["states"] = [State(**v)] + # this is a single-state fluorophore. probably a Dye. + # this is a bit of a hack around the fpbase API + state = State(**v) + out["states"] = [state] + out["defaultState"] = state return out - return v - - @field_validator("defaultState", mode="before") - @classmethod - def _v_default_state(cls, v: Any) -> int: - if isinstance(v, dict) and "id" in v: - return int(v["id"]) - return int(v) - - @property - def default_state(self) -> Optional[State]: - """Return the default state or the first state.""" - for state in self.states: - if state.id == self.defaultState: - return state - return next(iter(self.states), None) + return v # pragma: no cover + + def __repr_args__(self) -> Iterable[tuple[str | None, Any]]: + """Return the repr args, excluding the default state if it's the only one.""" + for key, val in super().__repr_args__(): + if key == "states" and len(val) == 1 and val[0] == self.default_state: + continue + yield key, val + + +class Reference(BaseModel): + doi: str + + @computed_field + def url(self) -> str: + """Return the DOI URL.""" + return f"https://doi.org/{self.doi}" + + +class Protein(Fluorophore): + seq: Optional[str] = None + pdb: list[str] = Field(default_factory=list) + genbank: Optional[str] = None + uniprot: Optional[str] = None + agg: Optional[Olig] = None + switch_type: Optional[SwitchType] = Field(None, alias="switchType") + primary_reference: Optional[Reference] = Field(None, alias="primaryReference") + references: list[Reference] = Field(default_factory=list) + states: list[State] = Field(default_factory=list) + # default_state: Optional[State] = Field(None, alias="defaultState") -class FilterPlacement(SpectrumOwner): +class FilterPlacement(BaseModel): """A filter placed in a microscope.""" - path: Literal["EX", "EM", "BS"] + path: FilterPath + filter: Filter reflects: bool = False @@ -146,8 +215,8 @@ class OpticalConfig(BaseModel): name: str filters: list[FilterPlacement] - camera: Optional[SpectrumOwner] - light: Optional[SpectrumOwner] + camera: Optional["Camera"] + light: Optional["LightSource"] laser: Optional[int] @@ -170,7 +239,7 @@ class MicroscopeResponse(BaseModel): class _ProteinPayload(BaseModel): - protein: Fluorophore + protein: Protein class ProteinResponse(BaseModel): @@ -189,43 +258,11 @@ class DyeResponse(BaseModel): data: _DyePayload -class _FilterSpectrumPayload(BaseModel): - spectrum: FilterSpectrum +class _SpectrumPayload(BaseModel): + spectrum: Spectrum -class FilterSpectrumResponse(BaseModel): +class SpectrumResponse(BaseModel): """Response for a filter spectrum query.""" - data: _FilterSpectrumPayload - - -# WIP -# def generate_graphql_query(model: type[BaseModel], model_name: str = "") -> str: -# def get_fields(model: type[BaseModel]) -> str: -# fields = [] -# for name, field in model.model_fields.items(): -# annotation = field.annotation - -# if isinstance(annotation, type) and issubclass(annotation, BaseModel): -# sub_fields = get_fields(annotation) -# fields.append(f"{name} {{ {sub_fields} }}") -# elif ( -# get_origin(annotation) in (list, tuple) -# and isinstance(type_ := get_args(annotation)[0], type) -# and issubclass(type_, BaseModel) -# ): -# sub_fields = get_fields(type_) -# fields.append(f"{name} {{ {sub_fields} }}") -# else: -# fields.append(name) -# return "\n".join(fields) - -# fields_str = get_fields(model) -# model_name = model_name or model.__name__ -# return f""" -# query get{model_name}($id: String!) {{ -# {model_name.lower()}(id: $id) {{ -# {fields_str} -# }} -# }} -# """ + data: _SpectrumPayload diff --git a/tests/test_fpbase.py b/tests/test_fpbase.py index 20ed4e9..a465ff1 100644 --- a/tests/test_fpbase.py +++ b/tests/test_fpbase.py @@ -1,23 +1,72 @@ import pytest -from fpbase import get_filter, get_fluorophore, get_microscope +import fpbase def test_get_microscope() -> None: - scope = get_microscope("wKqWbgApvguSNDSRZNSfpN") + scope = fpbase.get_microscope("wKqWbgApvguSNDSRZNSfpN") + repr(scope) assert scope.name == "Example Simple Widefield" @pytest.mark.parametrize("name", ["EGFP", "Alexa Fluor 488"]) def test_get_fluor(name: str) -> None: - fluor = get_fluorophore(name) + fluor = fpbase.get_fluorophore(name) + repr(fluor) assert fluor.name == name assert fluor.default_state assert fluor.default_state.excitation_spectrum is not None assert fluor.default_state.emission_spectrum is not None +@pytest.mark.parametrize("name", ["mEos3.2", "mScarlet-I"]) +def test_get_protein(name: str) -> None: + prot = fpbase.get_protein(name) + repr(prot) + assert prot.name == name + assert prot.default_state.excitation_spectrum is not None + assert prot.default_state.emission_spectrum is not None + + +def test_get_missing_protein() -> None: + with pytest.raises(ValueError, match="Did you mean 'mscarlet'"): + fpbase.get_protein("mScrlet") + + @pytest.mark.parametrize("name", ["Chroma ET525/50m", "Semrock FF01-520/35"]) def test_get_filter(name: str) -> None: - filt = get_filter(name) + filt = fpbase.get_filter(name) + repr(filt) assert filt.name == name + + +def test_get_camera() -> None: + cam = fpbase.get_camera("Andor Zyla 5.5") + repr(cam) + assert cam.name == "Andor Zyla 5.5" + + +def test_get_light_source() -> None: + light = fpbase.get_light_source("Lumencor Celesta UV") + repr(light) + assert light.name == "Lumencor Celesta UV" + + +def test_lists() -> None: + assert len(fpbase.list_microscopes()) > 0 + assert len(fpbase.list_fluorophores()) > 0 + assert len(fpbase.list_filters()) > 0 + assert len(fpbase.list_cameras()) > 0 + assert len(fpbase.list_light_sources()) > 0 + assert len(fpbase.list_dyes()) > 0 + assert len(fpbase.list_proteins()) > 0 + + +def test_generic_gql_query() -> None: + data = fpbase.graphql_query("{proteins { name seq } }") + EGFP = next(p for p in data["data"]["proteins"] if p["name"] == "EGFP") + assert EGFP["seq"].startswith("MVSK") + + q = "query getProtein($id: String!){ protein(id: $id){ name } }" + data = fpbase.graphql_query(q, {"id": "R9NL8"}) + assert data["data"]["protein"]["name"] == "EGFP"