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 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
@@ -0,0 +1,62 @@
1
+ # sbts-py
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/sbts-py)](https://pypi.org/project/sbts-py/)
4
+ [![License](https://img.shields.io/pypi/l/sbts-py)](LICENSE)
5
+ [![Python](https://img.shields.io/pypi/pyversions/sbts-py)](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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,4 @@
1
+ from .sbts import Sbts, read
2
+
3
+ __version__ = "1.0.0"
4
+ __all__ = ["Sbts", "read"]
@@ -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,4 @@
1
+ xlrd>=2.0
2
+
3
+ [dev]
4
+ pytest>=7.0
@@ -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