sbts-py 1.0.0__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.
- sbts_py-1.0.0/LICENSE +21 -0
- sbts_py-1.0.0/PKG-INFO +15 -0
- sbts_py-1.0.0/README.md +62 -0
- sbts_py-1.0.0/pyproject.toml +24 -0
- sbts_py-1.0.0/setup.cfg +4 -0
- sbts_py-1.0.0/src/sbts/__init__.py +4 -0
- sbts_py-1.0.0/src/sbts/sbts.py +128 -0
- sbts_py-1.0.0/src/sbts_py.egg-info/PKG-INFO +15 -0
- sbts_py-1.0.0/src/sbts_py.egg-info/SOURCES.txt +11 -0
- sbts_py-1.0.0/src/sbts_py.egg-info/dependency_links.txt +1 -0
- sbts_py-1.0.0/src/sbts_py.egg-info/requires.txt +4 -0
- sbts_py-1.0.0/src/sbts_py.egg-info/top_level.txt +1 -0
- sbts_py-1.0.0/tests/test_sbts.py +95 -0
sbts_py-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 123VincentB
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
sbts_py-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sbts-py
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Parser for Gigahertz-Optik S-BTS2048 and S-BTS256 spectral data files
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: xlrd>=2.0
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
15
|
+
Dynamic: license-file
|
sbts_py-1.0.0/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# sbts-py
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/sbts-py/)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](https://pypi.org/project/sbts-py/)
|
|
6
|
+
|
|
7
|
+
Parser for spectral data files exported by **Gigahertz-Optik S-BTS2048** and **S-BTS256** spectroradiometers (`.xls` BIFF8 format).
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install sbts-py
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from sbts import read
|
|
19
|
+
|
|
20
|
+
# Single measurement
|
|
21
|
+
measurements = read('measurement.xls')
|
|
22
|
+
s = measurements[0]
|
|
23
|
+
|
|
24
|
+
print(s.instrument) # 'S-BTS2048' or 'S-BTS256'
|
|
25
|
+
print(s.wavelengths[0]) # 350.0 (nm)
|
|
26
|
+
print(len(s.spd)) # 701 (S-BTS2048) or 371 (S-BTS256)
|
|
27
|
+
|
|
28
|
+
print(s.data.get('CCT')) # 4043.16
|
|
29
|
+
print(s.data.get('CRI: Ra')) # 82.45
|
|
30
|
+
print(s.data.get('date')) # '23.06.2025'
|
|
31
|
+
|
|
32
|
+
# Multi-measurement file
|
|
33
|
+
measurements = read('multi.xls')
|
|
34
|
+
for m in measurements:
|
|
35
|
+
print(m.data.get('CCT'), m.data.get('samplenumber'))
|
|
36
|
+
|
|
37
|
+
# S-BTS256 — diode progression
|
|
38
|
+
s = read('S-BTS256.xls')[0]
|
|
39
|
+
print(s.diode_time_ms) # [0.0, 0.02, ...] or None
|
|
40
|
+
print(s.data.get('Pst')) # flicker Pst
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Returned object
|
|
44
|
+
|
|
45
|
+
`read()` always returns `list[Sbts]` — a list of one element for a single-measurement file, N elements for a multi-measurement file.
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
@dataclass
|
|
49
|
+
class Sbts:
|
|
50
|
+
wavelengths: list[float] # wavelengths in nm
|
|
51
|
+
spd: list[float] # spectral irradiance (W/m²)/nm
|
|
52
|
+
data: dict[str, Any] # all scalar key/value pairs from the file
|
|
53
|
+
diode_time_ms: list[float] | None
|
|
54
|
+
diode_values: list[float] | None
|
|
55
|
+
instrument: str | None # 'S-BTS2048' | 'S-BTS256' | None
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
All missing or unavailable values (`-9999.0` convention) are normalized to `None`.
|
|
59
|
+
|
|
60
|
+
## License
|
|
61
|
+
|
|
62
|
+
MIT
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sbts-py"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Parser for Gigahertz-Optik S-BTS2048 and S-BTS256 spectral data files"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Programming Language :: Python :: 3.10",
|
|
15
|
+
"Programming Language :: Python :: 3.11",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
]
|
|
18
|
+
dependencies = ["xlrd>=2.0"]
|
|
19
|
+
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
dev = ["pytest>=7.0"]
|
|
22
|
+
|
|
23
|
+
[tool.setuptools.packages.find]
|
|
24
|
+
where = ["src"]
|
sbts_py-1.0.0/setup.cfg
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import xlrd
|
|
8
|
+
from xlrd import XL_CELL_EMPTY, XL_CELL_NUMBER, XL_CELL_TEXT
|
|
9
|
+
|
|
10
|
+
_NULL_NUMERIC = {-9999.0}
|
|
11
|
+
_SVM_NULL = {-1.0}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class Sbts:
|
|
16
|
+
wavelengths: list[float]
|
|
17
|
+
spd: list[float]
|
|
18
|
+
data: dict[str, Any]
|
|
19
|
+
diode_time_ms: list[float] | None = None
|
|
20
|
+
diode_values: list[float] | None = None
|
|
21
|
+
instrument: str | None = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _is_separator_row(ws, row_idx: int) -> bool:
|
|
25
|
+
if all(t == XL_CELL_EMPTY for t in ws.row_types(row_idx)):
|
|
26
|
+
return True
|
|
27
|
+
return ws.cell_type(row_idx, 0) == XL_CELL_TEXT and ws.cell_value(row_idx, 0).strip() == ''
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _normalize_value(key: str, v: Any) -> Any:
|
|
31
|
+
if isinstance(v, str) and v.strip() == '':
|
|
32
|
+
return None
|
|
33
|
+
if isinstance(v, float):
|
|
34
|
+
if v in _NULL_NUMERIC:
|
|
35
|
+
return None
|
|
36
|
+
if key == 'SVM' and v in _SVM_NULL:
|
|
37
|
+
return None
|
|
38
|
+
return v
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _detect_instrument(data: dict, n_spd_points: int) -> str | None:
|
|
42
|
+
if 'device type' in data:
|
|
43
|
+
return 'S-BTS2048'
|
|
44
|
+
if n_spd_points == 701:
|
|
45
|
+
return 'S-BTS2048'
|
|
46
|
+
if n_spd_points == 371:
|
|
47
|
+
return 'S-BTS256'
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _parse_sheet(ws, col_idx: int) -> Sbts:
|
|
52
|
+
data: dict[str, Any] = {}
|
|
53
|
+
wavelengths: list[float] = []
|
|
54
|
+
spd: list[float] = []
|
|
55
|
+
diode_time_ms_list: list[float] = []
|
|
56
|
+
diode_values_list: list[float] = []
|
|
57
|
+
|
|
58
|
+
mode: str | None = None # None | 'spd' | 'diode'
|
|
59
|
+
|
|
60
|
+
for row_idx in range(ws.nrows):
|
|
61
|
+
if _is_separator_row(ws, row_idx):
|
|
62
|
+
mode = None
|
|
63
|
+
continue
|
|
64
|
+
|
|
65
|
+
cell_type_0 = ws.cell_type(row_idx, 0)
|
|
66
|
+
col0_val = ws.cell_value(row_idx, 0)
|
|
67
|
+
|
|
68
|
+
if cell_type_0 == XL_CELL_NUMBER:
|
|
69
|
+
if mode == 'spd':
|
|
70
|
+
wavelengths.append(float(col0_val))
|
|
71
|
+
spd.append(float(ws.cell_value(row_idx, col_idx)))
|
|
72
|
+
elif mode == 'diode':
|
|
73
|
+
diode_time_ms_list.append(float(col0_val))
|
|
74
|
+
diode_values_list.append(float(ws.cell_value(row_idx, col_idx)))
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
if cell_type_0 != XL_CELL_TEXT:
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
key = col0_val.strip()
|
|
81
|
+
if not key:
|
|
82
|
+
mode = None
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
if key == 'wavelength /nm':
|
|
86
|
+
mode = 'spd'
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
if key == 'time / ms':
|
|
90
|
+
mode = 'diode'
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
val = ws.cell_value(row_idx, col_idx)
|
|
94
|
+
data[key] = _normalize_value(key, val)
|
|
95
|
+
|
|
96
|
+
instrument = _detect_instrument(data, len(wavelengths))
|
|
97
|
+
|
|
98
|
+
return Sbts(
|
|
99
|
+
wavelengths=wavelengths,
|
|
100
|
+
spd=spd,
|
|
101
|
+
data=data,
|
|
102
|
+
diode_time_ms=diode_time_ms_list or None,
|
|
103
|
+
diode_values=diode_values_list or None,
|
|
104
|
+
instrument=instrument,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def read(path: str | Path) -> list[Sbts]:
|
|
109
|
+
"""
|
|
110
|
+
Parse un fichier .xls Gigahertz-Optik S-BTS2048 ou S-BTS256.
|
|
111
|
+
|
|
112
|
+
Retourne toujours une list[Sbts] :
|
|
113
|
+
- liste de 1 élément pour un fichier single-mesure
|
|
114
|
+
- liste de N éléments pour un fichier multi-mesures (N colonnes de valeurs)
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
FileNotFoundError: fichier introuvable
|
|
118
|
+
ValueError: feuille 'Sheet3' absente ou fichier non parseable
|
|
119
|
+
"""
|
|
120
|
+
wb = xlrd.open_workbook(str(path))
|
|
121
|
+
|
|
122
|
+
if 'Sheet3' not in wb.sheet_names():
|
|
123
|
+
raise ValueError(f"Feuille 'Sheet3' absente dans {path}")
|
|
124
|
+
|
|
125
|
+
ws = wb.sheet_by_name('Sheet3')
|
|
126
|
+
n_measures = ws.ncols - 1
|
|
127
|
+
|
|
128
|
+
return [_parse_sheet(ws, col_idx) for col_idx in range(1, n_measures + 1)]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sbts-py
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Parser for Gigahertz-Optik S-BTS2048 and S-BTS256 spectral data files
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: xlrd>=2.0
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
15
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/sbts/__init__.py
|
|
5
|
+
src/sbts/sbts.py
|
|
6
|
+
src/sbts_py.egg-info/PKG-INFO
|
|
7
|
+
src/sbts_py.egg-info/SOURCES.txt
|
|
8
|
+
src/sbts_py.egg-info/dependency_links.txt
|
|
9
|
+
src/sbts_py.egg-info/requires.txt
|
|
10
|
+
src/sbts_py.egg-info/top_level.txt
|
|
11
|
+
tests/test_sbts.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sbts
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from sbts import read
|
|
3
|
+
|
|
4
|
+
FIXTURE_2048 = 'tests/fixtures/S_BTS2048_single.xls'
|
|
5
|
+
FIXTURE_2048_MULTI = 'tests/fixtures/S_BTS2048_multi.xls'
|
|
6
|
+
FIXTURE_256 = 'tests/fixtures/S_BTS256_single.xls'
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_read_returns_list():
|
|
10
|
+
result = read(FIXTURE_2048)
|
|
11
|
+
assert isinstance(result, list)
|
|
12
|
+
assert len(result) == 1
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_multi_returns_multiple():
|
|
16
|
+
result = read(FIXTURE_2048_MULTI)
|
|
17
|
+
assert isinstance(result, list)
|
|
18
|
+
assert len(result) == 3
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_instrument_detection_2048():
|
|
22
|
+
s = read(FIXTURE_2048)[0]
|
|
23
|
+
assert s.instrument == 'S-BTS2048'
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_instrument_detection_256():
|
|
27
|
+
s = read(FIXTURE_256)[0]
|
|
28
|
+
assert s.instrument == 'S-BTS256'
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_spd_2048():
|
|
32
|
+
s = read(FIXTURE_2048)[0]
|
|
33
|
+
assert len(s.wavelengths) == 701
|
|
34
|
+
assert len(s.spd) == len(s.wavelengths)
|
|
35
|
+
assert s.wavelengths[0] == 350.0
|
|
36
|
+
assert s.wavelengths[-1] == 1050.0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_spd_256():
|
|
40
|
+
s = read(FIXTURE_256)[0]
|
|
41
|
+
assert len(s.wavelengths) == 371
|
|
42
|
+
assert len(s.spd) == len(s.wavelengths)
|
|
43
|
+
assert s.wavelengths[0] == 380.0
|
|
44
|
+
assert s.wavelengths[-1] == 750.0
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_data_contains_scalar_keys():
|
|
48
|
+
s = read(FIXTURE_2048)[0]
|
|
49
|
+
assert 'CCT' in s.data
|
|
50
|
+
assert 'CRI: Ra' in s.data
|
|
51
|
+
assert 'date' in s.data
|
|
52
|
+
assert 'serial number' in s.data
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_minus9999_becomes_none():
|
|
56
|
+
s = read(FIXTURE_256)[0]
|
|
57
|
+
assert s.data.get('saturation') is None
|
|
58
|
+
assert s.data.get('temperature') is None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_svm_minus1_becomes_none():
|
|
62
|
+
s = read(FIXTURE_256)[0]
|
|
63
|
+
assert s.data.get('SVM') is None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_photometric_values_present():
|
|
67
|
+
s = read(FIXTURE_2048)[0]
|
|
68
|
+
assert s.data.get('photopic') is not None
|
|
69
|
+
assert s.data.get('photometric unit') == 'lx'
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_diode_progression_256():
|
|
73
|
+
s = read(FIXTURE_256)[0]
|
|
74
|
+
assert s.diode_time_ms is not None
|
|
75
|
+
assert s.diode_values is not None
|
|
76
|
+
assert len(s.diode_time_ms) == len(s.diode_values)
|
|
77
|
+
assert s.diode_time_ms[0] == 0.0
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_diode_progression_2048_absent():
|
|
81
|
+
s = read(FIXTURE_2048)[0]
|
|
82
|
+
assert s.diode_time_ms is None
|
|
83
|
+
assert s.diode_values is None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_key_stripping():
|
|
87
|
+
s = read(FIXTURE_2048)[0]
|
|
88
|
+
assert 'Blue Light Hazard [350 nm - 700 nm]' in s.data
|
|
89
|
+
assert 'ACGIH(skin & eye) [350 nm - 400 nm]' in s.data
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_multi_independent_measurements():
|
|
93
|
+
measurements = read(FIXTURE_2048_MULTI)
|
|
94
|
+
ccts = [m.data.get('CCT') for m in measurements]
|
|
95
|
+
assert len(set(ccts)) > 1
|