isof 0.1.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.
- isof-0.1.0/LICENSE +21 -0
- isof-0.1.0/PKG-INFO +258 -0
- isof-0.1.0/README.md +223 -0
- isof-0.1.0/isof/__init__.py +93 -0
- isof-0.1.0/isof/exceptions.py +71 -0
- isof-0.1.0/isof/models.py +336 -0
- isof-0.1.0/isof/parser.py +574 -0
- isof-0.1.0/isof/signature.py +310 -0
- isof-0.1.0/isof/trust/isofind_issuing_ca.pem +27 -0
- isof-0.1.0/isof/trust/isofind_root_ca.pem +36 -0
- isof-0.1.0/isof.egg-info/PKG-INFO +258 -0
- isof-0.1.0/isof.egg-info/SOURCES.txt +17 -0
- isof-0.1.0/isof.egg-info/dependency_links.txt +1 -0
- isof-0.1.0/isof.egg-info/requires.txt +11 -0
- isof-0.1.0/isof.egg-info/top_level.txt +1 -0
- isof-0.1.0/pyproject.toml +64 -0
- isof-0.1.0/setup.cfg +4 -0
- isof-0.1.0/tests/test_parser.py +301 -0
- isof-0.1.0/tests/test_signatures.py +370 -0
isof-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Colin Ferrari
|
|
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.
|
isof-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: isof
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lecteur et vérificateur du format ISOF — échange de données isotopiques
|
|
5
|
+
Author-email: IsoFind SAS <colin.ferrari@isofind.tech>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://isofind.tech
|
|
8
|
+
Project-URL: Repository, https://github.com/ColinFerrari/isof
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/ColinFerrari/isof/issues
|
|
10
|
+
Project-URL: Specification, https://isofind.tech/isof-spec
|
|
11
|
+
Keywords: isotopes,geochemistry,traceability,forensics,ISOF
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Chemistry
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: cryptography>=41.0
|
|
26
|
+
Provides-Extra: pandas
|
|
27
|
+
Requires-Dist: pandas>=1.5; extra == "pandas"
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
31
|
+
Requires-Dist: pandas>=1.5; extra == "dev"
|
|
32
|
+
Requires-Dist: ruff; extra == "dev"
|
|
33
|
+
Requires-Dist: mypy; extra == "dev"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# isof
|
|
37
|
+
|
|
38
|
+
Python reader and verifier for the **ISOF v1.0** format, an open standard for exchanging geochemical isotopic data.
|
|
39
|
+
|
|
40
|
+
This format allows exchanging in a single file the isotopic data along with all associated metadata (analytical methods used for each sample, purification yields, analysis pipeline...), while enabling traceability of modifications once files are produced and certifying the origin of the file (laboratory certification).
|
|
41
|
+
|
|
42
|
+
**Sovereignty and Confidentiality:** Signature verification is a 100% local process. No data is sent to a third-party server for validation.
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
import isof
|
|
46
|
+
|
|
47
|
+
report = isof.load("analyse_bolivie.isof")
|
|
48
|
+
|
|
49
|
+
if report.is_authentic():
|
|
50
|
+
print(f"Signed by: {report.signature.signed_by}")
|
|
51
|
+
|
|
52
|
+
df = report.to_pandas()
|
|
53
|
+
print(df[["sample_name", "element", "ratio", "ratio_2se"]])
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The ISOF format is used by [IsoFind](https://isofind.tech), but this parser is independent and can read any file compliant with the [ISOF v1.0 specification](https://isofind.tech/isof-spec).
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Installation
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install isof
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
With pandas support:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pip install isof[pandas]
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Requires Python ≥ 3.9.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Usage
|
|
77
|
+
|
|
78
|
+
### Load a file
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
import isof
|
|
82
|
+
|
|
83
|
+
report = isof.load("analyse.isof")
|
|
84
|
+
print(report)
|
|
85
|
+
# <ISOfDocument v1.0 — 12 échantillon(s) — IGE Grenoble>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
From a JSON string (API, database):
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
with open("analyse.isof") as f:
|
|
92
|
+
text = f.read()
|
|
93
|
+
|
|
94
|
+
report = isof.loads(text)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Verify integrity
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
# Simple check
|
|
101
|
+
if report.is_authentic():
|
|
102
|
+
print("Data integrity confirmed")
|
|
103
|
+
|
|
104
|
+
# Detailed result
|
|
105
|
+
result = report.verify()
|
|
106
|
+
print(result.valid) # bool
|
|
107
|
+
print(result.level) # 1 (SHA-256) or 2 (IsoFind PKI)
|
|
108
|
+
print(result.signer) # organisation or certificate CN
|
|
109
|
+
print(result.signed_at) # ISO 8601 timestamp
|
|
110
|
+
print(result.reason) # None if valid, error message otherwise
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Two signature levels coexist in the format:
|
|
114
|
+
|
|
115
|
+
| Level | Mechanism | Guarantee |
|
|
116
|
+
| ----- | -------------------------- | -------------------------------------------------------------- |
|
|
117
|
+
| 1 | SHA-256 over the data | Integrity — file has not been modified since export |
|
|
118
|
+
| 2 | ECDSA P-256 + IsoFind PKI | Authenticity — signed by a laboratory certified by IsoFind |
|
|
119
|
+
|
|
120
|
+
Verification works **offline**: IsoFind certificates are embedded in the package.
|
|
121
|
+
|
|
122
|
+
### Access data
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
# Sample list
|
|
126
|
+
for sample in report.samples:
|
|
127
|
+
print(sample.id, sample.name, sample.classification)
|
|
128
|
+
for iso in sample.isotope_data:
|
|
129
|
+
print(f" {iso.element} {iso.system} = {iso.ratio} ± {iso.ratio_2se}")
|
|
130
|
+
|
|
131
|
+
# Look up a sample by identifier
|
|
132
|
+
s = report.sample("BOL-24-01")
|
|
133
|
+
|
|
134
|
+
# Filter
|
|
135
|
+
sources = report.filter_samples(classification="source")
|
|
136
|
+
sb_samples = report.filter_samples(element="Sb")
|
|
137
|
+
combined = report.filter_samples(element="Pb", material_type="minerai")
|
|
138
|
+
|
|
139
|
+
# Metadata
|
|
140
|
+
print(report.created_by.organisation)
|
|
141
|
+
print(report.project.reference)
|
|
142
|
+
print(report.project.client)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Purification yields
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
# Yields for a sample
|
|
149
|
+
yields = report.yields_for_sample("BOL-24-01")
|
|
150
|
+
for y in yields:
|
|
151
|
+
print(f"{y.element}: {y.value_pct}%")
|
|
152
|
+
|
|
153
|
+
# Contamination alerts (yield > 105%)
|
|
154
|
+
suspects = report.suspicious_yields()
|
|
155
|
+
for y in suspects:
|
|
156
|
+
print(f"Possible contamination — {y.sample_id} / {y.element}: {y.value_pct}%")
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Methods and pipelines
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
# Preparation methods
|
|
163
|
+
for key, method in report.methods.items():
|
|
164
|
+
print(f"{key} — {method.name} ({method.type})")
|
|
165
|
+
if method.yield_min_pct:
|
|
166
|
+
print(f" Expected yield: {method.yield_min_pct}–{method.yield_max_pct}%")
|
|
167
|
+
|
|
168
|
+
# Pipelines
|
|
169
|
+
for key, pipeline in report.pipelines.items():
|
|
170
|
+
print(f"{pipeline.name} ({pipeline.element})")
|
|
171
|
+
for stage in pipeline.stages:
|
|
172
|
+
print(f" {stage.order}. {stage.label}")
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Export to pandas
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
df = report.to_pandas()
|
|
179
|
+
|
|
180
|
+
# One row per isotopic measurement, sample metadata included
|
|
181
|
+
df[["sample_name", "element", "ratio", "ratio_2se", "instrument"]]
|
|
182
|
+
|
|
183
|
+
# Standard pandas filtering
|
|
184
|
+
pb_data = df[df["element"] == "Pb"]
|
|
185
|
+
sources = df[df["classification"] == "source"]
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### CSV export
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
report.to_csv("export.csv")
|
|
192
|
+
# equivalent to report.to_pandas().to_csv("export.csv", index=False)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Error handling
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
from isof.exceptions import ISOfParseError, ISOfVersionError, ISOfSignatureError
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
report = isof.load("file.isof")
|
|
204
|
+
except ISOfVersionError as e:
|
|
205
|
+
print(f"Version {e.found} not supported, please update python-isof")
|
|
206
|
+
except ISOfParseError as e:
|
|
207
|
+
print(f"Invalid file: {e}")
|
|
208
|
+
|
|
209
|
+
# Corrupted vs. absent signature — two distinct cases
|
|
210
|
+
result = report.verify()
|
|
211
|
+
if result.level == 0:
|
|
212
|
+
print("No signature in this file")
|
|
213
|
+
elif not result.valid:
|
|
214
|
+
print(f"Signature present but invalid: {result.reason}")
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## ISOF format
|
|
220
|
+
|
|
221
|
+
Structure of an `.isof` document (JSON):
|
|
222
|
+
|
|
223
|
+
```
|
|
224
|
+
{
|
|
225
|
+
"isof_version": "1.0",
|
|
226
|
+
"created_at": "2025-03-10T14:32:00Z",
|
|
227
|
+
"created_by": { "software", "operator", "organisation" },
|
|
228
|
+
"project": { "name", "reference", "client", "classification" },
|
|
229
|
+
"samples": [ ... ], ← isotopic data per sample
|
|
230
|
+
"methods": { ... }, ← preparation protocols
|
|
231
|
+
"pipelines": { ... }, ← method sequences per element
|
|
232
|
+
"purification": { ... }, ← measured yields per (sample, element)
|
|
233
|
+
"assignments": [ ... ], ← method ↔ sample links
|
|
234
|
+
"signature": { ... } ← optional
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Full specification: [isofind.tech/isof-spec](https://isofind.tech/isof-spec)
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Development
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
git clone https://github.com/ColinFerrari/isof
|
|
246
|
+
cd isof
|
|
247
|
+
pip install -e ".[dev]"
|
|
248
|
+
pytest tests/ -v
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## License
|
|
254
|
+
|
|
255
|
+
MIT — see [LICENSE](LICENSE).
|
|
256
|
+
|
|
257
|
+
This package is maintained by [Colin Ferrari](https://isofind.tech).
|
|
258
|
+
The ISOF format is an open standard — third-party contributions and implementations are welcome.
|
isof-0.1.0/README.md
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# isof
|
|
2
|
+
|
|
3
|
+
Python reader and verifier for the **ISOF v1.0** format, an open standard for exchanging geochemical isotopic data.
|
|
4
|
+
|
|
5
|
+
This format allows exchanging in a single file the isotopic data along with all associated metadata (analytical methods used for each sample, purification yields, analysis pipeline...), while enabling traceability of modifications once files are produced and certifying the origin of the file (laboratory certification).
|
|
6
|
+
|
|
7
|
+
**Sovereignty and Confidentiality:** Signature verification is a 100% local process. No data is sent to a third-party server for validation.
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
import isof
|
|
11
|
+
|
|
12
|
+
report = isof.load("analyse_bolivie.isof")
|
|
13
|
+
|
|
14
|
+
if report.is_authentic():
|
|
15
|
+
print(f"Signed by: {report.signature.signed_by}")
|
|
16
|
+
|
|
17
|
+
df = report.to_pandas()
|
|
18
|
+
print(df[["sample_name", "element", "ratio", "ratio_2se"]])
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The ISOF format is used by [IsoFind](https://isofind.tech), but this parser is independent and can read any file compliant with the [ISOF v1.0 specification](https://isofind.tech/isof-spec).
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install isof
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
With pandas support:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install isof[pandas]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Requires Python ≥ 3.9.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
### Load a file
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import isof
|
|
47
|
+
|
|
48
|
+
report = isof.load("analyse.isof")
|
|
49
|
+
print(report)
|
|
50
|
+
# <ISOfDocument v1.0 — 12 échantillon(s) — IGE Grenoble>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
From a JSON string (API, database):
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
with open("analyse.isof") as f:
|
|
57
|
+
text = f.read()
|
|
58
|
+
|
|
59
|
+
report = isof.loads(text)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Verify integrity
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
# Simple check
|
|
66
|
+
if report.is_authentic():
|
|
67
|
+
print("Data integrity confirmed")
|
|
68
|
+
|
|
69
|
+
# Detailed result
|
|
70
|
+
result = report.verify()
|
|
71
|
+
print(result.valid) # bool
|
|
72
|
+
print(result.level) # 1 (SHA-256) or 2 (IsoFind PKI)
|
|
73
|
+
print(result.signer) # organisation or certificate CN
|
|
74
|
+
print(result.signed_at) # ISO 8601 timestamp
|
|
75
|
+
print(result.reason) # None if valid, error message otherwise
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Two signature levels coexist in the format:
|
|
79
|
+
|
|
80
|
+
| Level | Mechanism | Guarantee |
|
|
81
|
+
| ----- | -------------------------- | -------------------------------------------------------------- |
|
|
82
|
+
| 1 | SHA-256 over the data | Integrity — file has not been modified since export |
|
|
83
|
+
| 2 | ECDSA P-256 + IsoFind PKI | Authenticity — signed by a laboratory certified by IsoFind |
|
|
84
|
+
|
|
85
|
+
Verification works **offline**: IsoFind certificates are embedded in the package.
|
|
86
|
+
|
|
87
|
+
### Access data
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
# Sample list
|
|
91
|
+
for sample in report.samples:
|
|
92
|
+
print(sample.id, sample.name, sample.classification)
|
|
93
|
+
for iso in sample.isotope_data:
|
|
94
|
+
print(f" {iso.element} {iso.system} = {iso.ratio} ± {iso.ratio_2se}")
|
|
95
|
+
|
|
96
|
+
# Look up a sample by identifier
|
|
97
|
+
s = report.sample("BOL-24-01")
|
|
98
|
+
|
|
99
|
+
# Filter
|
|
100
|
+
sources = report.filter_samples(classification="source")
|
|
101
|
+
sb_samples = report.filter_samples(element="Sb")
|
|
102
|
+
combined = report.filter_samples(element="Pb", material_type="minerai")
|
|
103
|
+
|
|
104
|
+
# Metadata
|
|
105
|
+
print(report.created_by.organisation)
|
|
106
|
+
print(report.project.reference)
|
|
107
|
+
print(report.project.client)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Purification yields
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
# Yields for a sample
|
|
114
|
+
yields = report.yields_for_sample("BOL-24-01")
|
|
115
|
+
for y in yields:
|
|
116
|
+
print(f"{y.element}: {y.value_pct}%")
|
|
117
|
+
|
|
118
|
+
# Contamination alerts (yield > 105%)
|
|
119
|
+
suspects = report.suspicious_yields()
|
|
120
|
+
for y in suspects:
|
|
121
|
+
print(f"Possible contamination — {y.sample_id} / {y.element}: {y.value_pct}%")
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Methods and pipelines
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
# Preparation methods
|
|
128
|
+
for key, method in report.methods.items():
|
|
129
|
+
print(f"{key} — {method.name} ({method.type})")
|
|
130
|
+
if method.yield_min_pct:
|
|
131
|
+
print(f" Expected yield: {method.yield_min_pct}–{method.yield_max_pct}%")
|
|
132
|
+
|
|
133
|
+
# Pipelines
|
|
134
|
+
for key, pipeline in report.pipelines.items():
|
|
135
|
+
print(f"{pipeline.name} ({pipeline.element})")
|
|
136
|
+
for stage in pipeline.stages:
|
|
137
|
+
print(f" {stage.order}. {stage.label}")
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Export to pandas
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
df = report.to_pandas()
|
|
144
|
+
|
|
145
|
+
# One row per isotopic measurement, sample metadata included
|
|
146
|
+
df[["sample_name", "element", "ratio", "ratio_2se", "instrument"]]
|
|
147
|
+
|
|
148
|
+
# Standard pandas filtering
|
|
149
|
+
pb_data = df[df["element"] == "Pb"]
|
|
150
|
+
sources = df[df["classification"] == "source"]
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### CSV export
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
report.to_csv("export.csv")
|
|
157
|
+
# equivalent to report.to_pandas().to_csv("export.csv", index=False)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Error handling
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
from isof.exceptions import ISOfParseError, ISOfVersionError, ISOfSignatureError
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
report = isof.load("file.isof")
|
|
169
|
+
except ISOfVersionError as e:
|
|
170
|
+
print(f"Version {e.found} not supported, please update python-isof")
|
|
171
|
+
except ISOfParseError as e:
|
|
172
|
+
print(f"Invalid file: {e}")
|
|
173
|
+
|
|
174
|
+
# Corrupted vs. absent signature — two distinct cases
|
|
175
|
+
result = report.verify()
|
|
176
|
+
if result.level == 0:
|
|
177
|
+
print("No signature in this file")
|
|
178
|
+
elif not result.valid:
|
|
179
|
+
print(f"Signature present but invalid: {result.reason}")
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## ISOF format
|
|
185
|
+
|
|
186
|
+
Structure of an `.isof` document (JSON):
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
{
|
|
190
|
+
"isof_version": "1.0",
|
|
191
|
+
"created_at": "2025-03-10T14:32:00Z",
|
|
192
|
+
"created_by": { "software", "operator", "organisation" },
|
|
193
|
+
"project": { "name", "reference", "client", "classification" },
|
|
194
|
+
"samples": [ ... ], ← isotopic data per sample
|
|
195
|
+
"methods": { ... }, ← preparation protocols
|
|
196
|
+
"pipelines": { ... }, ← method sequences per element
|
|
197
|
+
"purification": { ... }, ← measured yields per (sample, element)
|
|
198
|
+
"assignments": [ ... ], ← method ↔ sample links
|
|
199
|
+
"signature": { ... } ← optional
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Full specification: [isofind.tech/isof-spec](https://isofind.tech/isof-spec)
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Development
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
git clone https://github.com/ColinFerrari/isof
|
|
211
|
+
cd isof
|
|
212
|
+
pip install -e ".[dev]"
|
|
213
|
+
pytest tests/ -v
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## License
|
|
219
|
+
|
|
220
|
+
MIT — see [LICENSE](LICENSE).
|
|
221
|
+
|
|
222
|
+
This package is maintained by [Colin Ferrari](https://isofind.tech).
|
|
223
|
+
The ISOF format is an open standard — third-party contributions and implementations are welcome.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""
|
|
2
|
+
isof — Lecteur et vérificateur du format ISOF v1.0 | ISOF v1.0 reader and verificator
|
|
3
|
+
|
|
4
|
+
Usage minimal : | minimal use :
|
|
5
|
+
|
|
6
|
+
import isof
|
|
7
|
+
|
|
8
|
+
report = isof.load("analyse_bolivie.isof")
|
|
9
|
+
if report.is_authentic():
|
|
10
|
+
print(f"Signé par : {report.signature.signed_by}")
|
|
11
|
+
df = report.to_pandas()
|
|
12
|
+
|
|
13
|
+
Le format ISOF est un standard ouvert pour l'échange de données isotopiques | ISOF format is an open standard for isotope data exchange
|
|
14
|
+
Spécification : https://isofind.tech/isof-spec
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from .exceptions import ISOfError, ISOfParseError, ISOfSignatureError, ISOfVersionError
|
|
18
|
+
from .models import (
|
|
19
|
+
Assignment,
|
|
20
|
+
CreatedBy,
|
|
21
|
+
IsotopeRecord,
|
|
22
|
+
Method,
|
|
23
|
+
Pipeline,
|
|
24
|
+
Project,
|
|
25
|
+
PurificationYield,
|
|
26
|
+
Sample,
|
|
27
|
+
Signature,
|
|
28
|
+
)
|
|
29
|
+
from .parser import ISOfDocument, load_file, load_string
|
|
30
|
+
from .signature import VerificationResult
|
|
31
|
+
|
|
32
|
+
__version__ = "0.1.0"
|
|
33
|
+
__author__ = "Colin Ferrari"
|
|
34
|
+
__all__ = [
|
|
35
|
+
# Fonctions d'entrée principales
|
|
36
|
+
"load",
|
|
37
|
+
"loads",
|
|
38
|
+
# Document
|
|
39
|
+
"ISOfDocument",
|
|
40
|
+
# Modèles
|
|
41
|
+
"Sample",
|
|
42
|
+
"IsotopeRecord",
|
|
43
|
+
"Method",
|
|
44
|
+
"Pipeline",
|
|
45
|
+
"PurificationYield",
|
|
46
|
+
"Assignment",
|
|
47
|
+
"CreatedBy",
|
|
48
|
+
"Project",
|
|
49
|
+
"Signature",
|
|
50
|
+
"VerificationResult",
|
|
51
|
+
# Exceptions
|
|
52
|
+
"ISOfError",
|
|
53
|
+
"ISOfParseError",
|
|
54
|
+
"ISOfVersionError",
|
|
55
|
+
"ISOfSignatureError",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def load(path) -> ISOfDocument:
|
|
60
|
+
"""Charge un fichier .isof depuis le disque. | Load an .isof file from disk
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
path: Chemin vers le fichier, str ou pathlib.Path.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
ISOfDocument prêt à l'emploi. | Ready to use
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
ISOfParseError: Fichier introuvable, JSON invalide, ou structure ISOF non reconnue. | Unfound file, invalid JSON or ISOF structure unrecognised
|
|
70
|
+
ISOfVersionError: Version du format non supportée par ce parser. | Format unsupported by this parser
|
|
71
|
+
|
|
72
|
+
Example: | Exemple :
|
|
73
|
+
>>> report = isof.load("analyse_bolivie.isof")
|
|
74
|
+
>>> print(report)
|
|
75
|
+
<ISOfDocument v1.0 — 12 échantillon(s) — IGE Grenoble>
|
|
76
|
+
"""
|
|
77
|
+
_, doc = load_file(path)
|
|
78
|
+
return doc
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def loads(text: str) -> ISOfDocument:
|
|
82
|
+
"""Charge un document ISOF depuis une chaîne JSON. | Load a ISOF document from a JSON chain.
|
|
83
|
+
|
|
84
|
+
Utile pour tester, ou pour lire depuis une API qui retourne du ISOF. | Useful to test or read from an API returning ISOF.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
text: Contenu JSON d'un document ISOF. | JSON content of an ISOF document.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
ISOfDocument prêt à l'emploi. | ISOFDocument ready to use.
|
|
91
|
+
"""
|
|
92
|
+
_, doc = load_string(text)
|
|
93
|
+
return doc
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hiérarchie d'exceptions du parser ISOF.
|
|
3
|
+
|
|
4
|
+
On distingue les erreurs de parsing (fichier malformé ou incompatible)
|
|
5
|
+
des erreurs de signature (fichier valide structurellement mais dont
|
|
6
|
+
l'intégrité ne peut pas être confirmée). Cette séparation permet aux
|
|
7
|
+
appelants de traiter les deux cas différemment sans inspecter le message.
|
|
8
|
+
|
|
9
|
+
ISOF parser exception hierarchy.
|
|
10
|
+
|
|
11
|
+
A distinction is made between parsing errors (malformed or incompatible file)
|
|
12
|
+
and signature errors (structurally valid file but whose
|
|
13
|
+
integrity cannot be confirmed). This separation allows
|
|
14
|
+
callers to handle the two cases differently without inspecting the message.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ISOfError(Exception):
|
|
19
|
+
"""
|
|
20
|
+
Classe de base, attraper celle-ci pour gérer toutes les erreurs ISOF.
|
|
21
|
+
Base class, catch this one to handle all ISOF errors.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ISOfParseError(ISOfError):
|
|
26
|
+
"""
|
|
27
|
+
Le fichier n'est pas un document ISOF valide ou lisible.
|
|
28
|
+
Causes typiques : JSON malformé, champ obligatoire absent,
|
|
29
|
+
version du format incompatible avec ce parser.
|
|
30
|
+
|
|
31
|
+
The file is not a valid or readable ISOF document.
|
|
32
|
+
Typical causes: Malformed JSON, missing required field,
|
|
33
|
+
format version incompatible with this parser.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ISOfVersionError(ISOfParseError):
|
|
38
|
+
"""
|
|
39
|
+
La version du format déclarée dans le fichier n'est pas supportée.
|
|
40
|
+
Elle est différente d'ISOfParseError pour que les outils puissent
|
|
41
|
+
suggérer une mise à jour du parser plutôt qu'un message d'erreur classique.
|
|
42
|
+
|
|
43
|
+
The format version declared in the file is not supported.
|
|
44
|
+
It differs from ISOfParseError so that tools can
|
|
45
|
+
suggest a parser update rather than a standard error message.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, found: str, supported: tuple[str, ...]) -> None:
|
|
49
|
+
self.found = found
|
|
50
|
+
self.supported = supported
|
|
51
|
+
super().__init__(
|
|
52
|
+
f"Version ISOF '{found}' non supportée. "
|
|
53
|
+
f"Versions supportées : {', '.join(supported)}"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ISOfSignatureError(ISOfError):
|
|
58
|
+
"""
|
|
59
|
+
La signature est présente mais ne peut pas être vérifiée.
|
|
60
|
+
|
|
61
|
+
Distinct d'une signature invalide (is_authentic() → False) :
|
|
62
|
+
ici c'est le processus de vérification lui-même qui a échoué,
|
|
63
|
+
par exemple parce que l'algorithme est inconnu ou que le certificat
|
|
64
|
+
est illisible.
|
|
65
|
+
|
|
66
|
+
The signature is present but cannot be verified.
|
|
67
|
+
|
|
68
|
+
This differs from an invalid signature (is_authentic() → False):
|
|
69
|
+
here, the verification process itself failed, for example,
|
|
70
|
+
because the algorithm is unknown or the certificate is unreadable.
|
|
71
|
+
"""
|