fpbase 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fpbase/__init__.py +21 -0
- fpbase/_fetch.py +161 -0
- fpbase/_graphql.py +74 -0
- fpbase/models.py +231 -0
- fpbase/py.typed +5 -0
- fpbase-0.0.1.dist-info/METADATA +197 -0
- fpbase-0.0.1.dist-info/RECORD +9 -0
- fpbase-0.0.1.dist-info/WHEEL +4 -0
- fpbase-0.0.1.dist-info/licenses/LICENSE +28 -0
fpbase/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Python wrapper for FPBase API."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
__version__ = version("fpbasepy")
|
|
7
|
+
except PackageNotFoundError:
|
|
8
|
+
__version__ = "uninstalled"
|
|
9
|
+
__author__ = "Talley Lambert"
|
|
10
|
+
__email__ = "talley.lambert@gmail.com"
|
|
11
|
+
|
|
12
|
+
from . import models
|
|
13
|
+
from ._fetch import FPbaseClient, get_filter, get_fluorophore, get_microscope
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"FPbaseClient",
|
|
17
|
+
"get_filter",
|
|
18
|
+
"get_fluorophore",
|
|
19
|
+
"get_microscope",
|
|
20
|
+
"models",
|
|
21
|
+
]
|
fpbase/_fetch.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Main fetching logic."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import threading
|
|
8
|
+
from difflib import get_close_matches
|
|
9
|
+
from functools import cached_property
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
import requests
|
|
13
|
+
|
|
14
|
+
from ._graphql import DYE_QUERY, FILTER_QUERY, MICROSCOPE_QUERY, PROTEIN_QUERY
|
|
15
|
+
from .models import (
|
|
16
|
+
DyeResponse,
|
|
17
|
+
Filter,
|
|
18
|
+
FilterSpectrumResponse,
|
|
19
|
+
Fluorophore,
|
|
20
|
+
Microscope,
|
|
21
|
+
MicroscopeResponse,
|
|
22
|
+
ProteinResponse,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from collections.abc import Mapping
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class FPbaseClient:
|
|
30
|
+
__instance: FPbaseClient | None = None
|
|
31
|
+
__lock: threading.Lock = threading.Lock()
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def instance(cls) -> FPbaseClient:
|
|
35
|
+
if cls.__instance is None:
|
|
36
|
+
with cls.__lock:
|
|
37
|
+
if cls.__instance is None: # Double-checked locking
|
|
38
|
+
cls.__instance = cls()
|
|
39
|
+
return cls.__instance
|
|
40
|
+
|
|
41
|
+
def __init__(self, base_url: str = "https://www.fpbase.org/graphql/"):
|
|
42
|
+
self.base_url = base_url
|
|
43
|
+
self.session = requests.Session()
|
|
44
|
+
self.session.headers.update(
|
|
45
|
+
{"Content-Type": "application/json", "User-Agent": "fpbase-py"}
|
|
46
|
+
)
|
|
47
|
+
self._cache: dict[str, bytes] = {}
|
|
48
|
+
|
|
49
|
+
def get_microscope(self, id: str = "i6WL2W") -> Microscope:
|
|
50
|
+
"""Get microscope by ID.
|
|
51
|
+
|
|
52
|
+
Examples
|
|
53
|
+
--------
|
|
54
|
+
>>> get_microscope("i6WL2W")
|
|
55
|
+
"""
|
|
56
|
+
resp = self._send_query(MICROSCOPE_QUERY, {"id": id})
|
|
57
|
+
return MicroscopeResponse.model_validate_json(resp).data.microscope
|
|
58
|
+
|
|
59
|
+
def get_fluorophore(self, name: str) -> Fluorophore:
|
|
60
|
+
"""Fetch fluorophore by name, slug, or ID.
|
|
61
|
+
|
|
62
|
+
Examples
|
|
63
|
+
--------
|
|
64
|
+
>>> get_fluorophore("mTurquoise2")
|
|
65
|
+
>>> get_fluorophore("mturquoise2")
|
|
66
|
+
"""
|
|
67
|
+
_ids = self._fluorophore_ids
|
|
68
|
+
if name in _ids: # direct hit
|
|
69
|
+
fluor_info = _ids[name]
|
|
70
|
+
else:
|
|
71
|
+
try:
|
|
72
|
+
fluor_info = _ids[name.lower()]
|
|
73
|
+
except KeyError as e:
|
|
74
|
+
if closest := get_close_matches(name, _ids, n=1, cutoff=0.5):
|
|
75
|
+
suggest = f" Did you mean {closest[0]!r}?"
|
|
76
|
+
else:
|
|
77
|
+
suggest = ""
|
|
78
|
+
raise ValueError(f"Fluorophore {name!r} not found.{suggest}") from e
|
|
79
|
+
|
|
80
|
+
if fluor_info["type"] == "d":
|
|
81
|
+
return self._get_dye_by_id(fluor_info["id"])
|
|
82
|
+
elif fluor_info["type"] == "p":
|
|
83
|
+
return self._get_protein_by_id(fluor_info["id"])
|
|
84
|
+
raise ValueError(f"Invalid fluorophore type {fluor_info['type']!r}")
|
|
85
|
+
|
|
86
|
+
def get_filter(self, name: str) -> Filter:
|
|
87
|
+
"""Fetch filter spectrum by name."""
|
|
88
|
+
normed = _norm_name(name)
|
|
89
|
+
try:
|
|
90
|
+
filter_id = self._filter_spectrum_ids[normed]
|
|
91
|
+
except KeyError as e:
|
|
92
|
+
if closest := get_close_matches(
|
|
93
|
+
normed, self._filter_spectrum_ids, n=1, cutoff=0.5
|
|
94
|
+
):
|
|
95
|
+
suggest = f" Did you mean {closest[0]!r}?"
|
|
96
|
+
else:
|
|
97
|
+
suggest = ""
|
|
98
|
+
raise ValueError(f"Filter {name!r} not found.{suggest}") from e
|
|
99
|
+
|
|
100
|
+
resp = self._send_query(FILTER_QUERY, {"id": int(filter_id)})
|
|
101
|
+
return FilterSpectrumResponse.model_validate_json(
|
|
102
|
+
resp
|
|
103
|
+
).data.spectrum.ownerFilter
|
|
104
|
+
|
|
105
|
+
# -----------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
def _send_query(self, query: str, variables: dict | None = None) -> bytes:
|
|
108
|
+
payload = {"query": query, "variables": variables or {}}
|
|
109
|
+
payload_str = json.dumps(payload, sort_keys=True) # Convert to JSON string
|
|
110
|
+
# Create a hash
|
|
111
|
+
hashkey = hashlib.md5(payload_str.encode("utf-8")).hexdigest()
|
|
112
|
+
if hashkey not in self._cache:
|
|
113
|
+
data = json.dumps(payload).encode("utf-8")
|
|
114
|
+
response = self.session.post(self.base_url, data=data)
|
|
115
|
+
response.raise_for_status()
|
|
116
|
+
self._cache[hashkey] = response.content
|
|
117
|
+
return self._cache[hashkey]
|
|
118
|
+
|
|
119
|
+
@cached_property
|
|
120
|
+
def _fluorophore_ids(self) -> dict[str, dict[str, str]]:
|
|
121
|
+
"""Return a lookup table of fluorophore {name: {id: ..., type: ...}}."""
|
|
122
|
+
resp = self._send_query("{ dyes { id name slug } proteins { id name slug } }")
|
|
123
|
+
data: dict[str, list[dict[str, str]]] = json.loads(resp)["data"]
|
|
124
|
+
lookup: dict[str, dict[str, str]] = {}
|
|
125
|
+
for key in ["dyes", "proteins"]:
|
|
126
|
+
for item in data[key]:
|
|
127
|
+
lookup[item["name"].lower()] = {"id": item["id"], "type": key[0]}
|
|
128
|
+
lookup[item["slug"]] = {"id": item["id"], "type": key[0]}
|
|
129
|
+
if key == "proteins":
|
|
130
|
+
lookup[item["id"]] = {"id": item["id"], "type": key[0]}
|
|
131
|
+
return lookup
|
|
132
|
+
|
|
133
|
+
@cached_property
|
|
134
|
+
def _filter_spectrum_ids(self) -> Mapping[str, int]:
|
|
135
|
+
resp = self._send_query('{ spectra(category:"F") { id owner { name } } }')
|
|
136
|
+
data: dict = json.loads(resp)["data"]["spectra"]
|
|
137
|
+
return {_norm_name(item["owner"]["name"]): int(item["id"]) for item in data}
|
|
138
|
+
|
|
139
|
+
def _get_dye_by_id(self, id: str | int) -> Fluorophore:
|
|
140
|
+
resp = self._send_query(DYE_QUERY, {"id": int(id)})
|
|
141
|
+
return DyeResponse.model_validate_json(resp).data.dye
|
|
142
|
+
|
|
143
|
+
def _get_protein_by_id(self, id: str) -> Fluorophore:
|
|
144
|
+
resp = self._send_query(PROTEIN_QUERY, {"id": id})
|
|
145
|
+
return ProteinResponse.model_validate_json(resp).data.protein
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _norm_name(name: str) -> str:
|
|
149
|
+
return name.lower().replace(" ", "-").replace("/", "-")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def get_microscope(id: str = "i6WL2W") -> Microscope:
|
|
153
|
+
return FPbaseClient.instance().get_microscope(id)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def get_fluorophore(name: str) -> Fluorophore:
|
|
157
|
+
return FPbaseClient.instance().get_fluorophore(name)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def get_filter(name: str) -> Filter:
|
|
161
|
+
return FPbaseClient.instance().get_filter(name)
|
fpbase/_graphql.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
MICROSCOPE_QUERY = """
|
|
2
|
+
query getMicroscope($id: String!) {
|
|
3
|
+
microscope(id: $id) {
|
|
4
|
+
id
|
|
5
|
+
name
|
|
6
|
+
opticalConfigs {
|
|
7
|
+
name
|
|
8
|
+
filters {
|
|
9
|
+
name
|
|
10
|
+
path
|
|
11
|
+
reflects
|
|
12
|
+
spectrum { subtype data }
|
|
13
|
+
}
|
|
14
|
+
camera { name spectrum { subtype data } }
|
|
15
|
+
light { name spectrum { subtype data } }
|
|
16
|
+
laser
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
DYE_QUERY = """
|
|
23
|
+
query getDye($id: Int!) {
|
|
24
|
+
dye(id: $id) {
|
|
25
|
+
name
|
|
26
|
+
id
|
|
27
|
+
exMax
|
|
28
|
+
emMax
|
|
29
|
+
extCoeff
|
|
30
|
+
qy
|
|
31
|
+
spectra { subtype data }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
PROTEIN_QUERY = """
|
|
37
|
+
query getProtein($id: String!) {
|
|
38
|
+
protein(id: $id) {
|
|
39
|
+
name
|
|
40
|
+
id
|
|
41
|
+
states {
|
|
42
|
+
id
|
|
43
|
+
name
|
|
44
|
+
exMax
|
|
45
|
+
emMax
|
|
46
|
+
emhex
|
|
47
|
+
exhex
|
|
48
|
+
extCoeff
|
|
49
|
+
qy
|
|
50
|
+
lifetime
|
|
51
|
+
spectra { subtype data }
|
|
52
|
+
}
|
|
53
|
+
defaultState {
|
|
54
|
+
id
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
FILTER_QUERY = """
|
|
61
|
+
query getSpectrum($id: Int!) {
|
|
62
|
+
spectrum(id: $id) {
|
|
63
|
+
subtype
|
|
64
|
+
data
|
|
65
|
+
ownerFilter {
|
|
66
|
+
name
|
|
67
|
+
manufacturer
|
|
68
|
+
bandcenter
|
|
69
|
+
bandwidth
|
|
70
|
+
edge
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
"""
|
fpbase/models.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Main fetching logic."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Any, Literal, Optional
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"Filter",
|
|
10
|
+
"FilterPlacement",
|
|
11
|
+
"FilterSpectrum",
|
|
12
|
+
"Fluorophore",
|
|
13
|
+
"Microscope",
|
|
14
|
+
"OpticalConfig",
|
|
15
|
+
"Spectrum",
|
|
16
|
+
"SpectrumOwner",
|
|
17
|
+
"SpectrumType",
|
|
18
|
+
"State",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SpectrumType(str, Enum):
|
|
23
|
+
"""Spectrum types."""
|
|
24
|
+
|
|
25
|
+
A_2P = "A_2P"
|
|
26
|
+
BM = "BM"
|
|
27
|
+
BP = "BP"
|
|
28
|
+
BS = "BS"
|
|
29
|
+
BX = "BX"
|
|
30
|
+
EM = "EM"
|
|
31
|
+
EX = "EX"
|
|
32
|
+
LP = "LP"
|
|
33
|
+
PD = "PD"
|
|
34
|
+
QE = "QE"
|
|
35
|
+
AB = "AB"
|
|
36
|
+
|
|
37
|
+
def __str__(self) -> str:
|
|
38
|
+
"""Return the string representation of the enum."""
|
|
39
|
+
return self.value
|
|
40
|
+
|
|
41
|
+
def __repr__(self) -> str:
|
|
42
|
+
"""Return the repr of the enum."""
|
|
43
|
+
return repr(self.value)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Spectrum(BaseModel):
|
|
47
|
+
"""Spectrum with data."""
|
|
48
|
+
|
|
49
|
+
subtype: SpectrumType
|
|
50
|
+
data: list[tuple[float, float]] = Field(..., repr=False)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Filter(BaseModel):
|
|
54
|
+
"""A filter with its properties."""
|
|
55
|
+
|
|
56
|
+
name: str
|
|
57
|
+
manufacturer: str
|
|
58
|
+
bandcenter: Optional[float]
|
|
59
|
+
bandwidth: Optional[float]
|
|
60
|
+
edge: Optional[float]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class FilterSpectrum(Spectrum):
|
|
64
|
+
"""Spectrum owned by a filter."""
|
|
65
|
+
|
|
66
|
+
ownerFilter: Filter
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class SpectrumOwner(BaseModel):
|
|
70
|
+
"""Something that can own a spectrum."""
|
|
71
|
+
|
|
72
|
+
name: str
|
|
73
|
+
spectrum: Spectrum
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class State(BaseModel):
|
|
77
|
+
"""Fluorophore state."""
|
|
78
|
+
|
|
79
|
+
id: int
|
|
80
|
+
exMax: float # nanometers
|
|
81
|
+
emMax: float # nanometers
|
|
82
|
+
emhex: str = ""
|
|
83
|
+
exhex: str = ""
|
|
84
|
+
extCoeff: Optional[float] = None # M^-1 cm^-1
|
|
85
|
+
qy: Optional[float] = None
|
|
86
|
+
spectra: list[Spectrum]
|
|
87
|
+
lifetime: Optional[float] = None # ns
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def excitation_spectrum(self) -> Optional[Spectrum]:
|
|
91
|
+
"""Return the excitation spectrum, absorption spectrum, or None."""
|
|
92
|
+
spect = next((s for s in self.spectra if s.subtype == "EX"), None)
|
|
93
|
+
if not spect:
|
|
94
|
+
spect = next((s for s in self.spectra if s.subtype == "AB"), None)
|
|
95
|
+
return spect
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def emission_spectrum(self) -> Optional[Spectrum]:
|
|
99
|
+
"""Return the emission spectrum or None."""
|
|
100
|
+
return next((s for s in self.spectra if s.subtype == "EM"), None)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class Fluorophore(BaseModel):
|
|
104
|
+
"""A fluorophore with its states."""
|
|
105
|
+
|
|
106
|
+
name: str
|
|
107
|
+
id: str
|
|
108
|
+
states: list[State] = Field(default_factory=list)
|
|
109
|
+
defaultState: Optional[int] = None
|
|
110
|
+
|
|
111
|
+
@model_validator(mode="before")
|
|
112
|
+
@classmethod
|
|
113
|
+
def _v_model(cls, v: Any) -> Any:
|
|
114
|
+
if isinstance(v, dict):
|
|
115
|
+
out = dict(v)
|
|
116
|
+
if "states" not in v and "exMax" in v:
|
|
117
|
+
out["states"] = [State(**v)]
|
|
118
|
+
return out
|
|
119
|
+
return v
|
|
120
|
+
|
|
121
|
+
@field_validator("defaultState", mode="before")
|
|
122
|
+
@classmethod
|
|
123
|
+
def _v_default_state(cls, v: Any) -> int:
|
|
124
|
+
if isinstance(v, dict) and "id" in v:
|
|
125
|
+
return int(v["id"])
|
|
126
|
+
return int(v)
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def default_state(self) -> Optional[State]:
|
|
130
|
+
"""Return the default state or the first state."""
|
|
131
|
+
for state in self.states:
|
|
132
|
+
if state.id == self.defaultState:
|
|
133
|
+
return state
|
|
134
|
+
return next(iter(self.states), None)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class FilterPlacement(SpectrumOwner):
|
|
138
|
+
"""A filter placed in a microscope."""
|
|
139
|
+
|
|
140
|
+
path: Literal["EX", "EM", "BS"]
|
|
141
|
+
reflects: bool = False
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class OpticalConfig(BaseModel):
|
|
145
|
+
"""A collection of filters and light sources."""
|
|
146
|
+
|
|
147
|
+
name: str
|
|
148
|
+
filters: list[FilterPlacement]
|
|
149
|
+
camera: Optional[SpectrumOwner]
|
|
150
|
+
light: Optional[SpectrumOwner]
|
|
151
|
+
laser: Optional[int]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class Microscope(BaseModel):
|
|
155
|
+
"""A microscope with its optical configurations."""
|
|
156
|
+
|
|
157
|
+
id: str
|
|
158
|
+
name: str
|
|
159
|
+
opticalConfigs: list[OpticalConfig]
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class _MicroscopePayload(BaseModel):
|
|
163
|
+
microscope: Microscope
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class MicroscopeResponse(BaseModel):
|
|
167
|
+
"""Response for a microscope query."""
|
|
168
|
+
|
|
169
|
+
data: _MicroscopePayload
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class _ProteinPayload(BaseModel):
|
|
173
|
+
protein: Fluorophore
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class ProteinResponse(BaseModel):
|
|
177
|
+
"""Response for a protein query."""
|
|
178
|
+
|
|
179
|
+
data: _ProteinPayload
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class _DyePayload(BaseModel):
|
|
183
|
+
dye: Fluorophore
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class DyeResponse(BaseModel):
|
|
187
|
+
"""Response for a dye query."""
|
|
188
|
+
|
|
189
|
+
data: _DyePayload
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class _FilterSpectrumPayload(BaseModel):
|
|
193
|
+
spectrum: FilterSpectrum
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class FilterSpectrumResponse(BaseModel):
|
|
197
|
+
"""Response for a filter spectrum query."""
|
|
198
|
+
|
|
199
|
+
data: _FilterSpectrumPayload
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# WIP
|
|
203
|
+
# def generate_graphql_query(model: type[BaseModel], model_name: str = "") -> str:
|
|
204
|
+
# def get_fields(model: type[BaseModel]) -> str:
|
|
205
|
+
# fields = []
|
|
206
|
+
# for name, field in model.model_fields.items():
|
|
207
|
+
# annotation = field.annotation
|
|
208
|
+
|
|
209
|
+
# if isinstance(annotation, type) and issubclass(annotation, BaseModel):
|
|
210
|
+
# sub_fields = get_fields(annotation)
|
|
211
|
+
# fields.append(f"{name} {{ {sub_fields} }}")
|
|
212
|
+
# elif (
|
|
213
|
+
# get_origin(annotation) in (list, tuple)
|
|
214
|
+
# and isinstance(type_ := get_args(annotation)[0], type)
|
|
215
|
+
# and issubclass(type_, BaseModel)
|
|
216
|
+
# ):
|
|
217
|
+
# sub_fields = get_fields(type_)
|
|
218
|
+
# fields.append(f"{name} {{ {sub_fields} }}")
|
|
219
|
+
# else:
|
|
220
|
+
# fields.append(name)
|
|
221
|
+
# return "\n".join(fields)
|
|
222
|
+
|
|
223
|
+
# fields_str = get_fields(model)
|
|
224
|
+
# model_name = model_name or model.__name__
|
|
225
|
+
# return f"""
|
|
226
|
+
# query get{model_name}($id: String!) {{
|
|
227
|
+
# {model_name.lower()}(id: $id) {{
|
|
228
|
+
# {fields_str}
|
|
229
|
+
# }}
|
|
230
|
+
# }}
|
|
231
|
+
# """
|
fpbase/py.typed
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: fpbase
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Python wrapper for FPBase API
|
|
5
|
+
Project-URL: homepage, https://github.com/tlambert03/fpbasepy
|
|
6
|
+
Project-URL: repository, https://github.com/tlambert03/fpbasepy
|
|
7
|
+
Author-email: Talley Lambert <talley.lambert@gmail.com>
|
|
8
|
+
License: BSD-3-Clause
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Requires-Dist: pydantic
|
|
20
|
+
Requires-Dist: requests
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: ipython; extra == 'dev'
|
|
23
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
24
|
+
Requires-Dist: pdbpp; extra == 'dev'
|
|
25
|
+
Requires-Dist: pre-commit; extra == 'dev'
|
|
26
|
+
Requires-Dist: rich; extra == 'dev'
|
|
27
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
28
|
+
Requires-Dist: types-requests; extra == 'dev'
|
|
29
|
+
Provides-Extra: test
|
|
30
|
+
Requires-Dist: pytest; extra == 'test'
|
|
31
|
+
Requires-Dist: pytest-cov; extra == 'test'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# fpbasepy
|
|
35
|
+
|
|
36
|
+
[](https://github.com/tlambert03/fpbasepy/raw/main/LICENSE)
|
|
37
|
+
[](https://pypi.org/project/fpbasepy)
|
|
38
|
+
[](https://python.org)
|
|
39
|
+
[](https://github.com/tlambert03/fpbasepy/actions/workflows/ci.yml)
|
|
40
|
+
[](https://codecov.io/gh/tlambert03/fpbasepy)
|
|
41
|
+
|
|
42
|
+
Python wrapper for FPBase.org GraphQL API.
|
|
43
|
+
|
|
44
|
+
See https://www.fpbase.org/graphql for full documentation on the graphql schema and an interactive playground.
|
|
45
|
+
|
|
46
|
+
This library provides simple Python access to commonly-accessed data.
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
In [1]: from fpbase import get_fluorophore, get_microscope
|
|
50
|
+
|
|
51
|
+
In [2]: print(get_fluorophore("mCherry"))
|
|
52
|
+
Fluorophore(
|
|
53
|
+
name='mCherry',
|
|
54
|
+
id='ZERB6',
|
|
55
|
+
states=[
|
|
56
|
+
State(
|
|
57
|
+
id=336,
|
|
58
|
+
exMax=587.0,
|
|
59
|
+
emMax=610.0,
|
|
60
|
+
emhex='#f70000',
|
|
61
|
+
exhex='#ff4600',
|
|
62
|
+
extCoeff=72000.0,
|
|
63
|
+
qy=0.22,
|
|
64
|
+
spectra=[Spectrum(subtype='EX'), Spectrum(subtype='EM'), Spectrum(subtype='A_2P')],
|
|
65
|
+
lifetime=1.4
|
|
66
|
+
)
|
|
67
|
+
],
|
|
68
|
+
defaultState=336
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
In [3]: print(get_fluorophore("DAPI"))
|
|
72
|
+
Fluorophore(
|
|
73
|
+
name='DAPI',
|
|
74
|
+
id='15',
|
|
75
|
+
states=[
|
|
76
|
+
State(
|
|
77
|
+
id=15,
|
|
78
|
+
exMax=359.0,
|
|
79
|
+
emMax=461.0,
|
|
80
|
+
emhex='',
|
|
81
|
+
exhex='',
|
|
82
|
+
extCoeff=None,
|
|
83
|
+
qy=None,
|
|
84
|
+
spectra=[Spectrum(subtype='AB'), Spectrum(subtype='EX'), Spectrum(subtype='EM')],
|
|
85
|
+
lifetime=None
|
|
86
|
+
)
|
|
87
|
+
],
|
|
88
|
+
defaultState=None
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
In [4]: print(get_microscope("i6WL2W"))
|
|
92
|
+
Microscope(
|
|
93
|
+
id='i6WL2WdgcDMgJYtPrpZcaJ',
|
|
94
|
+
name='Example Widefield (Sedat)',
|
|
95
|
+
opticalConfigs=[
|
|
96
|
+
OpticalConfig(
|
|
97
|
+
name='Widefield Blue',
|
|
98
|
+
filters=[
|
|
99
|
+
FilterPlacement(name='Chroma ET395/25x', spectrum=Spectrum(subtype='BX'), path='EX', reflects=False),
|
|
100
|
+
FilterPlacement(name='Chroma T425lpxr', spectrum=Spectrum(subtype='LP'), path='BS', reflects=False),
|
|
101
|
+
FilterPlacement(name='Chroma ET460/50m', spectrum=Spectrum(subtype='BM'), path='EM', reflects=False)
|
|
102
|
+
],
|
|
103
|
+
camera=SpectrumOwner(name='Andor Zyla 4.2 PLUS', spectrum=Spectrum(subtype='QE')),
|
|
104
|
+
light=SpectrumOwner(name='SOLA 395', spectrum=Spectrum(subtype='PD')),
|
|
105
|
+
laser=None
|
|
106
|
+
),
|
|
107
|
+
OpticalConfig(
|
|
108
|
+
name='Widefield Dual FRET',
|
|
109
|
+
filters=[
|
|
110
|
+
FilterPlacement(name='Lumencor 470/24x', spectrum=Spectrum(subtype='BX'), path='EX', reflects=False),
|
|
111
|
+
FilterPlacement(name='Chroma 59022bs', spectrum=Spectrum(subtype='BS'), path='BS', reflects=False),
|
|
112
|
+
FilterPlacement(name='Semrock FF02-641/75', spectrum=Spectrum(subtype='BP'), path='EM', reflects=False)
|
|
113
|
+
],
|
|
114
|
+
camera=SpectrumOwner(name='Andor Zyla 4.2 PLUS', spectrum=Spectrum(subtype='QE')),
|
|
115
|
+
light=SpectrumOwner(name='SOLA 395', spectrum=Spectrum(subtype='PD')),
|
|
116
|
+
laser=None
|
|
117
|
+
),
|
|
118
|
+
OpticalConfig(
|
|
119
|
+
name='Widefield Dual Green',
|
|
120
|
+
filters=[
|
|
121
|
+
FilterPlacement(name='Lumencor 470/24x', spectrum=Spectrum(subtype='BX'), path='EX', reflects=False),
|
|
122
|
+
FilterPlacement(name='Chroma 59022bs', spectrum=Spectrum(subtype='BS'), path='BS', reflects=False),
|
|
123
|
+
FilterPlacement(name='Semrock FF03-525/50', spectrum=Spectrum(subtype='BP'), path='EM', reflects=False)
|
|
124
|
+
],
|
|
125
|
+
camera=SpectrumOwner(name='Andor Zyla 4.2 PLUS', spectrum=Spectrum(subtype='QE')),
|
|
126
|
+
light=SpectrumOwner(name='SOLA 395', spectrum=Spectrum(subtype='PD')),
|
|
127
|
+
laser=None
|
|
128
|
+
),
|
|
129
|
+
OpticalConfig(
|
|
130
|
+
name='Widefield Dual Red',
|
|
131
|
+
filters=[
|
|
132
|
+
FilterPlacement(name='Lumencor 575/25x', spectrum=Spectrum(subtype='BX'), path='EX', reflects=False),
|
|
133
|
+
FilterPlacement(name='Chroma 59022bs', spectrum=Spectrum(subtype='BS'), path='BS', reflects=False),
|
|
134
|
+
FilterPlacement(name='Semrock FF02-641/75', spectrum=Spectrum(subtype='BP'), path='EM', reflects=False)
|
|
135
|
+
],
|
|
136
|
+
camera=SpectrumOwner(name='Andor Zyla 4.2 PLUS', spectrum=Spectrum(subtype='QE')),
|
|
137
|
+
light=SpectrumOwner(name='SOLA 395', spectrum=Spectrum(subtype='PD')),
|
|
138
|
+
laser=None
|
|
139
|
+
),
|
|
140
|
+
OpticalConfig(
|
|
141
|
+
name='Widefield Far-Red',
|
|
142
|
+
filters=[
|
|
143
|
+
FilterPlacement(name='Chroma ET640/30x', spectrum=Spectrum(subtype='BX'), path='EX', reflects=False),
|
|
144
|
+
FilterPlacement(name='Chroma T660lpxr', spectrum=Spectrum(subtype='LP'), path='BS', reflects=False),
|
|
145
|
+
FilterPlacement(name='Semrock FF01-698/70', spectrum=Spectrum(subtype='BP'), path='EM', reflects=False)
|
|
146
|
+
],
|
|
147
|
+
camera=SpectrumOwner(name='Andor Zyla 4.2 PLUS', spectrum=Spectrum(subtype='QE')),
|
|
148
|
+
light=SpectrumOwner(name='SOLA 395', spectrum=Spectrum(subtype='PD')),
|
|
149
|
+
laser=None
|
|
150
|
+
),
|
|
151
|
+
OpticalConfig(
|
|
152
|
+
name='Widefield Triple Cyan',
|
|
153
|
+
filters=[
|
|
154
|
+
FilterPlacement(name='Lumencor 440/20x', spectrum=Spectrum(subtype='BX'), path='EX', reflects=False),
|
|
155
|
+
FilterPlacement(name='Chroma 69008bs', spectrum=Spectrum(subtype='BS'), path='BS', reflects=False),
|
|
156
|
+
FilterPlacement(name='Chroma ET470/24m', spectrum=Spectrum(subtype='BM'), path='EM', reflects=False)
|
|
157
|
+
],
|
|
158
|
+
camera=SpectrumOwner(name='Andor Zyla 4.2 PLUS', spectrum=Spectrum(subtype='QE')),
|
|
159
|
+
light=SpectrumOwner(name='SOLA 395', spectrum=Spectrum(subtype='PD')),
|
|
160
|
+
laser=None
|
|
161
|
+
),
|
|
162
|
+
OpticalConfig(
|
|
163
|
+
name='Widefield Triple FRET',
|
|
164
|
+
filters=[
|
|
165
|
+
FilterPlacement(name='Lumencor 440/20x', spectrum=Spectrum(subtype='BX'), path='EX', reflects=False),
|
|
166
|
+
FilterPlacement(name='Chroma 69008bs', spectrum=Spectrum(subtype='BS'), path='BS', reflects=False),
|
|
167
|
+
FilterPlacement(name='Chroma ET535/30m', spectrum=Spectrum(subtype='BM'), path='EM', reflects=False)
|
|
168
|
+
],
|
|
169
|
+
camera=SpectrumOwner(name='Andor Zyla 4.2 PLUS', spectrum=Spectrum(subtype='QE')),
|
|
170
|
+
light=SpectrumOwner(name='SOLA 395', spectrum=Spectrum(subtype='PD')),
|
|
171
|
+
laser=None
|
|
172
|
+
),
|
|
173
|
+
OpticalConfig(
|
|
174
|
+
name='Widefield Triple Red',
|
|
175
|
+
filters=[
|
|
176
|
+
FilterPlacement(name='Lumencor 575/25x', spectrum=Spectrum(subtype='BX'), path='EX', reflects=False),
|
|
177
|
+
FilterPlacement(name='Chroma 69008bs', spectrum=Spectrum(subtype='BS'), path='BS', reflects=False),
|
|
178
|
+
FilterPlacement(name='Semrock FF02-641/75', spectrum=Spectrum(subtype='BP'), path='EM', reflects=False)
|
|
179
|
+
],
|
|
180
|
+
camera=SpectrumOwner(name='Andor Zyla 4.2 PLUS', spectrum=Spectrum(subtype='QE')),
|
|
181
|
+
light=SpectrumOwner(name='SOLA 395', spectrum=Spectrum(subtype='PD')),
|
|
182
|
+
laser=None
|
|
183
|
+
),
|
|
184
|
+
OpticalConfig(
|
|
185
|
+
name='Widefield Triple Yellow',
|
|
186
|
+
filters=[
|
|
187
|
+
FilterPlacement(name='Chroma ET500/20x', spectrum=Spectrum(subtype='BX'), path='EX', reflects=False),
|
|
188
|
+
FilterPlacement(name='Chroma 69008bs', spectrum=Spectrum(subtype='BS'), path='BS', reflects=False),
|
|
189
|
+
FilterPlacement(name='Chroma ET535/30m', spectrum=Spectrum(subtype='BM'), path='EM', reflects=False)
|
|
190
|
+
],
|
|
191
|
+
camera=SpectrumOwner(name='Andor Zyla 4.2 PLUS', spectrum=Spectrum(subtype='QE')),
|
|
192
|
+
light=SpectrumOwner(name='SOLA 395', spectrum=Spectrum(subtype='PD')),
|
|
193
|
+
laser=None
|
|
194
|
+
)
|
|
195
|
+
]
|
|
196
|
+
)
|
|
197
|
+
```
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
fpbase/__init__.py,sha256=Y0HgKVLDTclYpfYyEt-tD3B58ab2fi8wN3KST0JijTE,485
|
|
2
|
+
fpbase/_fetch.py,sha256=KF307XNFXSWPmJQoLk0nmw4dCWehK0udnCk-j9b7AoI,5782
|
|
3
|
+
fpbase/_graphql.py,sha256=x1NZ4fmVvY1ghi0TE6w_qjMIwvy0DOnwJRRNHGMUTtY,1268
|
|
4
|
+
fpbase/models.py,sha256=gNyTdfSJ-Kv2N8savlNtgo3o2Y84LOOnxLRLsTF9pLY,5559
|
|
5
|
+
fpbase/py.typed,sha256=esB4cHc6c07uVkGtqf8at7ttEnprwRxwk8obY8Qumq4,187
|
|
6
|
+
fpbase-0.0.1.dist-info/METADATA,sha256=KdFuK6UEuac3fW1zDApiyl4Hc4rAyOMY9dFlekVs9JU,9176
|
|
7
|
+
fpbase-0.0.1.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
|
|
8
|
+
fpbase-0.0.1.dist-info/licenses/LICENSE,sha256=pY8jqpmjisIM7lVsbSEw4b-PUVnM8_ZSFlmLVdrXmxk,1501
|
|
9
|
+
fpbase-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023, Talley Lambert
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|