|
7 | 7 | from ...optional_dependencies import require_zlib
|
8 | 8 | import pathlib
|
9 | 9 | import os
|
| 10 | +import io |
| 11 | +import zipfile |
10 | 12 |
|
11 | 13 |
|
12 | 14 | def save_model(model: GeoModel, path: str | None = None, validate_serialization: bool = True):
|
@@ -44,10 +46,10 @@ def save_model(model: GeoModel, path: str | None = None, validate_serialization:
|
44 | 46 | # If no extension, add the valid extension
|
45 | 47 | path = str(path_obj) + VALID_EXTENSION
|
46 | 48 |
|
47 |
| - binary_file = model_to_binary(model) |
| 49 | + binary_file = model_to_bytes(model) |
48 | 50 |
|
49 | 51 | if validate_serialization:
|
50 |
| - model_deserialized = _deserialize_binary_file(binary_file) |
| 52 | + model_deserialized = _load_model_from_bytes(binary_file) |
51 | 53 | _validate_serialization(model, model_deserialized)
|
52 | 54 |
|
53 | 55 | # Create directory if it doesn't exist
|
@@ -129,8 +131,62 @@ def load_model(path: str) -> GeoModel:
|
129 | 131 | with open(path, 'rb') as f:
|
130 | 132 | binary_file = f.read()
|
131 | 133 |
|
132 |
| - return _deserialize_binary_file(binary_file) |
| 134 | + return _load_model_from_bytes(binary_file) |
133 | 135 |
|
| 136 | +def model_to_bytes(model: GeoModel) -> bytes: |
| 137 | + # 1) Make a fully deterministic JSON header |
| 138 | + # header_dict = model.model_dump(by_alias=True) |
| 139 | + # header_json = json.dumps( |
| 140 | + # header_dict, |
| 141 | + # sort_keys=True, # always sort object keys |
| 142 | + # separators=(",", ":"), # no extra whitespace |
| 143 | + # ).encode("utf-8") |
| 144 | + |
| 145 | + header_json = model.model_dump_json(by_alias=True, indent=4) |
| 146 | + |
| 147 | + # 2) Raw binary chunks (no additional zlib.compress here) |
| 148 | + input_raw = model.structural_frame.input_tables_binary |
| 149 | + grid_raw = model.grid.grid_binary |
| 150 | + |
| 151 | + # 3) Pack into a ZIP archive in a fixed order: |
| 152 | + |
| 153 | + buf = io.BytesIO() |
| 154 | + with zipfile.ZipFile( |
| 155 | + buf, mode="w", |
| 156 | + compression=zipfile.ZIP_DEFLATED, |
| 157 | + compresslevel=6 |
| 158 | + ) as zf: |
| 159 | + # Force a fixed timestamp (1980-01-01) so the file headers don't vary |
| 160 | + def make_info(name): |
| 161 | + zi = zipfile.ZipInfo(name, date_time=(1980,1,1,0,0,0)) |
| 162 | + zi.external_attr = 0 # clear OS-specific file permissions |
| 163 | + return zi |
| 164 | + |
| 165 | + zf.writestr(make_info("header.json"), header_json) |
| 166 | + zf.writestr(make_info("input.bin"), input_raw) |
| 167 | + zf.writestr(make_info("grid.bin"), grid_raw) |
| 168 | + |
| 169 | + return buf.getvalue() |
| 170 | + |
| 171 | +def _load_model_from_bytes(data: bytes) -> GeoModel: |
| 172 | + import json, zlib |
| 173 | + from ...core.data.encoders.converters import loading_model_from_binary |
| 174 | + |
| 175 | + buf = io.BytesIO(data) |
| 176 | + with zipfile.ZipFile(buf, "r") as zf: |
| 177 | + header_json = zf.read("header.json").decode("utf-8") |
| 178 | + # header = json.loads(header_json) |
| 179 | + input_raw = zf.read("input.bin") |
| 180 | + grid_raw = zf.read("grid.bin") |
| 181 | + |
| 182 | + # If you want to validate or decompress further, do it here… |
| 183 | + with loading_model_from_binary( |
| 184 | + input_binary=input_raw, |
| 185 | + grid_binary= grid_raw |
| 186 | + ): |
| 187 | + model = GeoModel.model_validate_json(header_json) |
| 188 | + |
| 189 | + return model |
134 | 190 |
|
135 | 191 | def _deserialize_binary_file(binary_file):
|
136 | 192 | import json
|
|
0 commit comments