fpbase 0.0.1__tar.gz

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.
@@ -0,0 +1,15 @@
1
+ * fpbasepy version:
2
+ * Python version:
3
+ * Operating System:
4
+
5
+ ### Description
6
+
7
+ Describe what you were trying to get done.
8
+ Tell us what happened, what went wrong, and what you expected to happen.
9
+
10
+ ### What I Did
11
+
12
+ ```
13
+ Paste the command(s) you ran and the output.
14
+ If there was a crash, please include the traceback here.
15
+ ```
@@ -0,0 +1,12 @@
1
+ ---
2
+ title: "{{ env.TITLE }}"
3
+ labels: [bug]
4
+ ---
5
+ The {{ workflow }} workflow failed on {{ date | date("YYYY-MM-DD HH:mm") }} UTC
6
+
7
+ The most recent failing test was on {{ env.PLATFORM }} py{{ env.PYTHON }}
8
+ with commit: {{ sha }}
9
+
10
+ Full run: https://github.com/{{ repo }}/actions/runs/{{ env.RUN_ID }}
11
+
12
+ (This post will be updated if another test fails, as long as this issue remains open.)
@@ -0,0 +1,10 @@
1
+ # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
2
+
3
+ version: 2
4
+ updates:
5
+ - package-ecosystem: "github-actions"
6
+ directory: "/"
7
+ schedule:
8
+ interval: "weekly"
9
+ commit-message:
10
+ prefix: "ci(dependabot):"
@@ -0,0 +1,76 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ tags:
8
+ - "v*"
9
+ pull_request:
10
+ workflow_dispatch:
11
+ schedule:
12
+ # run every week (for --pre release tests)
13
+ - cron: "0 0 * * 0"
14
+
15
+ concurrency:
16
+ group: ${{ github.workflow }}-${{ github.ref }}
17
+ cancel-in-progress: true
18
+
19
+ jobs:
20
+ check-manifest:
21
+ runs-on: ubuntu-latest
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+ - run: pipx run check-manifest
25
+
26
+ test:
27
+ uses: pyapp-kit/workflows/.github/workflows/test-pyrepo.yml@v2
28
+ with:
29
+ os: ${{ matrix.platform }}
30
+ python-version: ${{ matrix.python-version }}
31
+ coverage-upload: artifact
32
+ strategy:
33
+ fail-fast: false
34
+ matrix:
35
+ python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
36
+ platform: [ubuntu-latest]
37
+
38
+ upload_coverage:
39
+ if: always()
40
+ needs: [test]
41
+ uses: pyapp-kit/workflows/.github/workflows/upload-coverage.yml@v2
42
+ secrets:
43
+ codecov_token: ${{ secrets.CODECOV_TOKEN }}
44
+
45
+ deploy:
46
+ name: Deploy
47
+ needs: test
48
+ if: success() && startsWith(github.ref, 'refs/tags/') && github.event_name != 'schedule'
49
+ runs-on: ubuntu-latest
50
+
51
+ permissions:
52
+ id-token: write
53
+ contents: write
54
+
55
+ steps:
56
+ - uses: actions/checkout@v4
57
+ with:
58
+ fetch-depth: 0
59
+
60
+ - name: 🐍 Set up Python
61
+ uses: actions/setup-python@v5
62
+ with:
63
+ python-version: "3.x"
64
+
65
+ - name: 👷 Build
66
+ run: |
67
+ python -m pip install build
68
+ python -m build
69
+
70
+ - name: 🚢 Publish to PyPI
71
+ uses: pypa/gh-action-pypi-publish@release/v1
72
+
73
+ - uses: softprops/action-gh-release@v2
74
+ with:
75
+ generate_release_notes: true
76
+ files: "./dist/*"
@@ -0,0 +1,111 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ env/
12
+ build/
13
+ develop-eggs/
14
+ dist/
15
+ downloads/
16
+ eggs/
17
+ .eggs/
18
+ lib/
19
+ lib64/
20
+ parts/
21
+ sdist/
22
+ var/
23
+ wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+
28
+ .DS_Store
29
+
30
+ # PyInstaller
31
+ # Usually these files are written by a python script from a template
32
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
33
+ *.manifest
34
+ *.spec
35
+
36
+ # Installer logs
37
+ pip-log.txt
38
+ pip-delete-this-directory.txt
39
+
40
+ # Unit test / coverage reports
41
+ htmlcov/
42
+ .tox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ .hypothesis/
50
+ .pytest_cache/
51
+
52
+ # Translations
53
+ *.mo
54
+ *.pot
55
+
56
+ # Django stuff:
57
+ *.log
58
+ local_settings.py
59
+
60
+ # Flask stuff:
61
+ instance/
62
+ .webassets-cache
63
+
64
+ # Scrapy stuff:
65
+ .scrapy
66
+
67
+ # Sphinx documentation
68
+ docs/_build/
69
+
70
+ # PyBuilder
71
+ target/
72
+
73
+ # Jupyter Notebook
74
+ .ipynb_checkpoints
75
+
76
+ # pyenv
77
+ .python-version
78
+
79
+ # celery beat schedule file
80
+ celerybeat-schedule
81
+
82
+ # SageMath parsed files
83
+ *.sage.py
84
+
85
+ # dotenv
86
+ .env
87
+
88
+ # virtualenv
89
+ .venv
90
+ venv/
91
+ ENV/
92
+
93
+ # Spyder project settings
94
+ .spyderproject
95
+ .spyproject
96
+
97
+ # Rope project settings
98
+ .ropeproject
99
+
100
+ # mkdocs documentation
101
+ /site
102
+
103
+ # mypy
104
+ .mypy_cache/
105
+
106
+ # ruff
107
+ .ruff_cache/
108
+
109
+ # IDE settings
110
+ .vscode/
111
+ .idea/
@@ -0,0 +1,37 @@
1
+ # enable pre-commit.ci at https://pre-commit.ci/
2
+ # it adds:
3
+ # 1. auto fixing pull requests
4
+ # 2. auto updating the pre-commit configuration
5
+ ci:
6
+ autoupdate_schedule: monthly
7
+ autofix_commit_msg: "style(pre-commit.ci): auto fixes [...]"
8
+ autoupdate_commit_msg: "ci(pre-commit.ci): autoupdate"
9
+
10
+ repos:
11
+ - repo: https://github.com/abravalheri/validate-pyproject
12
+ rev: v0.23
13
+ hooks:
14
+ - id: validate-pyproject
15
+
16
+ - repo: https://github.com/crate-ci/typos
17
+ rev: typos-dict-v0.11.35
18
+ hooks:
19
+ - id: typos
20
+ args: [--force-exclude] # omitting --write-changes
21
+
22
+ - repo: https://github.com/astral-sh/ruff-pre-commit
23
+ rev: v0.7.4
24
+ hooks:
25
+ - id: ruff
26
+ args: [--fix] # may also add '--unsafe-fixes'
27
+ - id: ruff-format
28
+
29
+ - repo: https://github.com/pre-commit/mirrors-mypy
30
+ rev: v1.13.0
31
+ hooks:
32
+ - id: mypy
33
+ files: "^src/"
34
+ additional_dependencies:
35
+ - pydantic
36
+ - types-requests
37
+
fpbase-0.0.1/LICENSE ADDED
@@ -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.
fpbase-0.0.1/PKG-INFO 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
+ [![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
+ ```
fpbase-0.0.1/README.md ADDED
@@ -0,0 +1,164 @@
1
+ # fpbasepy
2
+
3
+ [![License](https://img.shields.io/pypi/l/fpbasepy.svg?color=green)](https://github.com/tlambert03/fpbasepy/raw/main/LICENSE)
4
+ [![PyPI](https://img.shields.io/pypi/v/fpbasepy.svg?color=green)](https://pypi.org/project/fpbasepy)
5
+ [![Python Version](https://img.shields.io/pypi/pyversions/fpbasepy.svg?color=green)](https://python.org)
6
+ [![CI](https://github.com/tlambert03/fpbasepy/actions/workflows/ci.yml/badge.svg)](https://github.com/tlambert03/fpbasepy/actions/workflows/ci.yml)
7
+ [![codecov](https://codecov.io/gh/tlambert03/fpbasepy/branch/main/graph/badge.svg)](https://codecov.io/gh/tlambert03/fpbasepy)
8
+
9
+ Python wrapper for FPBase.org GraphQL API.
10
+
11
+ See https://www.fpbase.org/graphql for full documentation on the graphql schema and an interactive playground.
12
+
13
+ This library provides simple Python access to commonly-accessed data.
14
+
15
+ ```python
16
+ In [1]: from fpbase import get_fluorophore, get_microscope
17
+
18
+ In [2]: print(get_fluorophore("mCherry"))
19
+ Fluorophore(
20
+ name='mCherry',
21
+ id='ZERB6',
22
+ states=[
23
+ State(
24
+ id=336,
25
+ exMax=587.0,
26
+ emMax=610.0,
27
+ emhex='#f70000',
28
+ exhex='#ff4600',
29
+ extCoeff=72000.0,
30
+ qy=0.22,
31
+ spectra=[Spectrum(subtype='EX'), Spectrum(subtype='EM'), Spectrum(subtype='A_2P')],
32
+ lifetime=1.4
33
+ )
34
+ ],
35
+ defaultState=336
36
+ )
37
+
38
+ In [3]: print(get_fluorophore("DAPI"))
39
+ Fluorophore(
40
+ name='DAPI',
41
+ id='15',
42
+ states=[
43
+ State(
44
+ id=15,
45
+ exMax=359.0,
46
+ emMax=461.0,
47
+ emhex='',
48
+ exhex='',
49
+ extCoeff=None,
50
+ qy=None,
51
+ spectra=[Spectrum(subtype='AB'), Spectrum(subtype='EX'), Spectrum(subtype='EM')],
52
+ lifetime=None
53
+ )
54
+ ],
55
+ defaultState=None
56
+ )
57
+
58
+ In [4]: print(get_microscope("i6WL2W"))
59
+ Microscope(
60
+ id='i6WL2WdgcDMgJYtPrpZcaJ',
61
+ name='Example Widefield (Sedat)',
62
+ opticalConfigs=[
63
+ OpticalConfig(
64
+ name='Widefield Blue',
65
+ filters=[
66
+ FilterPlacement(name='Chroma ET395/25x', spectrum=Spectrum(subtype='BX'), path='EX', reflects=False),
67
+ FilterPlacement(name='Chroma T425lpxr', spectrum=Spectrum(subtype='LP'), path='BS', reflects=False),
68
+ FilterPlacement(name='Chroma ET460/50m', spectrum=Spectrum(subtype='BM'), path='EM', reflects=False)
69
+ ],
70
+ camera=SpectrumOwner(name='Andor Zyla 4.2 PLUS', spectrum=Spectrum(subtype='QE')),
71
+ light=SpectrumOwner(name='SOLA 395', spectrum=Spectrum(subtype='PD')),
72
+ laser=None
73
+ ),
74
+ OpticalConfig(
75
+ name='Widefield Dual FRET',
76
+ filters=[
77
+ FilterPlacement(name='Lumencor 470/24x', spectrum=Spectrum(subtype='BX'), path='EX', reflects=False),
78
+ FilterPlacement(name='Chroma 59022bs', spectrum=Spectrum(subtype='BS'), path='BS', reflects=False),
79
+ FilterPlacement(name='Semrock FF02-641/75', spectrum=Spectrum(subtype='BP'), path='EM', reflects=False)
80
+ ],
81
+ camera=SpectrumOwner(name='Andor Zyla 4.2 PLUS', spectrum=Spectrum(subtype='QE')),
82
+ light=SpectrumOwner(name='SOLA 395', spectrum=Spectrum(subtype='PD')),
83
+ laser=None
84
+ ),
85
+ OpticalConfig(
86
+ name='Widefield Dual Green',
87
+ filters=[
88
+ FilterPlacement(name='Lumencor 470/24x', spectrum=Spectrum(subtype='BX'), path='EX', reflects=False),
89
+ FilterPlacement(name='Chroma 59022bs', spectrum=Spectrum(subtype='BS'), path='BS', reflects=False),
90
+ FilterPlacement(name='Semrock FF03-525/50', spectrum=Spectrum(subtype='BP'), path='EM', reflects=False)
91
+ ],
92
+ camera=SpectrumOwner(name='Andor Zyla 4.2 PLUS', spectrum=Spectrum(subtype='QE')),
93
+ light=SpectrumOwner(name='SOLA 395', spectrum=Spectrum(subtype='PD')),
94
+ laser=None
95
+ ),
96
+ OpticalConfig(
97
+ name='Widefield Dual Red',
98
+ filters=[
99
+ FilterPlacement(name='Lumencor 575/25x', spectrum=Spectrum(subtype='BX'), path='EX', reflects=False),
100
+ FilterPlacement(name='Chroma 59022bs', spectrum=Spectrum(subtype='BS'), path='BS', reflects=False),
101
+ FilterPlacement(name='Semrock FF02-641/75', spectrum=Spectrum(subtype='BP'), 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 Far-Red',
109
+ filters=[
110
+ FilterPlacement(name='Chroma ET640/30x', spectrum=Spectrum(subtype='BX'), path='EX', reflects=False),
111
+ FilterPlacement(name='Chroma T660lpxr', spectrum=Spectrum(subtype='LP'), path='BS', reflects=False),
112
+ FilterPlacement(name='Semrock FF01-698/70', 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 Triple Cyan',
120
+ filters=[
121
+ FilterPlacement(name='Lumencor 440/20x', spectrum=Spectrum(subtype='BX'), path='EX', reflects=False),
122
+ FilterPlacement(name='Chroma 69008bs', spectrum=Spectrum(subtype='BS'), path='BS', reflects=False),
123
+ FilterPlacement(name='Chroma ET470/24m', spectrum=Spectrum(subtype='BM'), 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 Triple FRET',
131
+ filters=[
132
+ FilterPlacement(name='Lumencor 440/20x', spectrum=Spectrum(subtype='BX'), path='EX', reflects=False),
133
+ FilterPlacement(name='Chroma 69008bs', spectrum=Spectrum(subtype='BS'), path='BS', reflects=False),
134
+ FilterPlacement(name='Chroma ET535/30m', spectrum=Spectrum(subtype='BM'), 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 Triple Red',
142
+ filters=[
143
+ FilterPlacement(name='Lumencor 575/25x', spectrum=Spectrum(subtype='BX'), path='EX', reflects=False),
144
+ FilterPlacement(name='Chroma 69008bs', spectrum=Spectrum(subtype='BS'), path='BS', reflects=False),
145
+ FilterPlacement(name='Semrock FF02-641/75', 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 Yellow',
153
+ filters=[
154
+ FilterPlacement(name='Chroma ET500/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 ET535/30m', 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
+ ]
163
+ )
164
+ ```
@@ -0,0 +1,123 @@
1
+ # https://peps.python.org/pep-0517/
2
+ [build-system]
3
+ requires = ["hatchling", "hatch-vcs"]
4
+ build-backend = "hatchling.build"
5
+
6
+ # https://hatch.pypa.io/latest/config/metadata/
7
+ [tool.hatch.version]
8
+ source = "vcs"
9
+
10
+ # read more about configuring hatch at:
11
+ # https://hatch.pypa.io/latest/config/build/
12
+ [tool.hatch.build.targets.wheel]
13
+ only-include = ["src"]
14
+ sources = ["src"]
15
+
16
+ # https://peps.python.org/pep-0621/
17
+ [project]
18
+ name = "fpbase"
19
+ dynamic = ["version"]
20
+ description = "Python wrapper for FPBase API"
21
+ readme = "README.md"
22
+ requires-python = ">=3.9"
23
+ license = { text = "BSD-3-Clause" }
24
+ authors = [{ name = "Talley Lambert", email = "talley.lambert@gmail.com" }]
25
+ classifiers = [
26
+ "Development Status :: 3 - Alpha",
27
+ "License :: OSI Approved :: BSD License",
28
+ "Programming Language :: Python :: 3",
29
+ "Programming Language :: Python :: 3.9",
30
+ "Programming Language :: Python :: 3.10",
31
+ "Programming Language :: Python :: 3.11",
32
+ "Programming Language :: Python :: 3.12",
33
+ "Programming Language :: Python :: 3.13",
34
+ "Typing :: Typed",
35
+ ]
36
+ dependencies = ['pydantic', 'requests']
37
+
38
+ # https://peps.python.org/pep-0621/#dependencies-optional-dependencies
39
+ [project.optional-dependencies]
40
+ test = ["pytest", "pytest-cov"]
41
+ dev = [
42
+ "ipython",
43
+ "types-requests",
44
+ "mypy",
45
+ "pdbpp", # https://github.com/pdbpp/pdbpp
46
+ "pre-commit",
47
+ "rich", # https://github.com/Textualize/rich
48
+ "ruff",
49
+ ]
50
+
51
+ [project.urls]
52
+ homepage = "https://github.com/tlambert03/fpbasepy"
53
+ repository = "https://github.com/tlambert03/fpbasepy"
54
+
55
+ # https://docs.astral.sh/ruff
56
+ [tool.ruff]
57
+ line-length = 88
58
+ target-version = "py39"
59
+ src = ["src"]
60
+
61
+ # https://docs.astral.sh/ruff/rules
62
+ [tool.ruff.lint]
63
+ pydocstyle = { convention = "numpy" }
64
+ select = [
65
+ "E", # style errors
66
+ "W", # style warnings
67
+ "F", # flakes
68
+ "D", # pydocstyle
69
+ "D417", # Missing argument descriptions in Docstrings
70
+ "I", # isort
71
+ "UP", # pyupgrade
72
+ "C4", # flake8-comprehensions
73
+ "B", # flake8-bugbear
74
+ "A001", # flake8-builtins
75
+ "RUF", # ruff-specific rules
76
+ "TCH", # flake8-type-checking
77
+ "TID", # flake8-tidy-imports
78
+ ]
79
+ ignore = [
80
+ "D401", # First line should be in imperative mood (remove to opt in)
81
+ ]
82
+
83
+ [tool.ruff.lint.per-file-ignores]
84
+ "tests/*.py" = ["D", "S"]
85
+
86
+ # https://docs.astral.sh/ruff/formatter/
87
+ [tool.ruff.format]
88
+ docstring-code-format = true
89
+ skip-magic-trailing-comma = false # default is false
90
+
91
+ # https://mypy.readthedocs.io/en/stable/config_file.html
92
+ [tool.mypy]
93
+ files = "src/**/"
94
+ strict = true
95
+ disallow_any_generics = false
96
+ disallow_subclassing_any = false
97
+ show_error_codes = true
98
+ pretty = true
99
+
100
+ # https://docs.pytest.org/
101
+ [tool.pytest.ini_options]
102
+ minversion = "7.0"
103
+ testpaths = ["tests"]
104
+ filterwarnings = ["error"]
105
+
106
+ # https://coverage.readthedocs.io/
107
+ [tool.coverage.report]
108
+ show_missing = true
109
+ exclude_lines = [
110
+ "pragma: no cover",
111
+ "if TYPE_CHECKING:",
112
+ "@overload",
113
+ "except ImportError",
114
+ "\\.\\.\\.",
115
+ "raise NotImplementedError()",
116
+ "pass",
117
+ ]
118
+
119
+ [tool.coverage.run]
120
+ source = ["fpbase"]
121
+
122
+ [tool.check-manifest]
123
+ ignore = [".pre-commit-config.yaml", ".ruff_cache/**/*", "tests/**/*"]
@@ -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
+ ]
@@ -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)
@@ -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
+ """
@@ -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
+ # """
@@ -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,23 @@
1
+ import pytest
2
+
3
+ from fpbase import get_filter, get_fluorophore, get_microscope
4
+
5
+
6
+ def test_get_microscope() -> None:
7
+ scope = get_microscope("wKqWbgApvguSNDSRZNSfpN")
8
+ assert scope.name == "Example Simple Widefield"
9
+
10
+
11
+ @pytest.mark.parametrize("name", ["EGFP", "Alexa Fluor 488"])
12
+ def test_get_fluor(name: str) -> None:
13
+ fluor = get_fluorophore(name)
14
+ assert fluor.name == name
15
+ assert fluor.default_state
16
+ assert fluor.default_state.excitation_spectrum is not None
17
+ assert fluor.default_state.emission_spectrum is not None
18
+
19
+
20
+ @pytest.mark.parametrize("name", ["Chroma ET525/50m", "Semrock FF01-520/35"])
21
+ def test_get_filter(name: str) -> None:
22
+ filt = get_filter(name)
23
+ assert filt.name == name