mscs 2.2.0__py3-none-any.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.
- mscs/__init__.py +50 -0
- mscs/_core.py +1031 -0
- mscs/py.typed +0 -0
- mscs-2.2.0.dist-info/METADATA +234 -0
- mscs-2.2.0.dist-info/RECORD +7 -0
- mscs-2.2.0.dist-info/WHEEL +4 -0
- mscs-2.2.0.dist-info/licenses/LICENSE +21 -0
mscs/__init__.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MSCS — Safe serialization for Python. A secure, fast replacement for pickle.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
import mscs
|
|
6
|
+
|
|
7
|
+
data = mscs.dumps(obj)
|
|
8
|
+
obj = mscs.loads(data)
|
|
9
|
+
|
|
10
|
+
mscs.register(MyClass) # allow deserialization of custom classes
|
|
11
|
+
"""
|
|
12
|
+
from mscs._core import (
|
|
13
|
+
# Version
|
|
14
|
+
__version__,
|
|
15
|
+
# Public API
|
|
16
|
+
dump,
|
|
17
|
+
load,
|
|
18
|
+
dumps,
|
|
19
|
+
loads,
|
|
20
|
+
dump_compressed,
|
|
21
|
+
load_compressed,
|
|
22
|
+
register,
|
|
23
|
+
register_alias,
|
|
24
|
+
register_module,
|
|
25
|
+
inspect,
|
|
26
|
+
benchmark,
|
|
27
|
+
copy,
|
|
28
|
+
# Exceptions
|
|
29
|
+
MSCError,
|
|
30
|
+
MSCEncodeError,
|
|
31
|
+
MSCDecodeError,
|
|
32
|
+
MSCSecurityError,
|
|
33
|
+
# Constants (for advanced users)
|
|
34
|
+
MAGIC,
|
|
35
|
+
VERSION,
|
|
36
|
+
MAX_DEPTH,
|
|
37
|
+
MAX_SIZE,
|
|
38
|
+
MAX_COMPRESSED,
|
|
39
|
+
MAX_COLLECTION,
|
|
40
|
+
MAX_STRING,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
"__version__",
|
|
45
|
+
"dump", "load", "dumps", "loads",
|
|
46
|
+
"dump_compressed", "load_compressed",
|
|
47
|
+
"register", "register_alias", "register_module",
|
|
48
|
+
"inspect", "benchmark", "copy",
|
|
49
|
+
"MSCError", "MSCEncodeError", "MSCDecodeError", "MSCSecurityError",
|
|
50
|
+
]
|
mscs/_core.py
ADDED
|
@@ -0,0 +1,1031 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MSC Serial v2.2
|
|
3
|
+
===============
|
|
4
|
+
Reemplazo personal y seguro de pickle.
|
|
5
|
+
|
|
6
|
+
Soporta: dict, list, tuple, set, frozenset, str, int, float, complex,
|
|
7
|
+
bool, None, bytes, bytearray, datetime, date, time, timedelta,
|
|
8
|
+
Decimal, UUID, Path, Enum, numpy arrays, torch.Tensor,
|
|
9
|
+
dataclasses, objetos con __slots__, objetos custom registrados,
|
|
10
|
+
referencias circulares.
|
|
11
|
+
|
|
12
|
+
API compatible con pickle:
|
|
13
|
+
msc.dump(obj, file)
|
|
14
|
+
msc.load(file)
|
|
15
|
+
msc.dumps(obj) -> bytes
|
|
16
|
+
msc.loads(data) -> obj
|
|
17
|
+
msc.dump_compressed(obj, file)
|
|
18
|
+
msc.load_compressed(file)
|
|
19
|
+
|
|
20
|
+
Extras:
|
|
21
|
+
msc.register(cls) # registrar clase segura para deserialización
|
|
22
|
+
msc.register_alias(old, c) # alias para clases renombradas (backward compat)
|
|
23
|
+
msc.register_module(mod) # registrar todas las clases de un módulo
|
|
24
|
+
msc.inspect(data) -> dict # metadata sin deserializar
|
|
25
|
+
msc.benchmark(obj) -> dict # medir rendimiento
|
|
26
|
+
msc.copy(obj) -> obj # deep copy via round-trip
|
|
27
|
+
|
|
28
|
+
Seguridad:
|
|
29
|
+
- No ejecuta código arbitrario al deserializar
|
|
30
|
+
- Solo reconstruye objetos de clases explícitamente registradas
|
|
31
|
+
- Límites de profundidad y tamaño configurables
|
|
32
|
+
- Formato auditable con magic bytes + versión
|
|
33
|
+
- Sin importlib dinámico en deserialización
|
|
34
|
+
- Validación de numpy dtypes contra whitelist
|
|
35
|
+
- Protección anti zip-bomb en load_compressed
|
|
36
|
+
- NOTA: la seguridad del registry depende de que solo se registren
|
|
37
|
+
clases confiables. __setstate__ de clases registradas SE EJECUTA.
|
|
38
|
+
- NOTA: ref tracking usa id(obj); como el encoder mantiene refs a
|
|
39
|
+
todos los objetos serializados, los IDs no se reutilizan durante
|
|
40
|
+
una sola llamada a encode().
|
|
41
|
+
|
|
42
|
+
Changelog v2.2:
|
|
43
|
+
- FIX: timedelta usa tag dedicado _TIMEDELTA2 (0x19) — elimina la
|
|
44
|
+
ambiguedad heuristica entre formatos v2.0 y v2.1
|
|
45
|
+
- FIX: _encode_str ahora valida longitud contra MAX_STRING
|
|
46
|
+
- FIX: load_compressed protegido contra zip bombs (valida tamaño
|
|
47
|
+
comprimido Y descomprimido)
|
|
48
|
+
- ADD: soporte nativo torch.Tensor (tag 0x18) — serializa dtype,
|
|
49
|
+
shape, requires_grad sin conversión manual a numpy
|
|
50
|
+
- ADD: register_alias(old_path, cls) para backward-compat con
|
|
51
|
+
checkpoints de clases renombradas/movidas
|
|
52
|
+
- Retrocompatible con payloads v2.1, v2.0 y v1.0
|
|
53
|
+
|
|
54
|
+
Changelog v2.1:
|
|
55
|
+
- FIX: timedelta ahora codifica days/seconds/microseconds por separado
|
|
56
|
+
(v2.0 perdía precisión al usar total_seconds() como float)
|
|
57
|
+
- FIX: validación de numpy dtype contra whitelist de tipos seguros
|
|
58
|
+
- ADD: soporte UUID nativo
|
|
59
|
+
- ADD: soporte pathlib.Path nativo
|
|
60
|
+
- ADD: register_module() para registro masivo de clases
|
|
61
|
+
- ADD: copy() — deep copy vía serialización round-trip
|
|
62
|
+
- ADD: inspect() ahora muestra nombre del tag raíz
|
|
63
|
+
- ADD: contexto de ruta en errores de decode (breadcrumbs)
|
|
64
|
+
- Retrocompatible con payloads v2.0 y v1.0
|
|
65
|
+
|
|
66
|
+
Changelog v2.0:
|
|
67
|
+
- Registry de clases seguras (elimina importlib dinámico)
|
|
68
|
+
- Soporte: complex, frozenset, datetime/date/time/timedelta, Decimal, Enum
|
|
69
|
+
- Detección y manejo de referencias circulares
|
|
70
|
+
- Límites de profundidad y tamaño máximo
|
|
71
|
+
- Streaming encode/decode para objetos grandes
|
|
72
|
+
- Mejor manejo de errores con excepciones tipadas
|
|
73
|
+
- Soporte bytearray nativo
|
|
74
|
+
- Benchmark integrado
|
|
75
|
+
- Validación de integridad con CRC32 opcional
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
import struct
|
|
79
|
+
import io
|
|
80
|
+
import zlib
|
|
81
|
+
import inspect as _inspect_mod
|
|
82
|
+
import dataclasses
|
|
83
|
+
from datetime import datetime, date, time, timedelta
|
|
84
|
+
from decimal import Decimal
|
|
85
|
+
from enum import Enum
|
|
86
|
+
from pathlib import Path
|
|
87
|
+
from uuid import UUID
|
|
88
|
+
from typing import Any, Type, Dict, Optional, Set, List
|
|
89
|
+
|
|
90
|
+
__version__ = "2.2"
|
|
91
|
+
__all__ = [
|
|
92
|
+
"dump", "load", "dumps", "loads",
|
|
93
|
+
"dump_compressed", "load_compressed",
|
|
94
|
+
"register", "register_alias", "register_module",
|
|
95
|
+
"inspect", "benchmark", "copy",
|
|
96
|
+
"MSCError", "MSCEncodeError", "MSCDecodeError", "MSCSecurityError",
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
# ─────────────────────── EXCEPTIONS ───────────────────────────────
|
|
100
|
+
|
|
101
|
+
class MSCError(Exception):
|
|
102
|
+
"""Base para errores de MSC Serial."""
|
|
103
|
+
|
|
104
|
+
class MSCEncodeError(MSCError):
|
|
105
|
+
"""Error durante serialización."""
|
|
106
|
+
|
|
107
|
+
class MSCDecodeError(MSCError):
|
|
108
|
+
"""Error durante deserialización."""
|
|
109
|
+
|
|
110
|
+
class MSCSecurityError(MSCError):
|
|
111
|
+
"""Intento de deserializar clase no registrada."""
|
|
112
|
+
|
|
113
|
+
# ─────────────────────── TYPE TAGS ────────────────────────────────
|
|
114
|
+
|
|
115
|
+
_NONE = b'\x00'
|
|
116
|
+
_BOOL = b'\x01'
|
|
117
|
+
_INT = b'\x02'
|
|
118
|
+
_FLOAT = b'\x03'
|
|
119
|
+
_STR = b'\x04'
|
|
120
|
+
_BYTES = b'\x05'
|
|
121
|
+
_LIST = b'\x06'
|
|
122
|
+
_TUPLE = b'\x07'
|
|
123
|
+
_DICT = b'\x08'
|
|
124
|
+
_SET = b'\x09'
|
|
125
|
+
_NDARRAY = b'\x0A'
|
|
126
|
+
_OBJ = b'\x0B'
|
|
127
|
+
_COMPLEX = b'\x0C'
|
|
128
|
+
_FROZENSET = b'\x0D'
|
|
129
|
+
_DATETIME = b'\x0E'
|
|
130
|
+
_DATE = b'\x0F'
|
|
131
|
+
_TIME = b'\x10'
|
|
132
|
+
_TIMEDELTA = b'\x11'
|
|
133
|
+
_DECIMAL = b'\x12'
|
|
134
|
+
_ENUM = b'\x13'
|
|
135
|
+
_BYTEARRAY = b'\x14'
|
|
136
|
+
_REF = b'\x15'
|
|
137
|
+
_UUID = b'\x16'
|
|
138
|
+
_PATH = b'\x17'
|
|
139
|
+
_TENSOR = b'\x18'
|
|
140
|
+
_TIMEDELTA2 = b'\x19' # v2.2: timedelta sin ambiguedad
|
|
141
|
+
|
|
142
|
+
_TAG_NAMES: Dict[int, str] = {
|
|
143
|
+
0x00: 'None', 0x01: 'bool', 0x02: 'int', 0x03: 'float',
|
|
144
|
+
0x04: 'str', 0x05: 'bytes', 0x06: 'list', 0x07: 'tuple',
|
|
145
|
+
0x08: 'dict', 0x09: 'set', 0x0A: 'ndarray', 0x0B: 'object',
|
|
146
|
+
0x0C: 'complex', 0x0D: 'frozenset', 0x0E: 'datetime', 0x0F: 'date',
|
|
147
|
+
0x10: 'time', 0x11: 'timedelta', 0x12: 'Decimal', 0x13: 'Enum',
|
|
148
|
+
0x14: 'bytearray', 0x15: 'ref', 0x16: 'UUID', 0x17: 'Path',
|
|
149
|
+
0x18: 'tensor', 0x19: 'timedelta2',
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
MAGIC = b'MSCS'
|
|
153
|
+
VERSION = b'\x02' # formato binario sigue siendo v2; cambios son aditivos
|
|
154
|
+
|
|
155
|
+
# ─────────────────────── LIMITS ───────────────────────────────────
|
|
156
|
+
|
|
157
|
+
MAX_DEPTH = 256
|
|
158
|
+
MAX_SIZE = 512 * 1024 * 1024 # 512 MB
|
|
159
|
+
MAX_COMPRESSED = 512 * 1024 * 1024 # 512 MB (compressed input limit, anti zip-bomb)
|
|
160
|
+
MAX_COLLECTION = 10_000_000
|
|
161
|
+
MAX_STRING = 100 * 1024 * 1024 # 100 MB
|
|
162
|
+
|
|
163
|
+
# ─────────────────── NUMPY DTYPE WHITELIST ────────────────────────
|
|
164
|
+
|
|
165
|
+
_SAFE_NUMPY_DTYPES: Set[str] = {
|
|
166
|
+
# Enteros
|
|
167
|
+
'int8', 'int16', 'int32', 'int64',
|
|
168
|
+
'uint8', 'uint16', 'uint32', 'uint64',
|
|
169
|
+
# Flotantes
|
|
170
|
+
'float16', 'float32', 'float64', 'float128',
|
|
171
|
+
# Complejos
|
|
172
|
+
'complex64', 'complex128', 'complex256',
|
|
173
|
+
# Bool y bytes
|
|
174
|
+
'bool', 'bool_',
|
|
175
|
+
# Strings fijos
|
|
176
|
+
# (aceptamos S<n> y U<n> por regex abajo)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
import re as _re
|
|
181
|
+
_RE_DTYPE_SHORT = _re.compile(r'[fiubcUSV]\d+')
|
|
182
|
+
_RE_DTYPE_LONG = _re.compile(r'(int|uint|float|complex|bool)\d*_?')
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _is_safe_dtype(dtype_str: str) -> bool:
|
|
186
|
+
"""Valida que un dtype string sea seguro (no structured/object/void)."""
|
|
187
|
+
clean = dtype_str.strip().lower()
|
|
188
|
+
# Rechazar explícitamente tipos peligrosos
|
|
189
|
+
if clean in ('object', 'O', 'void', 'V'):
|
|
190
|
+
return False
|
|
191
|
+
# Tipos simples directos
|
|
192
|
+
if clean in _SAFE_NUMPY_DTYPES:
|
|
193
|
+
return True
|
|
194
|
+
# Con prefijo de byteorder: <f4, >i8, =f8, |b1, etc.
|
|
195
|
+
if len(clean) > 1 and clean[0] in '<>=|!':
|
|
196
|
+
clean = clean[1:]
|
|
197
|
+
# Numpy shorthand: f4, f8, i4, i8, u2, b1, c8, c16, etc.
|
|
198
|
+
if _RE_DTYPE_SHORT.fullmatch(clean):
|
|
199
|
+
# Rechazar V (void) — ya cubierto arriba
|
|
200
|
+
if clean[0] == 'V':
|
|
201
|
+
return False
|
|
202
|
+
return True
|
|
203
|
+
# Nombre completo con bitsize: float32, int64, etc.
|
|
204
|
+
if _RE_DTYPE_LONG.fullmatch(clean):
|
|
205
|
+
return True
|
|
206
|
+
return False
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# ─────────────────────── REGISTRY ─────────────────────────────────
|
|
210
|
+
|
|
211
|
+
_registry: Dict[str, Type] = {}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _class_key(cls: Type) -> str:
|
|
215
|
+
return f"{cls.__module__}.{cls.__qualname__}"
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def register(cls: Type) -> Type:
|
|
219
|
+
"""
|
|
220
|
+
Registra una clase como segura para deserialización.
|
|
221
|
+
Puede usarse como decorador:
|
|
222
|
+
|
|
223
|
+
@msc.register
|
|
224
|
+
@dataclass
|
|
225
|
+
class MiObjeto:
|
|
226
|
+
x: float
|
|
227
|
+
y: float
|
|
228
|
+
"""
|
|
229
|
+
key = _class_key(cls)
|
|
230
|
+
_registry[key] = cls
|
|
231
|
+
return cls
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def register_module(module) -> List[Type]:
|
|
235
|
+
"""
|
|
236
|
+
Registra todas las clases definidas en un módulo.
|
|
237
|
+
Retorna lista de clases registradas.
|
|
238
|
+
|
|
239
|
+
import my_models
|
|
240
|
+
msc.register_module(my_models)
|
|
241
|
+
"""
|
|
242
|
+
registered = []
|
|
243
|
+
for name, obj in _inspect_mod.getmembers(module, _inspect_mod.isclass):
|
|
244
|
+
# Solo clases definidas EN el módulo (no importadas de stdlib, etc.)
|
|
245
|
+
if obj.__module__ == module.__name__:
|
|
246
|
+
register(obj)
|
|
247
|
+
registered.append(obj)
|
|
248
|
+
return registered
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def register_alias(alias: str, cls: Type) -> None:
|
|
252
|
+
"""
|
|
253
|
+
Registra un alias para una clase (backward-compat con checkpoints viejos).
|
|
254
|
+
|
|
255
|
+
# La clase se renombro de OldName a NewName
|
|
256
|
+
msc.register_alias("my_module.OldName", NewName)
|
|
257
|
+
"""
|
|
258
|
+
_registry[alias] = cls
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _is_registered(class_path: str) -> bool:
|
|
262
|
+
return class_path in _registry
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _get_registered(class_path: str) -> Type:
|
|
266
|
+
if class_path not in _registry:
|
|
267
|
+
raise MSCSecurityError(
|
|
268
|
+
f"Clase no registrada: {class_path!r}. "
|
|
269
|
+
f"Usa msc.register({class_path.rsplit('.', 1)[-1]}) antes de deserializar."
|
|
270
|
+
)
|
|
271
|
+
return _registry[class_path]
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
# ──────────────────────── ENCODER ─────────────────────────────────
|
|
275
|
+
|
|
276
|
+
class _Encoder:
|
|
277
|
+
__slots__ = ('buf', 'depth', 'refs', 'ref_counter', 'use_refs')
|
|
278
|
+
|
|
279
|
+
def __init__(self, buf: io.BytesIO, *, use_refs: bool = True):
|
|
280
|
+
self.buf = buf
|
|
281
|
+
self.depth = 0
|
|
282
|
+
self.refs: Dict[int, int] = {} # id(obj) -> ref_id
|
|
283
|
+
self.ref_counter = 0
|
|
284
|
+
self.use_refs = use_refs
|
|
285
|
+
|
|
286
|
+
def encode(self, obj: Any):
|
|
287
|
+
self.depth += 1
|
|
288
|
+
if self.depth > MAX_DEPTH:
|
|
289
|
+
raise MSCEncodeError(
|
|
290
|
+
f"Profundidad máxima excedida ({MAX_DEPTH}). "
|
|
291
|
+
f"¿Referencia circular no detectada?"
|
|
292
|
+
)
|
|
293
|
+
try:
|
|
294
|
+
self._encode(obj)
|
|
295
|
+
finally:
|
|
296
|
+
self.depth -= 1
|
|
297
|
+
|
|
298
|
+
def _assign_ref(self, obj: Any) -> bool:
|
|
299
|
+
"""Retorna True si el objeto ya fue serializado (escribe REF)."""
|
|
300
|
+
if not self.use_refs:
|
|
301
|
+
return False
|
|
302
|
+
oid = id(obj)
|
|
303
|
+
if oid in self.refs:
|
|
304
|
+
self.buf.write(_REF)
|
|
305
|
+
self.buf.write(struct.pack('<I', self.refs[oid]))
|
|
306
|
+
return True
|
|
307
|
+
self.refs[oid] = self.ref_counter
|
|
308
|
+
self.ref_counter += 1
|
|
309
|
+
return False
|
|
310
|
+
|
|
311
|
+
def _write_length(self, n: int, max_val: int = MAX_COLLECTION, label: str = "colección"):
|
|
312
|
+
if n > max_val:
|
|
313
|
+
raise MSCEncodeError(f"Tamaño de {label} excede límite: {n:,} > {max_val:,}")
|
|
314
|
+
self.buf.write(struct.pack('<I', n))
|
|
315
|
+
|
|
316
|
+
def _encode(self, obj: Any):
|
|
317
|
+
buf = self.buf
|
|
318
|
+
|
|
319
|
+
# ── Singletons y primitivos inmutables (sin ref tracking) ──
|
|
320
|
+
|
|
321
|
+
if obj is None:
|
|
322
|
+
buf.write(_NONE)
|
|
323
|
+
return
|
|
324
|
+
|
|
325
|
+
if isinstance(obj, bool): # antes de int
|
|
326
|
+
buf.write(_BOOL)
|
|
327
|
+
buf.write(b'\x01' if obj else b'\x00')
|
|
328
|
+
return
|
|
329
|
+
|
|
330
|
+
if isinstance(obj, int):
|
|
331
|
+
buf.write(_INT)
|
|
332
|
+
if obj == 0:
|
|
333
|
+
buf.write(struct.pack('<H', 1))
|
|
334
|
+
buf.write(b'\x00')
|
|
335
|
+
else:
|
|
336
|
+
n_bytes = (obj.bit_length() + 8) // 8
|
|
337
|
+
raw = obj.to_bytes(n_bytes, 'little', signed=True)
|
|
338
|
+
buf.write(struct.pack('<H', len(raw)))
|
|
339
|
+
buf.write(raw)
|
|
340
|
+
return
|
|
341
|
+
|
|
342
|
+
if isinstance(obj, float):
|
|
343
|
+
buf.write(_FLOAT)
|
|
344
|
+
buf.write(struct.pack('<d', obj))
|
|
345
|
+
return
|
|
346
|
+
|
|
347
|
+
if isinstance(obj, complex):
|
|
348
|
+
buf.write(_COMPLEX)
|
|
349
|
+
buf.write(struct.pack('<dd', obj.real, obj.imag))
|
|
350
|
+
return
|
|
351
|
+
|
|
352
|
+
# ── Strings y bytes (ref tracking para grandes) ──
|
|
353
|
+
|
|
354
|
+
if isinstance(obj, str):
|
|
355
|
+
if self._assign_ref(obj):
|
|
356
|
+
return
|
|
357
|
+
buf.write(_STR)
|
|
358
|
+
raw = obj.encode('utf-8')
|
|
359
|
+
self._write_length(len(raw), MAX_STRING, "string")
|
|
360
|
+
buf.write(raw)
|
|
361
|
+
return
|
|
362
|
+
|
|
363
|
+
if isinstance(obj, bytearray):
|
|
364
|
+
if self._assign_ref(obj):
|
|
365
|
+
return
|
|
366
|
+
buf.write(_BYTEARRAY)
|
|
367
|
+
self._write_length(len(obj), MAX_STRING, "bytearray")
|
|
368
|
+
buf.write(bytes(obj))
|
|
369
|
+
return
|
|
370
|
+
|
|
371
|
+
if isinstance(obj, bytes):
|
|
372
|
+
if self._assign_ref(obj):
|
|
373
|
+
return
|
|
374
|
+
buf.write(_BYTES)
|
|
375
|
+
self._write_length(len(obj), MAX_STRING, "bytes")
|
|
376
|
+
buf.write(obj)
|
|
377
|
+
return
|
|
378
|
+
|
|
379
|
+
# ── UUID ──
|
|
380
|
+
|
|
381
|
+
if isinstance(obj, UUID):
|
|
382
|
+
buf.write(_UUID)
|
|
383
|
+
buf.write(obj.bytes) # siempre 16 bytes
|
|
384
|
+
return
|
|
385
|
+
|
|
386
|
+
# ── Path ──
|
|
387
|
+
|
|
388
|
+
if isinstance(obj, Path):
|
|
389
|
+
buf.write(_PATH)
|
|
390
|
+
raw = str(obj).encode('utf-8')
|
|
391
|
+
self._write_length(len(raw), MAX_STRING, "path")
|
|
392
|
+
buf.write(raw)
|
|
393
|
+
return
|
|
394
|
+
|
|
395
|
+
# ── Tipos temporales ──
|
|
396
|
+
|
|
397
|
+
if isinstance(obj, datetime):
|
|
398
|
+
buf.write(_DATETIME)
|
|
399
|
+
ts = obj.isoformat()
|
|
400
|
+
raw = ts.encode('utf-8')
|
|
401
|
+
buf.write(struct.pack('<H', len(raw)))
|
|
402
|
+
buf.write(raw)
|
|
403
|
+
return
|
|
404
|
+
|
|
405
|
+
if isinstance(obj, date):
|
|
406
|
+
buf.write(_DATE)
|
|
407
|
+
buf.write(struct.pack('<HBB', obj.year, obj.month, obj.day))
|
|
408
|
+
return
|
|
409
|
+
|
|
410
|
+
if isinstance(obj, time):
|
|
411
|
+
buf.write(_TIME)
|
|
412
|
+
ts = obj.isoformat()
|
|
413
|
+
raw = ts.encode('utf-8')
|
|
414
|
+
buf.write(struct.pack('<H', len(raw)))
|
|
415
|
+
buf.write(raw)
|
|
416
|
+
return
|
|
417
|
+
|
|
418
|
+
if isinstance(obj, timedelta):
|
|
419
|
+
# v2.2: tag dedicado sin ambiguedad con v2.0
|
|
420
|
+
buf.write(_TIMEDELTA2)
|
|
421
|
+
buf.write(struct.pack('<iiI', obj.days, obj.seconds, obj.microseconds))
|
|
422
|
+
return
|
|
423
|
+
|
|
424
|
+
if isinstance(obj, Decimal):
|
|
425
|
+
buf.write(_DECIMAL)
|
|
426
|
+
raw = str(obj).encode('utf-8')
|
|
427
|
+
buf.write(struct.pack('<H', len(raw)))
|
|
428
|
+
buf.write(raw)
|
|
429
|
+
return
|
|
430
|
+
|
|
431
|
+
# ── Enum ──
|
|
432
|
+
|
|
433
|
+
if isinstance(obj, Enum):
|
|
434
|
+
buf.write(_ENUM)
|
|
435
|
+
cls_path = _class_key(type(obj))
|
|
436
|
+
self._encode_str(cls_path)
|
|
437
|
+
self.encode(obj.value)
|
|
438
|
+
return
|
|
439
|
+
|
|
440
|
+
# ── Colecciones (con ref tracking) ──
|
|
441
|
+
|
|
442
|
+
if isinstance(obj, list):
|
|
443
|
+
if self._assign_ref(obj):
|
|
444
|
+
return
|
|
445
|
+
buf.write(_LIST)
|
|
446
|
+
self._write_length(len(obj))
|
|
447
|
+
for item in obj:
|
|
448
|
+
self.encode(item)
|
|
449
|
+
return
|
|
450
|
+
|
|
451
|
+
if isinstance(obj, tuple):
|
|
452
|
+
if self._assign_ref(obj):
|
|
453
|
+
return
|
|
454
|
+
buf.write(_TUPLE)
|
|
455
|
+
self._write_length(len(obj))
|
|
456
|
+
for item in obj:
|
|
457
|
+
self.encode(item)
|
|
458
|
+
return
|
|
459
|
+
|
|
460
|
+
if isinstance(obj, frozenset):
|
|
461
|
+
if self._assign_ref(obj):
|
|
462
|
+
return
|
|
463
|
+
buf.write(_FROZENSET)
|
|
464
|
+
items = sorted(obj, key=repr)
|
|
465
|
+
self._write_length(len(items))
|
|
466
|
+
for item in items:
|
|
467
|
+
self.encode(item)
|
|
468
|
+
return
|
|
469
|
+
|
|
470
|
+
if isinstance(obj, set):
|
|
471
|
+
if self._assign_ref(obj):
|
|
472
|
+
return
|
|
473
|
+
buf.write(_SET)
|
|
474
|
+
items = sorted(obj, key=repr)
|
|
475
|
+
self._write_length(len(items))
|
|
476
|
+
for item in items:
|
|
477
|
+
self.encode(item)
|
|
478
|
+
return
|
|
479
|
+
|
|
480
|
+
if isinstance(obj, dict):
|
|
481
|
+
if self._assign_ref(obj):
|
|
482
|
+
return
|
|
483
|
+
buf.write(_DICT)
|
|
484
|
+
self._write_length(len(obj))
|
|
485
|
+
for k, v in obj.items():
|
|
486
|
+
self.encode(k)
|
|
487
|
+
self.encode(v)
|
|
488
|
+
return
|
|
489
|
+
|
|
490
|
+
# ── Numpy ──
|
|
491
|
+
|
|
492
|
+
try:
|
|
493
|
+
import numpy as np
|
|
494
|
+
if isinstance(obj, np.ndarray):
|
|
495
|
+
if self._assign_ref(obj):
|
|
496
|
+
return
|
|
497
|
+
dtype_s = str(obj.dtype)
|
|
498
|
+
if not _is_safe_dtype(dtype_s):
|
|
499
|
+
raise MSCEncodeError(
|
|
500
|
+
f"numpy dtype no permitido: {dtype_s!r}. "
|
|
501
|
+
f"Solo se permiten dtypes numéricos simples."
|
|
502
|
+
)
|
|
503
|
+
buf.write(_NDARRAY)
|
|
504
|
+
shape_s = 'x'.join(map(str, obj.shape)) if obj.shape else ''
|
|
505
|
+
meta = f"{dtype_s}|{shape_s}"
|
|
506
|
+
self._encode_str(meta)
|
|
507
|
+
raw = obj.tobytes()
|
|
508
|
+
self._write_length(len(raw), MAX_SIZE, "ndarray data")
|
|
509
|
+
buf.write(raw)
|
|
510
|
+
return
|
|
511
|
+
except ImportError:
|
|
512
|
+
pass
|
|
513
|
+
|
|
514
|
+
# ── PyTorch Tensor ──
|
|
515
|
+
|
|
516
|
+
try:
|
|
517
|
+
import torch
|
|
518
|
+
if isinstance(obj, torch.Tensor):
|
|
519
|
+
if self._assign_ref(obj):
|
|
520
|
+
return
|
|
521
|
+
# Mover a CPU y hacer contiguous para serializar
|
|
522
|
+
t = obj.detach().cpu().contiguous()
|
|
523
|
+
import numpy as np # noqa: reusa np si ya importado por ndarray path
|
|
524
|
+
# Convertir a numpy para reutilizar la validación de dtype
|
|
525
|
+
arr = t.numpy()
|
|
526
|
+
dtype_s = str(arr.dtype)
|
|
527
|
+
if not _is_safe_dtype(dtype_s):
|
|
528
|
+
raise MSCEncodeError(
|
|
529
|
+
f"torch dtype no permitido: {obj.dtype} (numpy: {dtype_s!r})"
|
|
530
|
+
)
|
|
531
|
+
buf.write(_TENSOR)
|
|
532
|
+
shape_s = 'x'.join(map(str, arr.shape)) if arr.shape else ''
|
|
533
|
+
requires_grad = '1' if obj.requires_grad else '0'
|
|
534
|
+
meta = f"{dtype_s}|{shape_s}|{requires_grad}"
|
|
535
|
+
self._encode_str(meta)
|
|
536
|
+
raw = arr.tobytes()
|
|
537
|
+
self._write_length(len(raw), MAX_SIZE, "tensor data")
|
|
538
|
+
buf.write(raw)
|
|
539
|
+
return
|
|
540
|
+
except ImportError:
|
|
541
|
+
pass
|
|
542
|
+
|
|
543
|
+
# ── Objeto registrado ──
|
|
544
|
+
|
|
545
|
+
if self._assign_ref(obj):
|
|
546
|
+
return
|
|
547
|
+
|
|
548
|
+
buf.write(_OBJ)
|
|
549
|
+
cls_path = _class_key(type(obj))
|
|
550
|
+
self._encode_str(cls_path)
|
|
551
|
+
|
|
552
|
+
if dataclasses.is_dataclass(obj) and not isinstance(obj, type):
|
|
553
|
+
state = {f.name: getattr(obj, f.name) for f in dataclasses.fields(obj)}
|
|
554
|
+
elif hasattr(obj, '__slots__') and not hasattr(obj, '__dict__'):
|
|
555
|
+
# __slots__ sin __dict__ → extraer slots del MRO completo.
|
|
556
|
+
# Priorizado sobre __getstate__ porque Python 3.11+ añade un
|
|
557
|
+
# __getstate__ por defecto que retorna (None, slots_dict) — un
|
|
558
|
+
# formato que complica la deserialización innecesariamente.
|
|
559
|
+
state = {}
|
|
560
|
+
for cls in type(obj).__mro__:
|
|
561
|
+
for s in getattr(cls, '__slots__', ()):
|
|
562
|
+
if hasattr(obj, s) and s not in state:
|
|
563
|
+
state[s] = getattr(obj, s)
|
|
564
|
+
elif '__getstate__' in type(obj).__dict__ or any(
|
|
565
|
+
'__getstate__' in c.__dict__ for c in type(obj).__mro__[:-1]
|
|
566
|
+
if c is not object
|
|
567
|
+
):
|
|
568
|
+
# Solo usar __getstate__ si fue definido explícitamente por el
|
|
569
|
+
# usuario, no el default de object.
|
|
570
|
+
state = obj.__getstate__()
|
|
571
|
+
elif hasattr(obj, '__dict__'):
|
|
572
|
+
state = obj.__dict__
|
|
573
|
+
else:
|
|
574
|
+
raise MSCEncodeError(f"No se puede serializar: {type(obj)!r}")
|
|
575
|
+
|
|
576
|
+
self.encode(state)
|
|
577
|
+
|
|
578
|
+
def _encode_str(self, s: str):
|
|
579
|
+
"""Encode string directamente sin ref tracking (para metadata interna)."""
|
|
580
|
+
self.buf.write(_STR)
|
|
581
|
+
raw = s.encode('utf-8')
|
|
582
|
+
if len(raw) > MAX_STRING:
|
|
583
|
+
raise MSCEncodeError(f"Metadata string excede limite: {len(raw):,} > {MAX_STRING:,}")
|
|
584
|
+
self.buf.write(struct.pack('<I', len(raw)))
|
|
585
|
+
self.buf.write(raw)
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
# ──────────────────────── DECODER ─────────────────────────────────
|
|
589
|
+
|
|
590
|
+
class _Decoder:
|
|
591
|
+
__slots__ = ('buf', 'depth', 'refs', 'strict', 'path')
|
|
592
|
+
|
|
593
|
+
def __init__(self, buf: io.BytesIO, *, strict: bool = True):
|
|
594
|
+
self.buf = buf
|
|
595
|
+
self.depth = 0
|
|
596
|
+
self.refs: Dict[int, Any] = {}
|
|
597
|
+
self.strict = strict
|
|
598
|
+
self.path: List[str] = [] # breadcrumbs para errores
|
|
599
|
+
|
|
600
|
+
def decode(self) -> Any:
|
|
601
|
+
self.depth += 1
|
|
602
|
+
if self.depth > MAX_DEPTH:
|
|
603
|
+
raise MSCDecodeError(
|
|
604
|
+
f"Profundidad máxima excedida ({MAX_DEPTH}) en {self._path_str()}"
|
|
605
|
+
)
|
|
606
|
+
try:
|
|
607
|
+
return self._decode()
|
|
608
|
+
except (MSCDecodeError, MSCSecurityError):
|
|
609
|
+
raise
|
|
610
|
+
except Exception as e:
|
|
611
|
+
raise MSCDecodeError(
|
|
612
|
+
f"Error en {self._path_str()}: {e}"
|
|
613
|
+
) from e
|
|
614
|
+
finally:
|
|
615
|
+
self.depth -= 1
|
|
616
|
+
|
|
617
|
+
def _path_str(self) -> str:
|
|
618
|
+
return ' → '.join(self.path) if self.path else '<root>'
|
|
619
|
+
|
|
620
|
+
def _read(self, n: int) -> bytes:
|
|
621
|
+
data = self.buf.read(n)
|
|
622
|
+
if len(data) < n:
|
|
623
|
+
raise MSCDecodeError(
|
|
624
|
+
f"Fin inesperado en {self._path_str()}: "
|
|
625
|
+
f"esperaba {n} bytes, obtuvo {len(data)}"
|
|
626
|
+
)
|
|
627
|
+
return data
|
|
628
|
+
|
|
629
|
+
def _read_length(self, max_val: int = MAX_COLLECTION) -> int:
|
|
630
|
+
n = struct.unpack('<I', self._read(4))[0]
|
|
631
|
+
if n > max_val:
|
|
632
|
+
raise MSCDecodeError(
|
|
633
|
+
f"Tamaño excede límite en {self._path_str()}: {n:,} > {max_val:,}"
|
|
634
|
+
)
|
|
635
|
+
return n
|
|
636
|
+
|
|
637
|
+
def _store_ref(self, obj: Any) -> Any:
|
|
638
|
+
self.refs[len(self.refs)] = obj
|
|
639
|
+
return obj
|
|
640
|
+
|
|
641
|
+
def _decode(self) -> Any:
|
|
642
|
+
tag = self._read(1)
|
|
643
|
+
|
|
644
|
+
if tag == _NONE:
|
|
645
|
+
return None
|
|
646
|
+
|
|
647
|
+
if tag == _BOOL:
|
|
648
|
+
return self._read(1) == b'\x01'
|
|
649
|
+
|
|
650
|
+
if tag == _INT:
|
|
651
|
+
n = struct.unpack('<H', self._read(2))[0]
|
|
652
|
+
return int.from_bytes(self._read(n), 'little', signed=True)
|
|
653
|
+
|
|
654
|
+
if tag == _FLOAT:
|
|
655
|
+
return struct.unpack('<d', self._read(8))[0]
|
|
656
|
+
|
|
657
|
+
if tag == _COMPLEX:
|
|
658
|
+
r, i = struct.unpack('<dd', self._read(16))
|
|
659
|
+
return complex(r, i)
|
|
660
|
+
|
|
661
|
+
if tag == _REF:
|
|
662
|
+
ref_id = struct.unpack('<I', self._read(4))[0]
|
|
663
|
+
if ref_id not in self.refs:
|
|
664
|
+
raise MSCDecodeError(
|
|
665
|
+
f"Referencia inválida: {ref_id} en {self._path_str()}"
|
|
666
|
+
)
|
|
667
|
+
return self.refs[ref_id]
|
|
668
|
+
|
|
669
|
+
if tag == _STR:
|
|
670
|
+
n = self._read_length(MAX_STRING)
|
|
671
|
+
s = self._read(n).decode('utf-8')
|
|
672
|
+
return self._store_ref(s)
|
|
673
|
+
|
|
674
|
+
if tag == _BYTES:
|
|
675
|
+
n = self._read_length(MAX_STRING)
|
|
676
|
+
b = self._read(n)
|
|
677
|
+
return self._store_ref(b)
|
|
678
|
+
|
|
679
|
+
if tag == _BYTEARRAY:
|
|
680
|
+
n = self._read_length(MAX_STRING)
|
|
681
|
+
ba = bytearray(self._read(n))
|
|
682
|
+
return self._store_ref(ba)
|
|
683
|
+
|
|
684
|
+
if tag == _UUID:
|
|
685
|
+
raw = self._read(16)
|
|
686
|
+
return UUID(bytes=raw)
|
|
687
|
+
|
|
688
|
+
if tag == _PATH:
|
|
689
|
+
n = self._read_length(MAX_STRING)
|
|
690
|
+
s = self._read(n).decode('utf-8')
|
|
691
|
+
return Path(s)
|
|
692
|
+
|
|
693
|
+
if tag == _DATETIME:
|
|
694
|
+
n = struct.unpack('<H', self._read(2))[0]
|
|
695
|
+
s = self._read(n).decode('utf-8')
|
|
696
|
+
return datetime.fromisoformat(s)
|
|
697
|
+
|
|
698
|
+
if tag == _DATE:
|
|
699
|
+
y, m, d = struct.unpack('<HBB', self._read(4))
|
|
700
|
+
return date(y, m, d)
|
|
701
|
+
|
|
702
|
+
if tag == _TIME:
|
|
703
|
+
n = struct.unpack('<H', self._read(2))[0]
|
|
704
|
+
s = self._read(n).decode('utf-8')
|
|
705
|
+
return time.fromisoformat(s)
|
|
706
|
+
|
|
707
|
+
if tag == _TIMEDELTA2:
|
|
708
|
+
# v2.2: tag dedicado, sin ambiguedad
|
|
709
|
+
days, secs, us = struct.unpack('<iiI', self._read(12))
|
|
710
|
+
return timedelta(days=days, seconds=secs, microseconds=us)
|
|
711
|
+
|
|
712
|
+
if tag == _TIMEDELTA:
|
|
713
|
+
# Legacy: payloads v2.0/v2.1 usaban el mismo tag para 2 formatos.
|
|
714
|
+
# Heuristica: v2.1 = (days:i4, seconds:i4, microseconds:U4)
|
|
715
|
+
# v2.0 = (days:i4, total_seconds:f8)
|
|
716
|
+
raw12 = self._read(12)
|
|
717
|
+
days_21, secs_21, us_21 = struct.unpack('<iiI', raw12)
|
|
718
|
+
if 0 <= secs_21 < 86400 and us_21 < 1_000_000:
|
|
719
|
+
return timedelta(days=days_21, seconds=secs_21, microseconds=us_21)
|
|
720
|
+
# Fallback v2.0
|
|
721
|
+
_days_20, total_20 = struct.unpack('<id', raw12)
|
|
722
|
+
return timedelta(seconds=total_20)
|
|
723
|
+
|
|
724
|
+
if tag == _DECIMAL:
|
|
725
|
+
n = struct.unpack('<H', self._read(2))[0]
|
|
726
|
+
s = self._read(n).decode('utf-8')
|
|
727
|
+
return Decimal(s)
|
|
728
|
+
|
|
729
|
+
if tag == _ENUM:
|
|
730
|
+
class_path = self._decode_str()
|
|
731
|
+
self.path.append(f'Enum({class_path})')
|
|
732
|
+
value = self.decode()
|
|
733
|
+
self.path.pop()
|
|
734
|
+
if self.strict:
|
|
735
|
+
cls = _get_registered(class_path)
|
|
736
|
+
return cls(value)
|
|
737
|
+
else:
|
|
738
|
+
return {'__enum__': class_path, '__value__': value}
|
|
739
|
+
|
|
740
|
+
if tag == _LIST:
|
|
741
|
+
n = self._read_length()
|
|
742
|
+
result = []
|
|
743
|
+
self._store_ref(result)
|
|
744
|
+
for i in range(n):
|
|
745
|
+
self.path.append(f'[{i}]')
|
|
746
|
+
result.append(self.decode())
|
|
747
|
+
self.path.pop()
|
|
748
|
+
return result
|
|
749
|
+
|
|
750
|
+
if tag == _TUPLE:
|
|
751
|
+
n = self._read_length()
|
|
752
|
+
items = []
|
|
753
|
+
for i in range(n):
|
|
754
|
+
self.path.append(f'({i})')
|
|
755
|
+
items.append(self.decode())
|
|
756
|
+
self.path.pop()
|
|
757
|
+
t = tuple(items)
|
|
758
|
+
return self._store_ref(t)
|
|
759
|
+
|
|
760
|
+
if tag == _FROZENSET:
|
|
761
|
+
n = self._read_length()
|
|
762
|
+
items = frozenset(self.decode() for _ in range(n))
|
|
763
|
+
return self._store_ref(items)
|
|
764
|
+
|
|
765
|
+
if tag == _SET:
|
|
766
|
+
n = self._read_length()
|
|
767
|
+
result = set()
|
|
768
|
+
self._store_ref(result)
|
|
769
|
+
result.update(self.decode() for _ in range(n))
|
|
770
|
+
return result
|
|
771
|
+
|
|
772
|
+
if tag == _DICT:
|
|
773
|
+
n = self._read_length()
|
|
774
|
+
result = {}
|
|
775
|
+
self._store_ref(result)
|
|
776
|
+
for _ in range(n):
|
|
777
|
+
k = self.decode()
|
|
778
|
+
self.path.append(f'.{k!r}' if isinstance(k, str) else f'[{k!r}]')
|
|
779
|
+
v = self.decode()
|
|
780
|
+
self.path.pop()
|
|
781
|
+
result[k] = v
|
|
782
|
+
return result
|
|
783
|
+
|
|
784
|
+
if tag == _NDARRAY:
|
|
785
|
+
try:
|
|
786
|
+
import numpy as np
|
|
787
|
+
except ImportError:
|
|
788
|
+
raise MSCDecodeError("numpy requerido para deserializar arrays")
|
|
789
|
+
meta = self._decode_str()
|
|
790
|
+
dtype_str, shape_str = meta.split('|')
|
|
791
|
+
if not _is_safe_dtype(dtype_str):
|
|
792
|
+
raise MSCSecurityError(
|
|
793
|
+
f"numpy dtype no permitido en deserialización: {dtype_str!r}"
|
|
794
|
+
)
|
|
795
|
+
shape = tuple(int(x) for x in shape_str.split('x')) if shape_str else ()
|
|
796
|
+
n = self._read_length(MAX_SIZE)
|
|
797
|
+
raw = self._read(n)
|
|
798
|
+
arr = np.frombuffer(raw, dtype=np.dtype(dtype_str)).copy().reshape(shape)
|
|
799
|
+
return self._store_ref(arr)
|
|
800
|
+
|
|
801
|
+
if tag == _TENSOR:
|
|
802
|
+
try:
|
|
803
|
+
import torch
|
|
804
|
+
import numpy as np
|
|
805
|
+
except ImportError:
|
|
806
|
+
raise MSCDecodeError("torch y numpy requeridos para deserializar tensores")
|
|
807
|
+
meta = self._decode_str()
|
|
808
|
+
parts = meta.split('|')
|
|
809
|
+
dtype_str, shape_str = parts[0], parts[1]
|
|
810
|
+
requires_grad = parts[2] == '1' if len(parts) > 2 else False
|
|
811
|
+
if not _is_safe_dtype(dtype_str):
|
|
812
|
+
raise MSCSecurityError(
|
|
813
|
+
f"tensor dtype no permitido: {dtype_str!r}"
|
|
814
|
+
)
|
|
815
|
+
shape = tuple(int(x) for x in shape_str.split('x')) if shape_str else ()
|
|
816
|
+
n = self._read_length(MAX_SIZE)
|
|
817
|
+
raw = self._read(n)
|
|
818
|
+
arr = np.frombuffer(raw, dtype=np.dtype(dtype_str)).copy().reshape(shape)
|
|
819
|
+
t = torch.from_numpy(arr)
|
|
820
|
+
if requires_grad:
|
|
821
|
+
t = t.requires_grad_(True)
|
|
822
|
+
return self._store_ref(t)
|
|
823
|
+
|
|
824
|
+
if tag == _OBJ:
|
|
825
|
+
class_path = self._decode_str()
|
|
826
|
+
self.path.append(class_path.rsplit('.', 1)[-1])
|
|
827
|
+
state = self.decode()
|
|
828
|
+
self.path.pop()
|
|
829
|
+
|
|
830
|
+
if self.strict:
|
|
831
|
+
cls = _get_registered(class_path)
|
|
832
|
+
elif _is_registered(class_path):
|
|
833
|
+
cls = _registry[class_path]
|
|
834
|
+
else:
|
|
835
|
+
return {'__class__': class_path, '__state__': state}
|
|
836
|
+
|
|
837
|
+
obj = cls.__new__(cls)
|
|
838
|
+
if dataclasses.is_dataclass(cls):
|
|
839
|
+
for k, v in state.items():
|
|
840
|
+
setattr(obj, k, v)
|
|
841
|
+
elif hasattr(obj, '__slots__') and not hasattr(obj, '__dict__'):
|
|
842
|
+
for k, v in state.items():
|
|
843
|
+
setattr(obj, k, v)
|
|
844
|
+
elif '__setstate__' in type(obj).__dict__ or any(
|
|
845
|
+
'__setstate__' in c.__dict__ for c in type(obj).__mro__[:-1]
|
|
846
|
+
if c is not object
|
|
847
|
+
):
|
|
848
|
+
obj.__setstate__(state)
|
|
849
|
+
elif hasattr(obj, '__dict__'):
|
|
850
|
+
obj.__dict__.update(state)
|
|
851
|
+
else:
|
|
852
|
+
# Fallback: intentar setattr (cubre slots con __dict__ mixto)
|
|
853
|
+
for k, v in state.items():
|
|
854
|
+
setattr(obj, k, v)
|
|
855
|
+
return obj
|
|
856
|
+
|
|
857
|
+
raise MSCDecodeError(
|
|
858
|
+
f"Tag desconocido: {tag!r} en {self._path_str()}"
|
|
859
|
+
)
|
|
860
|
+
|
|
861
|
+
def _decode_str(self) -> str:
|
|
862
|
+
"""Decode string sin afectar ref counter (para metadata interna)."""
|
|
863
|
+
tag = self._read(1)
|
|
864
|
+
if tag != _STR:
|
|
865
|
+
raise MSCDecodeError(
|
|
866
|
+
f"Esperaba STR tag, obtuvo {tag!r} en {self._path_str()}"
|
|
867
|
+
)
|
|
868
|
+
n = self._read_length(MAX_STRING)
|
|
869
|
+
return self._read(n).decode('utf-8')
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
# ──────────────────────── PUBLIC API ──────────────────────────────
|
|
873
|
+
|
|
874
|
+
def dumps(obj: Any, *, with_crc: bool = False) -> bytes:
|
|
875
|
+
"""Serializa obj a bytes."""
|
|
876
|
+
buf = io.BytesIO()
|
|
877
|
+
buf.write(MAGIC + VERSION)
|
|
878
|
+
flags = 0x01 if with_crc else 0x00
|
|
879
|
+
buf.write(struct.pack('B', flags))
|
|
880
|
+
enc = _Encoder(buf)
|
|
881
|
+
enc.encode(obj)
|
|
882
|
+
data = buf.getvalue()
|
|
883
|
+
if with_crc:
|
|
884
|
+
crc = zlib.crc32(data) & 0xFFFFFFFF
|
|
885
|
+
data += struct.pack('<I', crc)
|
|
886
|
+
return data
|
|
887
|
+
|
|
888
|
+
|
|
889
|
+
def loads(data: bytes, *, strict: bool = True) -> Any:
|
|
890
|
+
"""
|
|
891
|
+
Deserializa bytes a objeto.
|
|
892
|
+
strict=True: solo reconstruye clases registradas (lanza MSCSecurityError).
|
|
893
|
+
strict=False: clases no registradas retornan dict fallback.
|
|
894
|
+
"""
|
|
895
|
+
if len(data) < 6:
|
|
896
|
+
raise MSCDecodeError("Datos demasiado cortos para ser MSC Serial")
|
|
897
|
+
buf = io.BytesIO(data)
|
|
898
|
+
magic = buf.read(4)
|
|
899
|
+
if magic != MAGIC:
|
|
900
|
+
raise MSCDecodeError(f"Magic bytes inválidos: {magic!r}")
|
|
901
|
+
ver = buf.read(1)
|
|
902
|
+
if ver == b'\x01':
|
|
903
|
+
# Retrocompatibilidad con v1.0 (sin flags)
|
|
904
|
+
dec = _Decoder(buf, strict=False)
|
|
905
|
+
return dec.decode()
|
|
906
|
+
if ver != VERSION:
|
|
907
|
+
raise MSCDecodeError(f"Versión no soportada: {ver!r}")
|
|
908
|
+
flags = struct.unpack('B', buf.read(1))[0]
|
|
909
|
+
has_crc = bool(flags & 0x01)
|
|
910
|
+
if has_crc:
|
|
911
|
+
payload = data[:-4]
|
|
912
|
+
stored_crc = struct.unpack('<I', data[-4:])[0]
|
|
913
|
+
computed_crc = zlib.crc32(payload) & 0xFFFFFFFF
|
|
914
|
+
if stored_crc != computed_crc:
|
|
915
|
+
raise MSCDecodeError(
|
|
916
|
+
f"CRC32 no coincide: almacenado={stored_crc:#010x}, "
|
|
917
|
+
f"calculado={computed_crc:#010x}"
|
|
918
|
+
)
|
|
919
|
+
dec = _Decoder(buf, strict=strict)
|
|
920
|
+
return dec.decode()
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
def dump(obj: Any, file, **kwargs) -> None:
|
|
924
|
+
"""Serializa obj al archivo (modo binario)."""
|
|
925
|
+
file.write(dumps(obj, **kwargs))
|
|
926
|
+
|
|
927
|
+
|
|
928
|
+
def load(file, **kwargs) -> Any:
|
|
929
|
+
"""Deserializa desde archivo (modo binario)."""
|
|
930
|
+
return loads(file.read(), **kwargs)
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
def dump_compressed(obj: Any, file, level: int = 6, **kwargs) -> None:
|
|
934
|
+
"""Serializa con compresión zlib."""
|
|
935
|
+
raw = dumps(obj, **kwargs)
|
|
936
|
+
compressed = zlib.compress(raw, level)
|
|
937
|
+
file.write(struct.pack('<I', len(raw)))
|
|
938
|
+
file.write(compressed)
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
def load_compressed(file, **kwargs) -> Any:
|
|
942
|
+
"""Deserializa desde archivo comprimido."""
|
|
943
|
+
orig_size = struct.unpack('<I', file.read(4))[0]
|
|
944
|
+
if orig_size > MAX_SIZE:
|
|
945
|
+
raise MSCDecodeError(f"Tamaño original excede límite: {orig_size:,}")
|
|
946
|
+
compressed = file.read()
|
|
947
|
+
if len(compressed) > MAX_COMPRESSED:
|
|
948
|
+
raise MSCDecodeError(
|
|
949
|
+
f"Datos comprimidos exceden límite: {len(compressed):,} > {MAX_COMPRESSED:,}"
|
|
950
|
+
)
|
|
951
|
+
raw = zlib.decompress(compressed, bufsize=orig_size)
|
|
952
|
+
if len(raw) > MAX_SIZE:
|
|
953
|
+
raise MSCDecodeError(
|
|
954
|
+
f"Datos descomprimidos exceden límite: {len(raw):,} > {MAX_SIZE:,}"
|
|
955
|
+
)
|
|
956
|
+
return loads(raw, **kwargs)
|
|
957
|
+
|
|
958
|
+
|
|
959
|
+
def copy(obj: Any) -> Any:
|
|
960
|
+
"""Deep copy vía serialización round-trip. Más seguro que copy.deepcopy."""
|
|
961
|
+
return loads(dumps(obj), strict=False)
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
# ────────────────────── UTILIDADES ────────────────────────────────
|
|
965
|
+
|
|
966
|
+
def inspect(data: bytes) -> dict:
|
|
967
|
+
"""Retorna metadata del payload sin deserializar el objeto."""
|
|
968
|
+
if len(data) < 5 or data[:4] != MAGIC:
|
|
969
|
+
return {'valid': False, 'error': 'Magic bytes inválidos'}
|
|
970
|
+
|
|
971
|
+
ver = data[4]
|
|
972
|
+
info = {
|
|
973
|
+
'valid': True,
|
|
974
|
+
'version': ver,
|
|
975
|
+
'size_bytes': len(data),
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
root_tag = None
|
|
979
|
+
if ver == 1:
|
|
980
|
+
root_tag = data[5] if len(data) > 5 else None
|
|
981
|
+
elif ver == 2:
|
|
982
|
+
if len(data) > 6:
|
|
983
|
+
flags = data[5]
|
|
984
|
+
info['has_crc'] = bool(flags & 0x01)
|
|
985
|
+
root_tag = data[6]
|
|
986
|
+
else:
|
|
987
|
+
root_tag = None
|
|
988
|
+
else:
|
|
989
|
+
info['valid'] = False
|
|
990
|
+
info['error'] = f'Versión desconocida: {ver}'
|
|
991
|
+
return info
|
|
992
|
+
|
|
993
|
+
if root_tag is not None:
|
|
994
|
+
info['root_tag'] = hex(root_tag)
|
|
995
|
+
info['root_type'] = _TAG_NAMES.get(root_tag, 'unknown')
|
|
996
|
+
|
|
997
|
+
return info
|
|
998
|
+
|
|
999
|
+
|
|
1000
|
+
def benchmark(obj: Any, rounds: int = 100) -> dict:
|
|
1001
|
+
"""Mide rendimiento de serialización/deserialización."""
|
|
1002
|
+
import time as _time
|
|
1003
|
+
|
|
1004
|
+
# Encode
|
|
1005
|
+
t0 = _time.perf_counter()
|
|
1006
|
+
for _ in range(rounds):
|
|
1007
|
+
data = dumps(obj)
|
|
1008
|
+
encode_time = (_time.perf_counter() - t0) / rounds
|
|
1009
|
+
|
|
1010
|
+
# Decode
|
|
1011
|
+
t0 = _time.perf_counter()
|
|
1012
|
+
for _ in range(rounds):
|
|
1013
|
+
loads(data, strict=False)
|
|
1014
|
+
decode_time = (_time.perf_counter() - t0) / rounds
|
|
1015
|
+
|
|
1016
|
+
# Compressed
|
|
1017
|
+
raw_size = len(data)
|
|
1018
|
+
buf = io.BytesIO()
|
|
1019
|
+
dump_compressed(obj, buf)
|
|
1020
|
+
comp_size = len(buf.getvalue())
|
|
1021
|
+
|
|
1022
|
+
return {
|
|
1023
|
+
'encode_ms': round(encode_time * 1000, 3),
|
|
1024
|
+
'decode_ms': round(decode_time * 1000, 3),
|
|
1025
|
+
'raw_bytes': raw_size,
|
|
1026
|
+
'compressed_bytes': comp_size,
|
|
1027
|
+
'compression_ratio': round(raw_size / comp_size, 2) if comp_size else float('inf'),
|
|
1028
|
+
'rounds': rounds,
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
|
mscs/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mscs
|
|
3
|
+
Version: 2.2.0
|
|
4
|
+
Summary: Safe, fast serialization for Python — a secure replacement for pickle with native support for numpy arrays and PyTorch tensors.
|
|
5
|
+
Project-URL: Homepage, https://github.com/esraderey/mscs
|
|
6
|
+
Project-URL: Repository, https://github.com/esraderey/mscs
|
|
7
|
+
Project-URL: Issues, https://github.com/esraderey/mscs/issues
|
|
8
|
+
Author: Esraderey
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: binary,checkpoint,fast,numpy,pickle,pytorch,safe,secure,serialization,tensor
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
24
|
+
Classifier: Topic :: Security
|
|
25
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
26
|
+
Classifier: Typing :: Typed
|
|
27
|
+
Requires-Python: >=3.9
|
|
28
|
+
Provides-Extra: all
|
|
29
|
+
Requires-Dist: numpy>=1.20; extra == 'all'
|
|
30
|
+
Requires-Dist: torch>=2.0; extra == 'all'
|
|
31
|
+
Provides-Extra: numpy
|
|
32
|
+
Requires-Dist: numpy>=1.20; extra == 'numpy'
|
|
33
|
+
Provides-Extra: torch
|
|
34
|
+
Requires-Dist: numpy>=1.20; extra == 'torch'
|
|
35
|
+
Requires-Dist: torch>=2.0; extra == 'torch'
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
# MSCS — Safe Serialization for Python
|
|
39
|
+
|
|
40
|
+
A secure, fast, binary serialization library. Drop-in replacement for `pickle` that **never executes arbitrary code** during deserialization.
|
|
41
|
+
|
|
42
|
+
Built for AI/ML workflows — native support for **NumPy arrays** and **PyTorch tensors** with zero-copy performance.
|
|
43
|
+
|
|
44
|
+
## Why not pickle?
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
# pickle: arbitrary code execution on load
|
|
48
|
+
data = pickle.loads(untrusted_bytes) # can run os.system("rm -rf /")
|
|
49
|
+
|
|
50
|
+
# mscs: only reconstructs explicitly registered classes
|
|
51
|
+
data = mscs.loads(untrusted_bytes) # MSCSecurityError if class not registered
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Install
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install mscs # core (no dependencies)
|
|
58
|
+
pip install mscs[numpy] # + numpy support
|
|
59
|
+
pip install mscs[torch] # + numpy + PyTorch tensor support
|
|
60
|
+
pip install mscs[all] # everything
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Quick Start
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
import mscs
|
|
67
|
+
|
|
68
|
+
# Primitives, collections, nested structures — just works
|
|
69
|
+
data = {"model": "v5.2", "lr": 0.001, "layers": [64, 128, 256]}
|
|
70
|
+
encoded = mscs.dumps(data)
|
|
71
|
+
decoded = mscs.loads(encoded)
|
|
72
|
+
|
|
73
|
+
# NumPy arrays
|
|
74
|
+
import numpy as np
|
|
75
|
+
arr = np.random.randn(100, 100).astype(np.float32)
|
|
76
|
+
encoded = mscs.dumps(arr) # 39 KB (vs 39.5 KB pickle)
|
|
77
|
+
|
|
78
|
+
# PyTorch tensors — no .numpy() conversion needed
|
|
79
|
+
import torch
|
|
80
|
+
weights = torch.randn(256, 256)
|
|
81
|
+
encoded = mscs.dumps(weights) # safe, no pickle involved
|
|
82
|
+
|
|
83
|
+
# Full model checkpoints
|
|
84
|
+
checkpoint = {
|
|
85
|
+
"epoch": 100,
|
|
86
|
+
"model_state": {k: v for k, v in model.state_dict().items()},
|
|
87
|
+
"optimizer_lr": 0.0003,
|
|
88
|
+
}
|
|
89
|
+
mscs.dump(checkpoint, open("checkpoint.mscs", "wb"))
|
|
90
|
+
restored = mscs.load(open("checkpoint.mscs", "rb"))
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Custom Classes
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
import mscs
|
|
97
|
+
from dataclasses import dataclass
|
|
98
|
+
|
|
99
|
+
@mscs.register
|
|
100
|
+
@dataclass
|
|
101
|
+
class Config:
|
|
102
|
+
state_size: int = 256
|
|
103
|
+
lr: float = 0.001
|
|
104
|
+
|
|
105
|
+
config = Config(512, 0.0003)
|
|
106
|
+
data = mscs.dumps(config)
|
|
107
|
+
restored = mscs.loads(data) # Config(state_size=512, lr=0.0003)
|
|
108
|
+
|
|
109
|
+
# Unregistered classes raise MSCSecurityError in strict mode
|
|
110
|
+
mscs.loads(data_with_unknown_class) # MSCSecurityError
|
|
111
|
+
|
|
112
|
+
# Or get a dict fallback in non-strict mode
|
|
113
|
+
mscs.loads(data_with_unknown_class, strict=False) # {'__class__': '...', '__state__': {...}}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Backward Compatibility with Renamed Classes
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
# Class was renamed from OldConfig to Config
|
|
120
|
+
mscs.register_alias("my_module.OldConfig", Config)
|
|
121
|
+
# Old checkpoints now deserialize correctly
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Register All Classes in a Module
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
import my_models
|
|
128
|
+
mscs.register_module(my_models) # registers all classes defined in the module
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Compression & Integrity
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
# zlib compression
|
|
135
|
+
with open("data.mscs.z", "wb") as f:
|
|
136
|
+
mscs.dump_compressed(large_obj, f)
|
|
137
|
+
|
|
138
|
+
with open("data.mscs.z", "rb") as f:
|
|
139
|
+
obj = mscs.load_compressed(f)
|
|
140
|
+
|
|
141
|
+
# CRC32 integrity check
|
|
142
|
+
data = mscs.dumps(obj, with_crc=True)
|
|
143
|
+
mscs.loads(data) # verifies CRC, raises MSCDecodeError if corrupted
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## API Reference
|
|
147
|
+
|
|
148
|
+
### Core
|
|
149
|
+
|
|
150
|
+
| Function | Description |
|
|
151
|
+
|----------|------------|
|
|
152
|
+
| `dumps(obj, *, with_crc=False) -> bytes` | Serialize to bytes |
|
|
153
|
+
| `loads(data, *, strict=True) -> Any` | Deserialize from bytes |
|
|
154
|
+
| `dump(obj, file, **kwargs)` | Serialize to file (binary mode) |
|
|
155
|
+
| `load(file, **kwargs) -> Any` | Deserialize from file |
|
|
156
|
+
| `dump_compressed(obj, file, level=6)` | Serialize with zlib compression |
|
|
157
|
+
| `load_compressed(file) -> Any` | Deserialize compressed data |
|
|
158
|
+
|
|
159
|
+
### Registry
|
|
160
|
+
|
|
161
|
+
| Function | Description |
|
|
162
|
+
|----------|------------|
|
|
163
|
+
| `register(cls) -> cls` | Register class as safe (also works as decorator) |
|
|
164
|
+
| `register_alias(old_path, cls)` | Map old class path to new class |
|
|
165
|
+
| `register_module(module) -> list` | Register all classes in a module |
|
|
166
|
+
|
|
167
|
+
### Utilities
|
|
168
|
+
|
|
169
|
+
| Function | Description |
|
|
170
|
+
|----------|------------|
|
|
171
|
+
| `inspect(data) -> dict` | Get metadata without deserializing |
|
|
172
|
+
| `benchmark(obj, rounds=100) -> dict` | Measure encode/decode performance |
|
|
173
|
+
| `copy(obj) -> obj` | Deep copy via serialization round-trip |
|
|
174
|
+
|
|
175
|
+
## Supported Types
|
|
176
|
+
|
|
177
|
+
| Type | Tag | Notes |
|
|
178
|
+
|------|-----|-------|
|
|
179
|
+
| `None`, `bool`, `int`, `float`, `complex` | Built-in | Arbitrary precision ints |
|
|
180
|
+
| `str`, `bytes`, `bytearray` | Built-in | UTF-8, ref-tracked |
|
|
181
|
+
| `list`, `tuple`, `dict`, `set`, `frozenset` | Built-in | Circular refs supported |
|
|
182
|
+
| `datetime`, `date`, `time`, `timedelta` | Built-in | ISO 8601 |
|
|
183
|
+
| `Decimal`, `UUID`, `Path` | Built-in | Lossless |
|
|
184
|
+
| `Enum` | Registry | Must be registered |
|
|
185
|
+
| `numpy.ndarray` | Built-in | dtype whitelist enforced |
|
|
186
|
+
| `torch.Tensor` | Built-in | Auto CPU transfer, preserves requires_grad |
|
|
187
|
+
| `dataclass`, `__slots__`, `__dict__` | Registry | Must be registered |
|
|
188
|
+
|
|
189
|
+
## Performance
|
|
190
|
+
|
|
191
|
+
Benchmarked on a state_dict with 4 tensors (~57K parameters):
|
|
192
|
+
|
|
193
|
+
| Method | Roundtrip | Size | Safe |
|
|
194
|
+
|--------|-----------|------|------|
|
|
195
|
+
| **mscs** | **0.098 ms** | **65 KB** | **Yes** |
|
|
196
|
+
| pickle | 0.580 ms | 68 KB | No (RCE) |
|
|
197
|
+
| torch.save | 0.437 ms | 67 KB | No (RCE) |
|
|
198
|
+
|
|
199
|
+
**5.9x faster than pickle, 4.1x faster than torch.save** — while being the only option that doesn't allow arbitrary code execution.
|
|
200
|
+
|
|
201
|
+
Tensor scaling (encode+decode):
|
|
202
|
+
|
|
203
|
+
| Shape | mscs | pickle | Speedup |
|
|
204
|
+
|-------|------|--------|---------|
|
|
205
|
+
| 10x10 | 0.019 ms | 0.156 ms | 8.2x |
|
|
206
|
+
| 256x256 | 0.069 ms | 0.278 ms | 4.0x |
|
|
207
|
+
| 1024x1024 | 4.7 ms | 5.0 ms | 1.1x |
|
|
208
|
+
|
|
209
|
+
## Security Model
|
|
210
|
+
|
|
211
|
+
1. **No code execution**: Deserialization only reconstructs data, never runs arbitrary code
|
|
212
|
+
2. **Explicit registry**: Custom classes must be registered before deserialization
|
|
213
|
+
3. **No dynamic imports**: Class names in the binary stream are only used as registry keys
|
|
214
|
+
4. **NumPy dtype whitelist**: Blocks `object`, `void`, and structured dtypes
|
|
215
|
+
5. **Configurable limits**: `MAX_DEPTH=256`, `MAX_SIZE=512MB`, `MAX_COLLECTION=10M`
|
|
216
|
+
6. **Anti zip-bomb**: `load_compressed` validates both compressed and decompressed sizes
|
|
217
|
+
7. **CRC32 integrity**: Optional checksum to detect corruption
|
|
218
|
+
8. **Auditable format**: Magic bytes (`MSCS`) + version byte + type tags
|
|
219
|
+
|
|
220
|
+
## Binary Format
|
|
221
|
+
|
|
222
|
+
```
|
|
223
|
+
[MSCS][version:1][flags:1][type_tag:1][...payload...]
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
- Magic: `MSCS` (4 bytes)
|
|
227
|
+
- Version: `\x02` (1 byte)
|
|
228
|
+
- Flags: bit 0 = CRC32 appended (1 byte)
|
|
229
|
+
- Payload: recursive type-tagged binary data
|
|
230
|
+
- Optional CRC32 trailer (4 bytes)
|
|
231
|
+
|
|
232
|
+
## License
|
|
233
|
+
|
|
234
|
+
MIT
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
mscs/__init__.py,sha256=X6eNeE47moHbJpHGuQ-PycJcpdFdcnijCVbogTwZ0Sc,1003
|
|
2
|
+
mscs/_core.py,sha256=blerUQ1qYLW-0LxP0Va-P-z6fXh_85mU4Kk3aCmp0Gc,35301
|
|
3
|
+
mscs/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
mscs-2.2.0.dist-info/METADATA,sha256=Y86KzbN95zSBT2ev2tmMaeFh_D3CG6UTXv6saCduXjE,7756
|
|
5
|
+
mscs-2.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
6
|
+
mscs-2.2.0.dist-info/licenses/LICENSE,sha256=5JE5UgNWDjlc72iGP35F3hwU6b787nIO02slbWRjsfw,1066
|
|
7
|
+
mscs-2.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Esraderey
|
|
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.
|