pysof 0.1.2__cp311-cp311-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pysof/__init__.py +330 -0
- pysof/_pysof.cp311-win_amd64.pyd +0 -0
- pysof/_pysof.pyi +46 -0
- pysof/py.typed +0 -0
- pysof-0.1.2.dist-info/METADATA +552 -0
- pysof-0.1.2.dist-info/RECORD +7 -0
- pysof-0.1.2.dist-info/WHEEL +4 -0
pysof/__init__.py
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""pysof: Python wrapper for the Helios SOF (SQL on FHIR) toolkit.
|
|
2
|
+
|
|
3
|
+
This package provides Python bindings for the Rust `sof` crate, enabling
|
|
4
|
+
transformation of FHIR resources into tabular data using ViewDefinitions.
|
|
5
|
+
|
|
6
|
+
Public API:
|
|
7
|
+
run_view_definition: Transform FHIR data using ViewDefinition
|
|
8
|
+
run_view_definition_with_options: Transform with filtering/pagination
|
|
9
|
+
validate_view_definition: Pre-validate ViewDefinition structure
|
|
10
|
+
validate_bundle: Pre-validate Bundle structure
|
|
11
|
+
get_supported_fhir_versions: List available FHIR versions
|
|
12
|
+
parse_content_type: Parse MIME types to format strings
|
|
13
|
+
|
|
14
|
+
Exception hierarchy:
|
|
15
|
+
SofError: Base exception for all pysof errors
|
|
16
|
+
InvalidViewDefinitionError: ViewDefinition validation errors
|
|
17
|
+
FhirPathError: FHIRPath expression evaluation errors
|
|
18
|
+
SerializationError: JSON/data serialization errors
|
|
19
|
+
UnsupportedContentTypeError: Unsupported output format errors
|
|
20
|
+
CsvError: CSV generation errors
|
|
21
|
+
IoError: File/IO related errors
|
|
22
|
+
InvalidSourceError: Invalid source parameter value
|
|
23
|
+
SourceNotFoundError: Source not found
|
|
24
|
+
SourceFetchError: Failed to fetch from source
|
|
25
|
+
SourceReadError: Failed to read from source
|
|
26
|
+
InvalidSourceContentError: Invalid content in source
|
|
27
|
+
UnsupportedSourceProtocolError: Unsupported source protocol
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from typing import Any, cast
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
# Import the Rust extension module
|
|
34
|
+
from pysof._pysof import (
|
|
35
|
+
CsvError,
|
|
36
|
+
FhirPathError,
|
|
37
|
+
InvalidSourceContentError,
|
|
38
|
+
InvalidSourceError,
|
|
39
|
+
InvalidViewDefinitionError,
|
|
40
|
+
IoError,
|
|
41
|
+
SerializationError,
|
|
42
|
+
# Exception classes
|
|
43
|
+
SofError,
|
|
44
|
+
SourceFetchError,
|
|
45
|
+
SourceNotFoundError,
|
|
46
|
+
SourceReadError,
|
|
47
|
+
UnsupportedContentTypeError,
|
|
48
|
+
UnsupportedSourceProtocolError,
|
|
49
|
+
py_get_supported_fhir_versions,
|
|
50
|
+
py_parse_content_type,
|
|
51
|
+
py_run_view_definition,
|
|
52
|
+
py_run_view_definition_with_options,
|
|
53
|
+
py_validate_bundle,
|
|
54
|
+
py_validate_view_definition,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Create Python-friendly wrapper functions
|
|
58
|
+
def run_view_definition(
|
|
59
|
+
view: dict[str, Any],
|
|
60
|
+
bundle: dict[str, Any],
|
|
61
|
+
format: str,
|
|
62
|
+
*,
|
|
63
|
+
fhir_version: str = "R4",
|
|
64
|
+
) -> bytes:
|
|
65
|
+
"""Transform FHIR Bundle data using a ViewDefinition.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
view: ViewDefinition resource as a Python dictionary
|
|
69
|
+
bundle: FHIR Bundle resource as a Python dictionary
|
|
70
|
+
format: Output format ("csv", "csv_with_header", "json", "ndjson", "parquet")
|
|
71
|
+
fhir_version: FHIR version to use ("R4", "R4B", "R5", "R6"). Defaults to "R4"
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Transformed data in the requested format as bytes
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
InvalidViewDefinitionError: ViewDefinition structure is invalid
|
|
78
|
+
FhirPathError: FHIRPath expression evaluation failed
|
|
79
|
+
SerializationError: JSON parsing/serialization failed
|
|
80
|
+
UnsupportedContentTypeError: Unsupported output format
|
|
81
|
+
CsvError: CSV generation failed
|
|
82
|
+
IoError: I/O operation failed
|
|
83
|
+
"""
|
|
84
|
+
return py_run_view_definition(view, bundle, format, fhir_version)
|
|
85
|
+
|
|
86
|
+
def run_view_definition_with_options(
|
|
87
|
+
view: dict[str, Any],
|
|
88
|
+
bundle: dict[str, Any],
|
|
89
|
+
format: str,
|
|
90
|
+
*,
|
|
91
|
+
since: str | None = None,
|
|
92
|
+
limit: int | None = None,
|
|
93
|
+
page: int | None = None,
|
|
94
|
+
fhir_version: str = "R4",
|
|
95
|
+
) -> bytes:
|
|
96
|
+
"""Transform FHIR Bundle data using a ViewDefinition with additional options.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
view: ViewDefinition resource as a Python dictionary
|
|
100
|
+
bundle: FHIR Bundle resource as a Python dictionary
|
|
101
|
+
format: Output format ("csv", "csv_with_header", "json", "ndjson", "parquet")
|
|
102
|
+
since: Filter resources modified after this ISO8601 datetime
|
|
103
|
+
limit: Limit the number of results returned
|
|
104
|
+
page: Page number for pagination (1-based)
|
|
105
|
+
fhir_version: FHIR version to use ("R4", "R4B", "R5", "R6"). Defaults to "R4"
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Transformed data in the requested format as bytes
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
InvalidViewDefinitionError: ViewDefinition structure is invalid
|
|
112
|
+
FhirPathError: FHIRPath expression evaluation failed
|
|
113
|
+
SerializationError: JSON parsing/serialization failed
|
|
114
|
+
UnsupportedContentTypeError: Unsupported output format
|
|
115
|
+
CsvError: CSV generation failed
|
|
116
|
+
IoError: I/O operation failed
|
|
117
|
+
"""
|
|
118
|
+
return py_run_view_definition_with_options(
|
|
119
|
+
view,
|
|
120
|
+
bundle,
|
|
121
|
+
format,
|
|
122
|
+
since=since,
|
|
123
|
+
limit=limit,
|
|
124
|
+
page=page,
|
|
125
|
+
fhir_version=fhir_version,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def validate_view_definition(
|
|
129
|
+
view: dict[str, Any], *, fhir_version: str = "R4"
|
|
130
|
+
) -> bool:
|
|
131
|
+
"""Validate a ViewDefinition structure without executing it.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
view: ViewDefinition resource as a Python dictionary
|
|
135
|
+
fhir_version: FHIR version to use ("R4", "R4B", "R5", "R6"). Defaults to "R4"
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
True if valid
|
|
139
|
+
|
|
140
|
+
Raises:
|
|
141
|
+
InvalidViewDefinitionError: ViewDefinition structure is invalid
|
|
142
|
+
SerializationError: JSON parsing failed
|
|
143
|
+
"""
|
|
144
|
+
return py_validate_view_definition(view, fhir_version)
|
|
145
|
+
|
|
146
|
+
def validate_bundle(bundle: dict[str, Any], *, fhir_version: str = "R4") -> bool:
|
|
147
|
+
"""Validate a Bundle structure without executing transformations.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
bundle: FHIR Bundle resource as a Python dictionary
|
|
151
|
+
fhir_version: FHIR version to use ("R4", "R4B", "R5", "R6"). Defaults to "R4"
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
True if valid
|
|
155
|
+
|
|
156
|
+
Raises:
|
|
157
|
+
SerializationError: JSON parsing failed
|
|
158
|
+
"""
|
|
159
|
+
return py_validate_bundle(bundle, fhir_version)
|
|
160
|
+
|
|
161
|
+
def parse_content_type(mime_type: str) -> str:
|
|
162
|
+
"""Parse MIME type string to format identifier.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
mime_type: MIME type string (e.g., "text/csv", "application/json")
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Format identifier suitable for use with run_view_definition
|
|
169
|
+
|
|
170
|
+
Raises:
|
|
171
|
+
UnsupportedContentTypeError: Unknown or unsupported MIME type
|
|
172
|
+
"""
|
|
173
|
+
return py_parse_content_type(mime_type)
|
|
174
|
+
|
|
175
|
+
def get_supported_fhir_versions() -> list[str]:
|
|
176
|
+
"""Get list of supported FHIR versions compiled into this build.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
List of supported FHIR version strings
|
|
180
|
+
"""
|
|
181
|
+
return py_get_supported_fhir_versions()
|
|
182
|
+
|
|
183
|
+
except ImportError as e:
|
|
184
|
+
# Fallback for when the Rust extension is not available
|
|
185
|
+
import warnings
|
|
186
|
+
|
|
187
|
+
warnings.warn(
|
|
188
|
+
f"Rust extension module not available: {e}. Using placeholder functions.",
|
|
189
|
+
ImportWarning,
|
|
190
|
+
stacklevel=2,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# Define placeholder exception classes
|
|
194
|
+
class SofError(Exception): # type: ignore[no-redef]
|
|
195
|
+
"""Base exception for all pysof errors"""
|
|
196
|
+
|
|
197
|
+
pass
|
|
198
|
+
|
|
199
|
+
class InvalidViewDefinitionError(SofError): # type: ignore[no-redef]
|
|
200
|
+
"""ViewDefinition validation errors"""
|
|
201
|
+
|
|
202
|
+
pass
|
|
203
|
+
|
|
204
|
+
class FhirPathError(SofError): # type: ignore[no-redef]
|
|
205
|
+
"""FHIRPath expression evaluation errors"""
|
|
206
|
+
|
|
207
|
+
pass
|
|
208
|
+
|
|
209
|
+
class SerializationError(SofError): # type: ignore[no-redef]
|
|
210
|
+
"""JSON/data serialization errors"""
|
|
211
|
+
|
|
212
|
+
pass
|
|
213
|
+
|
|
214
|
+
class UnsupportedContentTypeError(SofError): # type: ignore[no-redef]
|
|
215
|
+
"""Unsupported output format errors"""
|
|
216
|
+
|
|
217
|
+
pass
|
|
218
|
+
|
|
219
|
+
class CsvError(SofError): # type: ignore[no-redef]
|
|
220
|
+
"""CSV generation errors"""
|
|
221
|
+
|
|
222
|
+
pass
|
|
223
|
+
|
|
224
|
+
class IoError(SofError): # type: ignore[no-redef]
|
|
225
|
+
"""File/IO related errors"""
|
|
226
|
+
|
|
227
|
+
pass
|
|
228
|
+
|
|
229
|
+
class InvalidSourceError(SofError): # type: ignore[no-redef]
|
|
230
|
+
"""Invalid source parameter value"""
|
|
231
|
+
|
|
232
|
+
pass
|
|
233
|
+
|
|
234
|
+
class SourceNotFoundError(SofError): # type: ignore[no-redef]
|
|
235
|
+
"""Source not found"""
|
|
236
|
+
|
|
237
|
+
pass
|
|
238
|
+
|
|
239
|
+
class SourceFetchError(SofError): # type: ignore[no-redef]
|
|
240
|
+
"""Failed to fetch from source"""
|
|
241
|
+
|
|
242
|
+
pass
|
|
243
|
+
|
|
244
|
+
class SourceReadError(SofError): # type: ignore[no-redef]
|
|
245
|
+
"""Failed to read from source"""
|
|
246
|
+
|
|
247
|
+
pass
|
|
248
|
+
|
|
249
|
+
class InvalidSourceContentError(SofError): # type: ignore[no-redef]
|
|
250
|
+
"""Invalid content in source"""
|
|
251
|
+
|
|
252
|
+
pass
|
|
253
|
+
|
|
254
|
+
class UnsupportedSourceProtocolError(SofError): # type: ignore[no-redef]
|
|
255
|
+
"""Unsupported source protocol"""
|
|
256
|
+
|
|
257
|
+
pass
|
|
258
|
+
|
|
259
|
+
# Define placeholder functions
|
|
260
|
+
def run_view_definition(
|
|
261
|
+
view: dict[str, Any],
|
|
262
|
+
bundle: dict[str, Any],
|
|
263
|
+
format: str,
|
|
264
|
+
*,
|
|
265
|
+
fhir_version: str = "R4",
|
|
266
|
+
) -> bytes:
|
|
267
|
+
raise NotImplementedError("Rust extension module not available")
|
|
268
|
+
|
|
269
|
+
def run_view_definition_with_options(
|
|
270
|
+
view: dict[str, Any],
|
|
271
|
+
bundle: dict[str, Any],
|
|
272
|
+
format: str,
|
|
273
|
+
*,
|
|
274
|
+
since: str | None = None,
|
|
275
|
+
limit: int | None = None,
|
|
276
|
+
page: int | None = None,
|
|
277
|
+
fhir_version: str = "R4",
|
|
278
|
+
) -> bytes:
|
|
279
|
+
raise NotImplementedError("Rust extension module not available")
|
|
280
|
+
|
|
281
|
+
def validate_view_definition(
|
|
282
|
+
view: dict[str, Any], *, fhir_version: str = "R4"
|
|
283
|
+
) -> bool:
|
|
284
|
+
raise NotImplementedError("Rust extension module not available")
|
|
285
|
+
|
|
286
|
+
def validate_bundle(bundle: dict[str, Any], *, fhir_version: str = "R4") -> bool:
|
|
287
|
+
raise NotImplementedError("Rust extension module not available")
|
|
288
|
+
|
|
289
|
+
def parse_content_type(mime_type: str) -> str:
|
|
290
|
+
raise NotImplementedError("Rust extension module not available")
|
|
291
|
+
|
|
292
|
+
def get_supported_fhir_versions() -> list[str]:
|
|
293
|
+
raise NotImplementedError("Rust extension module not available")
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
__all__: list[str] = [
|
|
297
|
+
# Core functions
|
|
298
|
+
"run_view_definition",
|
|
299
|
+
"run_view_definition_with_options",
|
|
300
|
+
"validate_view_definition",
|
|
301
|
+
"validate_bundle",
|
|
302
|
+
"get_supported_fhir_versions",
|
|
303
|
+
"parse_content_type",
|
|
304
|
+
# Exception classes
|
|
305
|
+
"SofError",
|
|
306
|
+
"InvalidViewDefinitionError",
|
|
307
|
+
"FhirPathError",
|
|
308
|
+
"SerializationError",
|
|
309
|
+
"UnsupportedContentTypeError",
|
|
310
|
+
"CsvError",
|
|
311
|
+
"IoError",
|
|
312
|
+
"InvalidSourceError",
|
|
313
|
+
"SourceNotFoundError",
|
|
314
|
+
"SourceFetchError",
|
|
315
|
+
"SourceReadError",
|
|
316
|
+
"InvalidSourceContentError",
|
|
317
|
+
"UnsupportedSourceProtocolError",
|
|
318
|
+
]
|
|
319
|
+
|
|
320
|
+
__version__ = "0.1.0"
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def get_version() -> str:
|
|
324
|
+
"""Return the package version."""
|
|
325
|
+
return __version__
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def get_status() -> str:
|
|
329
|
+
"""Return the current implementation status."""
|
|
330
|
+
return "v1: Rust bindings available with full SOF transformation capabilities."
|
|
Binary file
|
pysof/_pysof.pyi
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Type stubs for the pysof Rust extension module."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
# Exception classes
|
|
6
|
+
class SofError(Exception): ...
|
|
7
|
+
class InvalidViewDefinitionError(SofError): ...
|
|
8
|
+
class FhirPathError(SofError): ...
|
|
9
|
+
class SerializationError(SofError): ...
|
|
10
|
+
class UnsupportedContentTypeError(SofError): ...
|
|
11
|
+
class CsvError(SofError): ...
|
|
12
|
+
class IoError(SofError): ...
|
|
13
|
+
class InvalidSourceError(SofError): ...
|
|
14
|
+
class SourceNotFoundError(SofError): ...
|
|
15
|
+
class SourceFetchError(SofError): ...
|
|
16
|
+
class SourceReadError(SofError): ...
|
|
17
|
+
class InvalidSourceContentError(SofError): ...
|
|
18
|
+
class UnsupportedSourceProtocolError(SofError): ...
|
|
19
|
+
|
|
20
|
+
# Core functions
|
|
21
|
+
def py_run_view_definition(
|
|
22
|
+
view: dict[str, Any],
|
|
23
|
+
bundle: dict[str, Any],
|
|
24
|
+
format: str,
|
|
25
|
+
fhir_version: str,
|
|
26
|
+
) -> bytes: ...
|
|
27
|
+
def py_run_view_definition_with_options(
|
|
28
|
+
view: dict[str, Any],
|
|
29
|
+
bundle: dict[str, Any],
|
|
30
|
+
format: str,
|
|
31
|
+
*,
|
|
32
|
+
since: str | None = None,
|
|
33
|
+
limit: int | None = None,
|
|
34
|
+
page: int | None = None,
|
|
35
|
+
fhir_version: str = "R4",
|
|
36
|
+
) -> bytes: ...
|
|
37
|
+
def py_validate_view_definition(
|
|
38
|
+
view: dict[str, Any],
|
|
39
|
+
fhir_version: str,
|
|
40
|
+
) -> bool: ...
|
|
41
|
+
def py_validate_bundle(
|
|
42
|
+
bundle: dict[str, Any],
|
|
43
|
+
fhir_version: str,
|
|
44
|
+
) -> bool: ...
|
|
45
|
+
def py_parse_content_type(mime_type: str) -> str: ...
|
|
46
|
+
def py_get_supported_fhir_versions() -> list[str]: ...
|
pysof/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pysof
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Classifier: Programming Language :: Python :: 3
|
|
5
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
6
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
7
|
+
Classifier: Operating System :: OS Independent
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Intended Audience :: Healthcare Industry
|
|
11
|
+
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
|
|
12
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
13
|
+
Summary: Python wrapper for the Helios SOF (SQL on FHIR) toolkit.
|
|
14
|
+
Keywords: SQL on FHIR,FHIR,healthcare,sof,helios
|
|
15
|
+
Author-email: Helios Team <team@heliossoftware.com>
|
|
16
|
+
License: MIT
|
|
17
|
+
Requires-Python: >=3.11, <3.12
|
|
18
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
19
|
+
|
|
20
|
+
# pysof
|
|
21
|
+
|
|
22
|
+
Python wrapper for the Helios SOF (SQL on FHIR) toolkit.
|
|
23
|
+
|
|
24
|
+
This package provides Python bindings for the Rust `helios-sof` library via PyO3 and maturin. Use `uv` to manage the environment and run builds.
|
|
25
|
+
|
|
26
|
+
> **Note**: This crate is excluded from the default workspace build via workspace default-members. When running `cargo build` from the repository root, `pysof` will not be built automatically. Build it explicitly when needed.
|
|
27
|
+
|
|
28
|
+
## Requirements
|
|
29
|
+
|
|
30
|
+
- Python 3.11
|
|
31
|
+
- uv (package and environment manager)
|
|
32
|
+
|
|
33
|
+
## Building the Crate
|
|
34
|
+
|
|
35
|
+
### Building with Cargo
|
|
36
|
+
|
|
37
|
+
This crate is excluded from the default workspace build to allow building the core Rust components without Python. To build it explicitly:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Your current directory MUST be the pysof crate:
|
|
41
|
+
cd crates/pysof
|
|
42
|
+
|
|
43
|
+
# From the pysof folder
|
|
44
|
+
cargo build
|
|
45
|
+
|
|
46
|
+
# Or build with specific FHIR version features
|
|
47
|
+
cargo build -p pysof --features R4,R5
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Building with Maturin (Recommended)
|
|
51
|
+
|
|
52
|
+
For Python development, it's recommended to use `maturin` via `uv`:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# From repo root
|
|
56
|
+
cd crates/pysof
|
|
57
|
+
|
|
58
|
+
# Ensure Python 3.11 is available and create a venv
|
|
59
|
+
uv venv --python 3.11
|
|
60
|
+
|
|
61
|
+
# Install the project dev dependencies
|
|
62
|
+
uv sync --group dev
|
|
63
|
+
|
|
64
|
+
# Build and install the Rust extension into the venv
|
|
65
|
+
uv run maturin develop --release
|
|
66
|
+
|
|
67
|
+
# Build distributable artifacts
|
|
68
|
+
uv run maturin build --release -o dist # wheels
|
|
69
|
+
uv run maturin sdist -o dist # source distribution
|
|
70
|
+
|
|
71
|
+
# Sanity checks
|
|
72
|
+
uv run python -c "import pysof; print(pysof.__version__); print(pysof.get_status()); print(pysof.get_supported_fhir_versions())"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Installation
|
|
76
|
+
|
|
77
|
+
### From PyPI (Recommended)
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
pip install pysof
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### From Source
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# Install from source (requires Rust toolchain)
|
|
87
|
+
pip install -e .
|
|
88
|
+
|
|
89
|
+
# Or build wheel locally
|
|
90
|
+
maturin build --release --out dist
|
|
91
|
+
pip install dist/*.whl
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### From GitHub Releases
|
|
95
|
+
|
|
96
|
+
Download the appropriate wheel for your platform from the [releases page](https://github.com/HeliosSoftware/hfs/releases) and install:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
pip install pysof-*.whl
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Supported Platforms
|
|
103
|
+
|
|
104
|
+
- **Linux**: x86_64 (glibc and musl)
|
|
105
|
+
- **Windows**: x86_64 (MSVC)
|
|
106
|
+
- **macOS**: AArch64 (x86_64 is not supported)
|
|
107
|
+
- **Python**: 3.11 only
|
|
108
|
+
|
|
109
|
+
For detailed information about wheel building and distribution, see [WHEEL_BUILDING.md](WHEEL_BUILDING.md).
|
|
110
|
+
|
|
111
|
+
## Testing
|
|
112
|
+
|
|
113
|
+
The project has separate test suites for Python and Rust components:
|
|
114
|
+
|
|
115
|
+
### Python Tests
|
|
116
|
+
|
|
117
|
+
Run the comprehensive Python test suite:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
# Run all Python tests
|
|
121
|
+
uv run pytest python-tests/
|
|
122
|
+
|
|
123
|
+
# Run specific test files
|
|
124
|
+
uv run pytest python-tests/test_core_functions.py -v
|
|
125
|
+
uv run pytest python-tests/test_content_types.py -v
|
|
126
|
+
uv run pytest python-tests/test_import.py -v
|
|
127
|
+
|
|
128
|
+
# Run with coverage
|
|
129
|
+
uv run pytest python-tests/ --cov=pysof --cov-report=html
|
|
130
|
+
|
|
131
|
+
# Run tests with detailed output
|
|
132
|
+
uv run pytest python-tests/ -v --tb=short
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Current Python test coverage:
|
|
136
|
+
- **58 total tests** across 5 test files
|
|
137
|
+
- Core API functions (19 tests)
|
|
138
|
+
- Content type support (14 tests)
|
|
139
|
+
- FHIR version support (16 tests)
|
|
140
|
+
- Package structure and imports (6 tests)
|
|
141
|
+
- Package metadata (3 tests)
|
|
142
|
+
|
|
143
|
+
### Rust Tests
|
|
144
|
+
|
|
145
|
+
Run the Rust unit and integration tests:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
# Run all Rust tests
|
|
149
|
+
cargo test
|
|
150
|
+
|
|
151
|
+
# Run unit tests only
|
|
152
|
+
cargo test --test lib_tests
|
|
153
|
+
|
|
154
|
+
# Run integration tests only
|
|
155
|
+
cargo test --test integration
|
|
156
|
+
|
|
157
|
+
# Run with verbose output
|
|
158
|
+
cargo test -- --nocapture
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Current Rust test coverage:
|
|
162
|
+
- **17 total tests** across 2 test files
|
|
163
|
+
- Unit tests: 14 tests covering core library functions
|
|
164
|
+
- Integration tests: 3 tests covering component interactions
|
|
165
|
+
|
|
166
|
+
## Usage
|
|
167
|
+
|
|
168
|
+
### Basic Example
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
import pysof
|
|
172
|
+
import json
|
|
173
|
+
|
|
174
|
+
# Sample ViewDefinition for extracting patient data
|
|
175
|
+
view_definition = {
|
|
176
|
+
"resourceType": "ViewDefinition",
|
|
177
|
+
"id": "patient-demographics",
|
|
178
|
+
"name": "PatientDemographics",
|
|
179
|
+
"status": "active",
|
|
180
|
+
"resource": "Patient",
|
|
181
|
+
"select": [
|
|
182
|
+
{
|
|
183
|
+
"column": [
|
|
184
|
+
{"name": "id", "path": "id"},
|
|
185
|
+
{"name": "family_name", "path": "name.family"},
|
|
186
|
+
{"name": "given_name", "path": "name.given.first()"},
|
|
187
|
+
{"name": "gender", "path": "gender"},
|
|
188
|
+
{"name": "birth_date", "path": "birthDate"}
|
|
189
|
+
]
|
|
190
|
+
}
|
|
191
|
+
]
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
# Sample FHIR Bundle with patient data
|
|
195
|
+
bundle = {
|
|
196
|
+
"resourceType": "Bundle",
|
|
197
|
+
"type": "collection",
|
|
198
|
+
"entry": [
|
|
199
|
+
{
|
|
200
|
+
"resource": {
|
|
201
|
+
"resourceType": "Patient",
|
|
202
|
+
"id": "patient-1",
|
|
203
|
+
"name": [{"family": "Doe", "given": ["John"]}],
|
|
204
|
+
"gender": "male",
|
|
205
|
+
"birthDate": "1990-01-01"
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
"resource": {
|
|
210
|
+
"resourceType": "Patient",
|
|
211
|
+
"id": "patient-2",
|
|
212
|
+
"name": [{"family": "Smith", "given": ["Jane"]}],
|
|
213
|
+
"gender": "female",
|
|
214
|
+
"birthDate": "1985-05-15"
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
]
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
# Transform to different formats
|
|
221
|
+
csv_result = pysof.run_view_definition(view_definition, bundle, "csv")
|
|
222
|
+
json_result = pysof.run_view_definition(view_definition, bundle, "json")
|
|
223
|
+
ndjson_result = pysof.run_view_definition(view_definition, bundle, "ndjson")
|
|
224
|
+
parquet_result = pysof.run_view_definition(view_definition, bundle, "parquet")
|
|
225
|
+
|
|
226
|
+
print("CSV Output:")
|
|
227
|
+
print(csv_result.decode('utf-8'))
|
|
228
|
+
|
|
229
|
+
print("\nJSON Output:")
|
|
230
|
+
data = json.loads(json_result.decode('utf-8'))
|
|
231
|
+
print(json.dumps(data, indent=2))
|
|
232
|
+
|
|
233
|
+
print("\nParquet Output (binary):")
|
|
234
|
+
print(f"Parquet data: {len(parquet_result)} bytes")
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Advanced Usage with Options
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
import pysof
|
|
241
|
+
from datetime import datetime
|
|
242
|
+
|
|
243
|
+
# Transform with pagination and filtering
|
|
244
|
+
result = pysof.run_view_definition_with_options(
|
|
245
|
+
view_definition,
|
|
246
|
+
bundle,
|
|
247
|
+
"json",
|
|
248
|
+
limit=10, # Limit to 10 results
|
|
249
|
+
page=1, # First page
|
|
250
|
+
since="2023-01-01T00:00:00Z", # Filter by modification date
|
|
251
|
+
fhir_version="R4" # Specify FHIR version
|
|
252
|
+
)
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Utility Functions
|
|
256
|
+
|
|
257
|
+
```python
|
|
258
|
+
import pysof
|
|
259
|
+
|
|
260
|
+
# Validate structures before processing
|
|
261
|
+
is_valid_view = pysof.validate_view_definition(view_definition)
|
|
262
|
+
is_valid_bundle = pysof.validate_bundle(bundle)
|
|
263
|
+
|
|
264
|
+
# Parse content types
|
|
265
|
+
format_str = pysof.parse_content_type("text/csv") # Returns "csv_with_header"
|
|
266
|
+
format_str = pysof.parse_content_type("application/json") # Returns "json"
|
|
267
|
+
|
|
268
|
+
# Check supported FHIR versions
|
|
269
|
+
versions = pysof.get_supported_fhir_versions() # Returns ["R4"] (or more based on build)
|
|
270
|
+
print(f"Supported FHIR versions: {versions}")
|
|
271
|
+
|
|
272
|
+
# Check package status
|
|
273
|
+
print(pysof.get_status()) # Shows current implementation status
|
|
274
|
+
print(f"Version: {pysof.get_version()}")
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Error Handling
|
|
278
|
+
|
|
279
|
+
```python
|
|
280
|
+
import pysof
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
result = pysof.run_view_definition(view_definition, bundle, "json")
|
|
284
|
+
except pysof.InvalidViewDefinitionError as e:
|
|
285
|
+
print(f"ViewDefinition validation error: {e}")
|
|
286
|
+
except pysof.SerializationError as e:
|
|
287
|
+
print(f"JSON parsing error: {e}")
|
|
288
|
+
except pysof.UnsupportedContentTypeError as e:
|
|
289
|
+
print(f"Unsupported format: {e}")
|
|
290
|
+
except pysof.SofError as e:
|
|
291
|
+
print(f"General SOF error: {e}")
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Key Features
|
|
295
|
+
|
|
296
|
+
- **Performance**: Efficient processing of large FHIR Bundles using Rust
|
|
297
|
+
- **Parallel Processing**: Automatic multithreading of FHIR resources using rayon (5-7x speedup on multi-core systems)
|
|
298
|
+
- **GIL Release**: Python GIL is released during Rust execution for better performance
|
|
299
|
+
- **Multiple Formats**: Support for CSV, JSON, NDJSON, and Parquet outputs
|
|
300
|
+
- **FHIR Versions**: Support for R4, R4B, R5, and R6 (depending on build features)
|
|
301
|
+
|
|
302
|
+
### Available Options
|
|
303
|
+
|
|
304
|
+
The `run_view_definition_with_options()` function accepts the following parameters:
|
|
305
|
+
|
|
306
|
+
- **limit**: Limit the number of results returned
|
|
307
|
+
- **page**: Page number for pagination (1-based)
|
|
308
|
+
- **since**: Filter resources modified after this ISO8601 datetime
|
|
309
|
+
- **fhir_version**: FHIR version to use ("R4", "R4B", "R5", "R6")
|
|
310
|
+
|
|
311
|
+
### Supported Content Types
|
|
312
|
+
|
|
313
|
+
| Format | Description | Output |
|
|
314
|
+
|--------|-------------|---------|
|
|
315
|
+
| `csv` | CSV with headers | Comma-separated values with header row |
|
|
316
|
+
| `json` | JSON array | Array of objects, one per result row |
|
|
317
|
+
| `ndjson` | Newline-delimited JSON | One JSON object per line |
|
|
318
|
+
| `parquet` | Parquet format | Columnar binary format optimized for analytics |
|
|
319
|
+
|
|
320
|
+
### Supported FHIR Versions
|
|
321
|
+
|
|
322
|
+
- **R4** (default, always available)
|
|
323
|
+
- **R4B** (if compiled with R4B feature)
|
|
324
|
+
- **R5** (if compiled with R5 feature)
|
|
325
|
+
- **R6** (if compiled with R6 feature)
|
|
326
|
+
|
|
327
|
+
Use `pysof.get_supported_fhir_versions()` to check what's available in your build.
|
|
328
|
+
|
|
329
|
+
### Usage Notes
|
|
330
|
+
|
|
331
|
+
- The basic `run_view_definition()` function is suitable for simple use cases
|
|
332
|
+
- Use `run_view_definition_with_options()` for pagination, filtering, and other advanced features
|
|
333
|
+
- All heavy processing happens in Rust code; Python GIL is properly released for optimal performance
|
|
334
|
+
|
|
335
|
+
## Multithreading and Performance
|
|
336
|
+
|
|
337
|
+
### Automatic Parallel Processing
|
|
338
|
+
|
|
339
|
+
pysof automatically processes FHIR resources in parallel using rayon, providing significant performance improvements on multi-core systems:
|
|
340
|
+
|
|
341
|
+
- **5-7x speedup** for typical workloads on modern CPUs
|
|
342
|
+
- **Zero configuration required** - parallelization is always enabled
|
|
343
|
+
- **Python GIL released** during processing for true parallel execution
|
|
344
|
+
|
|
345
|
+
### Controlling Thread Count
|
|
346
|
+
|
|
347
|
+
By default, pysof uses all available CPU cores. You can control the thread count using the `RAYON_NUM_THREADS` environment variable:
|
|
348
|
+
|
|
349
|
+
```python
|
|
350
|
+
import os
|
|
351
|
+
import pysof
|
|
352
|
+
|
|
353
|
+
# Set thread count BEFORE first use (must be set before rayon initializes)
|
|
354
|
+
os.environ['RAYON_NUM_THREADS'] = '4'
|
|
355
|
+
|
|
356
|
+
# Now all operations will use 4 threads
|
|
357
|
+
result = pysof.run_view_definition(view_definition, bundle, "json")
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Or from the command line:
|
|
361
|
+
|
|
362
|
+
```bash
|
|
363
|
+
# Linux/Mac
|
|
364
|
+
RAYON_NUM_THREADS=4 python my_script.py
|
|
365
|
+
|
|
366
|
+
# Windows (cmd)
|
|
367
|
+
set RAYON_NUM_THREADS=4
|
|
368
|
+
python my_script.py
|
|
369
|
+
|
|
370
|
+
# Windows (PowerShell)
|
|
371
|
+
$env:RAYON_NUM_THREADS=4
|
|
372
|
+
python my_script.py
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
**Important**: The environment variable must be set before rayon initializes its global thread pool (typically on first use of pysof). Once initialized, the thread count cannot be changed for that process.
|
|
376
|
+
|
|
377
|
+
### Performance Tips
|
|
378
|
+
|
|
379
|
+
- **Large datasets**: Use all available cores (default behavior)
|
|
380
|
+
- **Shared systems**: Limit threads to avoid resource contention (`RAYON_NUM_THREADS=4`)
|
|
381
|
+
- **Memory constrained**: Reduce thread count to lower memory usage
|
|
382
|
+
- **Benchmarking**: Test different thread counts to find optimal performance for your workload
|
|
383
|
+
|
|
384
|
+
## Configuring FHIR Version Support
|
|
385
|
+
|
|
386
|
+
By default, pysof is compiled with **R4 support only**. You can configure which FHIR versions are available by modifying the feature compilation settings.
|
|
387
|
+
|
|
388
|
+
### Change Default FHIR Version
|
|
389
|
+
|
|
390
|
+
To change from R4 to another version (e.g., R5):
|
|
391
|
+
|
|
392
|
+
1. **Edit `crates/pysof/Cargo.toml`**:
|
|
393
|
+
```toml
|
|
394
|
+
[features]
|
|
395
|
+
default = ["R5"] # Changed from ["R4"]
|
|
396
|
+
R4 = ["helios-sof/R4", "helios-fhir/R4"]
|
|
397
|
+
R4B = ["helios-sof/R4B", "helios-fhir/R4B"]
|
|
398
|
+
R5 = ["helios-sof/R5", "helios-fhir/R5"]
|
|
399
|
+
R6 = ["helios-sof/R6", "helios-fhir/R6"]
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
2. **Rebuild the extension**:
|
|
403
|
+
```bash
|
|
404
|
+
cd crates/pysof
|
|
405
|
+
uv run maturin develop --release
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
3. **Verify the change**:
|
|
409
|
+
```bash
|
|
410
|
+
uv run python -c "
|
|
411
|
+
import pysof
|
|
412
|
+
versions = pysof.get_supported_fhir_versions()
|
|
413
|
+
print('Supported FHIR versions:', versions)
|
|
414
|
+
"
|
|
415
|
+
```
|
|
416
|
+
This should now show `['R5']` instead of `['R4']`.
|
|
417
|
+
|
|
418
|
+
### Enable Multiple FHIR Versions
|
|
419
|
+
|
|
420
|
+
To support multiple FHIR versions simultaneously:
|
|
421
|
+
|
|
422
|
+
1. **Edit `crates/pysof/Cargo.toml`**:
|
|
423
|
+
```toml
|
|
424
|
+
[features]
|
|
425
|
+
default = ["R4", "R5"] # Enable both R4 and R5
|
|
426
|
+
# Or enable all versions:
|
|
427
|
+
# default = ["R4", "R4B", "R5", "R6"]
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
2. **Rebuild and verify**:
|
|
431
|
+
```bash
|
|
432
|
+
uv run maturin develop --release
|
|
433
|
+
uv run python -c "import pysof; print(pysof.get_supported_fhir_versions())"
|
|
434
|
+
```
|
|
435
|
+
This should show `['R4', 'R5']` (or all enabled versions).
|
|
436
|
+
|
|
437
|
+
3. **Use specific versions in code**:
|
|
438
|
+
```python
|
|
439
|
+
import pysof
|
|
440
|
+
|
|
441
|
+
# Use R4 explicitly
|
|
442
|
+
result_r4 = pysof.run_view_definition(view, bundle, "json", fhir_version="R4")
|
|
443
|
+
|
|
444
|
+
# Use R5 explicitly
|
|
445
|
+
result_r5 = pysof.run_view_definition(view, bundle, "json", fhir_version="R5")
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### Build with Specific Features (Without Changing Default)
|
|
449
|
+
|
|
450
|
+
To temporarily build with different features without modifying `Cargo.toml`:
|
|
451
|
+
|
|
452
|
+
```bash
|
|
453
|
+
# Build with only R5
|
|
454
|
+
cargo build --features R5 --no-default-features
|
|
455
|
+
|
|
456
|
+
# Build with R4 and R6
|
|
457
|
+
cargo build --features R4,R6 --no-default-features
|
|
458
|
+
|
|
459
|
+
# With maturin
|
|
460
|
+
uv run --with maturin -- maturin develop --release --cargo-extra-args="--features R5 --no-default-features"
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### Testing After Version Changes
|
|
464
|
+
|
|
465
|
+
After changing FHIR version support, run the test suite to ensure compatibility:
|
|
466
|
+
|
|
467
|
+
```bash
|
|
468
|
+
# Run all tests
|
|
469
|
+
uv run pytest
|
|
470
|
+
|
|
471
|
+
# Run FHIR version-specific tests
|
|
472
|
+
uv run pytest tests/test_fhir_versions.py -v
|
|
473
|
+
|
|
474
|
+
# Test with your new default version
|
|
475
|
+
uv run python -c "
|
|
476
|
+
import pysof
|
|
477
|
+
|
|
478
|
+
# Test with default version (should be your new default)
|
|
479
|
+
view = {'resourceType': 'ViewDefinition', 'id': 'test', 'name': 'Test', 'status': 'active', 'resource': 'Patient', 'select': [{'column': [{'name': 'id', 'path': 'id'}]}]}
|
|
480
|
+
bundle = {'resourceType': 'Bundle', 'type': 'collection', 'entry': [{'resource': {'resourceType': 'Patient', 'id': 'test'}}]}
|
|
481
|
+
|
|
482
|
+
result = pysof.run_view_definition(view, bundle, 'json')
|
|
483
|
+
print('Default version test successful:', len(result), 'bytes')
|
|
484
|
+
"
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
### Important Considerations
|
|
488
|
+
|
|
489
|
+
1. **Dependency Requirements**: Ensure the underlying `helios-sof` and `helios-fhir` crates support the features you want to enable
|
|
490
|
+
2. **Binary Size**: Enabling multiple FHIR versions increases the compiled binary size significantly
|
|
491
|
+
3. **Memory Usage**: Multiple versions require more memory at runtime
|
|
492
|
+
4. **Testing**: Always run your test suite after changing version support
|
|
493
|
+
5. **Documentation**: Update any project documentation that mentions specific FHIR versions
|
|
494
|
+
6. **CI/CD**: Update build scripts and CI workflows if they depend on specific versions
|
|
495
|
+
|
|
496
|
+
### Version Compatibility Matrix
|
|
497
|
+
|
|
498
|
+
| FHIR Version | Feature Flag | Status | Notes |
|
|
499
|
+
|--------------|--------------|---------|-------|
|
|
500
|
+
| R4 | `R4` | ✅ Stable | Default, always recommended |
|
|
501
|
+
| R4B | `R4B` | ✅ Stable | Minor updates to R4 |
|
|
502
|
+
| R5 | `R5` | ✅ Stable | Current latest stable version |
|
|
503
|
+
| R6 | `R6` | ⚠️ Preview | May have limited support |
|
|
504
|
+
|
|
505
|
+
### Common Configuration Examples
|
|
506
|
+
|
|
507
|
+
```toml
|
|
508
|
+
# Production: Single version for minimal size
|
|
509
|
+
default = ["R4"]
|
|
510
|
+
|
|
511
|
+
# Development: Multiple versions for testing
|
|
512
|
+
default = ["R4", "R5"]
|
|
513
|
+
|
|
514
|
+
# Bleeding edge: Latest versions only
|
|
515
|
+
default = ["R5", "R6"]
|
|
516
|
+
|
|
517
|
+
# Maximum compatibility: All versions (not recommended for production)
|
|
518
|
+
default = ["R4", "R4B", "R5", "R6"]
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
## Project layout
|
|
522
|
+
|
|
523
|
+
```
|
|
524
|
+
crates/pysof/
|
|
525
|
+
├─ pyproject.toml # PEP 621 metadata, Python >=3.11, uv-compatible
|
|
526
|
+
├─ README.md
|
|
527
|
+
├─ src/
|
|
528
|
+
│ ├─ pysof/
|
|
529
|
+
│ │ └─ __init__.py # Python package root
|
|
530
|
+
│ └─ lib.rs # Rust PyO3 bindings
|
|
531
|
+
├─ tests/ # Rust tests (17 tests)
|
|
532
|
+
│ ├─ lib_tests.rs # Unit tests for core library functions
|
|
533
|
+
│ ├─ integration.rs # Integration tests for component interactions
|
|
534
|
+
│ └─ integration/ # Organized integration test modules
|
|
535
|
+
│ ├─ mod.rs
|
|
536
|
+
│ ├─ content_types.rs
|
|
537
|
+
│ ├─ error_handling.rs
|
|
538
|
+
│ └─ fhir_versions.rs
|
|
539
|
+
├─ python-tests/ # Python test suite (58 tests)
|
|
540
|
+
│ ├─ __init__.py
|
|
541
|
+
│ ├─ test_core_functions.py
|
|
542
|
+
│ ├─ test_content_types.py
|
|
543
|
+
│ ├─ test_fhir_versions.py
|
|
544
|
+
│ ├─ test_import.py
|
|
545
|
+
│ └─ test_package_metadata.py
|
|
546
|
+
└─ Cargo.toml # Rust crate metadata
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
## License
|
|
550
|
+
|
|
551
|
+
This package inherits the license from the repository root.
|
|
552
|
+
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
pysof-0.1.2.dist-info/METADATA,sha256=Z4lDuie6T4gehChRpWivscQPrGiGvJkz5dH_9R-T97E,17006
|
|
2
|
+
pysof-0.1.2.dist-info/WHEEL,sha256=XthGvlhx1v097WptSw_sEjm7LbRsMyYW6bZuLs4TDpg,96
|
|
3
|
+
pysof/__init__.py,sha256=s_PWzd0kLvPqAj5CTnmlEvqv1e76jZr8VutJD6Nch3k,10918
|
|
4
|
+
pysof/_pysof.cp311-win_amd64.pyd,sha256=eAEZDbBx5Li1xUHDV94HAXPds2CCoHe8r4fvJe1CR4o,87613952
|
|
5
|
+
pysof/_pysof.pyi,sha256=2ibUgIuek1UJOWOoKxulIF7_mqX0TovvtpJWzTKH144,1368
|
|
6
|
+
pysof/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
pysof-0.1.2.dist-info/RECORD,,
|