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 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,5 @@
1
+ You may remove this file if you don't intend to add types to your package
2
+
3
+ Details at:
4
+
5
+ https://mypy.readthedocs.io/en/stable/installed_packages.html#creating-pep-561-compatible-packages
@@ -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
+ [![License](https://img.shields.io/pypi/l/fpbasepy.svg?color=green)](https://github.com/tlambert03/fpbasepy/raw/main/LICENSE)
37
+ [![PyPI](https://img.shields.io/pypi/v/fpbasepy.svg?color=green)](https://pypi.org/project/fpbasepy)
38
+ [![Python Version](https://img.shields.io/pypi/pyversions/fpbasepy.svg?color=green)](https://python.org)
39
+ [![CI](https://github.com/tlambert03/fpbasepy/actions/workflows/ci.yml/badge.svg)](https://github.com/tlambert03/fpbasepy/actions/workflows/ci.yml)
40
+ [![codecov](https://codecov.io/gh/tlambert03/fpbasepy/branch/main/graph/badge.svg)](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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.26.3
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.