mlbundle 0.1.0a1__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.
- mlbundle/__init__.py +82 -0
- mlbundle/_base.py +19 -0
- mlbundle/_canonical_work.py +67 -0
- mlbundle/_catboost/__init__.py +159 -0
- mlbundle/_catboost/budget.py +22 -0
- mlbundle/_catboost/flatbuffer.py +104 -0
- mlbundle/_catboost/native.py +244 -0
- mlbundle/_catboost/prediction.py +67 -0
- mlbundle/_catboost/scope.py +109 -0
- mlbundle/_catboost/state.py +62 -0
- mlbundle/_composite/__init__.py +1 -0
- mlbundle/_composite/claims.py +133 -0
- mlbundle/_composite/io.py +158 -0
- mlbundle/_composite/prediction.py +77 -0
- mlbundle/_container_rules.py +73 -0
- mlbundle/_export_manifest.py +78 -0
- mlbundle/_native_io.py +53 -0
- mlbundle/_onnx/__init__.py +115 -0
- mlbundle/_onnx/_wire_runs.py +103 -0
- mlbundle/_onnx/description.py +252 -0
- mlbundle/_onnx/prediction.py +136 -0
- mlbundle/_onnx/protobuf.py +111 -0
- mlbundle/_onnx/session.py +87 -0
- mlbundle/_onnx/signatures.py +63 -0
- mlbundle/_onnx/validation.py +164 -0
- mlbundle/_pipeline_contract.py +104 -0
- mlbundle/_prediction_input.py +63 -0
- mlbundle/_schema_fields.py +68 -0
- mlbundle/_sklearn/__init__.py +183 -0
- mlbundle/_sklearn/_width_checks.py +59 -0
- mlbundle/_sklearn/archive.py +32 -0
- mlbundle/_sklearn/arrays.py +150 -0
- mlbundle/_sklearn/column_width.py +198 -0
- mlbundle/_sklearn/consistency.py +127 -0
- mlbundle/_sklearn/dtypes.py +41 -0
- mlbundle/_sklearn/encoder_width.py +123 -0
- mlbundle/_sklearn/fitted.py +113 -0
- mlbundle/_sklearn/labels.py +52 -0
- mlbundle/_sklearn/nodes.py +75 -0
- mlbundle/_sklearn/parameters.py +94 -0
- mlbundle/_sklearn/payload.py +234 -0
- mlbundle/_sklearn/pipeline_scope.py +141 -0
- mlbundle/_sklearn/polynomial_names.py +31 -0
- mlbundle/_sklearn/prediction.py +119 -0
- mlbundle/_sklearn/prediction_state.py +61 -0
- mlbundle/_sklearn/references.py +110 -0
- mlbundle/_sklearn/serialization.py +80 -0
- mlbundle/_sklearn/state.py +144 -0
- mlbundle/_sklearn/state_fields.json +228 -0
- mlbundle/_sklearn/structure.py +265 -0
- mlbundle/_sklearn/transform_names.py +111 -0
- mlbundle/_sklearn/transform_width.py +270 -0
- mlbundle/_sklearn/transformers.py +69 -0
- mlbundle/_sklearn/trusted.py +89 -0
- mlbundle/_xgboost/__init__.py +262 -0
- mlbundle/_xgboost/arrays.py +47 -0
- mlbundle/_xgboost/categories.py +109 -0
- mlbundle/_xgboost/native.py +193 -0
- mlbundle/_xgboost/prediction.py +59 -0
- mlbundle/_xgboost/snapshots.py +41 -0
- mlbundle/_xgboost/state.py +147 -0
- mlbundle/_xgboost/structure.py +47 -0
- mlbundle/_xgboost/ubjson.py +119 -0
- mlbundle/_zip_directory.py +173 -0
- mlbundle/_zip_members.py +116 -0
- mlbundle/adapters.py +33 -0
- mlbundle/canonical.py +290 -0
- mlbundle/claims.py +124 -0
- mlbundle/cli.py +193 -0
- mlbundle/container.py +93 -0
- mlbundle/contract.py +55 -0
- mlbundle/declarations.py +68 -0
- mlbundle/environment.py +85 -0
- mlbundle/errors.py +92 -0
- mlbundle/inspection.py +30 -0
- mlbundle/integrity.py +30 -0
- mlbundle/limits.py +38 -0
- mlbundle/manifest.py +222 -0
- mlbundle/model_io.py +151 -0
- mlbundle/py.typed +0 -0
- mlbundle/registry.py +103 -0
- mlbundle/results.py +68 -0
- mlbundle/samples.py +71 -0
- mlbundle/schema.py +145 -0
- mlbundle/training.py +104 -0
- mlbundle/writing.py +256 -0
- mlbundle-0.1.0a1.dist-info/METADATA +347 -0
- mlbundle-0.1.0a1.dist-info/RECORD +91 -0
- mlbundle-0.1.0a1.dist-info/WHEEL +4 -0
- mlbundle-0.1.0a1.dist-info/entry_points.txt +3 -0
- mlbundle-0.1.0a1.dist-info/licenses/LICENSE +21 -0
mlbundle/__init__.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Pickle-free model bundles."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import version as _version
|
|
4
|
+
|
|
5
|
+
from .claims import ModelClaims
|
|
6
|
+
from .contract import (
|
|
7
|
+
FORMAT_NAME,
|
|
8
|
+
FORMAT_VERSION,
|
|
9
|
+
FeatureOrigin,
|
|
10
|
+
Library,
|
|
11
|
+
MemberName,
|
|
12
|
+
ModelKind,
|
|
13
|
+
SerializationFormat,
|
|
14
|
+
)
|
|
15
|
+
from .environment import OnnxInfo, Opset, TrainingEnvironment
|
|
16
|
+
from .errors import (
|
|
17
|
+
AdapterNotImplementedError,
|
|
18
|
+
ClaimMismatchError,
|
|
19
|
+
IntegrityMismatchError,
|
|
20
|
+
InvalidContainerError,
|
|
21
|
+
InvalidJSONError,
|
|
22
|
+
InvalidManifestError,
|
|
23
|
+
InvalidModelError,
|
|
24
|
+
InvalidSamplesError,
|
|
25
|
+
InvalidSchemaError,
|
|
26
|
+
LimitExceededError,
|
|
27
|
+
MissingOptionalDependencyError,
|
|
28
|
+
MLBundleError,
|
|
29
|
+
PredictionInputError,
|
|
30
|
+
UnsupportedFormatVersionError,
|
|
31
|
+
UnsupportedModelError,
|
|
32
|
+
)
|
|
33
|
+
from .inspection import inspect
|
|
34
|
+
from .integrity import IntegrityRecord
|
|
35
|
+
from .limits import ArchiveLimits
|
|
36
|
+
from .manifest import FeatureInfo, Manifest, MemberReferences, ModelInfo, TargetInfo
|
|
37
|
+
from .model_io import export, load
|
|
38
|
+
from .results import LoadedBundle, MetadataView
|
|
39
|
+
|
|
40
|
+
__version__ = _version("mlbundle")
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"FORMAT_NAME",
|
|
44
|
+
"FORMAT_VERSION",
|
|
45
|
+
"AdapterNotImplementedError",
|
|
46
|
+
"ArchiveLimits",
|
|
47
|
+
"ClaimMismatchError",
|
|
48
|
+
"FeatureInfo",
|
|
49
|
+
"FeatureOrigin",
|
|
50
|
+
"IntegrityMismatchError",
|
|
51
|
+
"IntegrityRecord",
|
|
52
|
+
"InvalidContainerError",
|
|
53
|
+
"InvalidJSONError",
|
|
54
|
+
"InvalidManifestError",
|
|
55
|
+
"InvalidModelError",
|
|
56
|
+
"InvalidSamplesError",
|
|
57
|
+
"InvalidSchemaError",
|
|
58
|
+
"Library",
|
|
59
|
+
"LimitExceededError",
|
|
60
|
+
"LoadedBundle",
|
|
61
|
+
"MLBundleError",
|
|
62
|
+
"Manifest",
|
|
63
|
+
"MemberName",
|
|
64
|
+
"MemberReferences",
|
|
65
|
+
"MetadataView",
|
|
66
|
+
"MissingOptionalDependencyError",
|
|
67
|
+
"ModelClaims",
|
|
68
|
+
"ModelInfo",
|
|
69
|
+
"ModelKind",
|
|
70
|
+
"OnnxInfo",
|
|
71
|
+
"Opset",
|
|
72
|
+
"PredictionInputError",
|
|
73
|
+
"SerializationFormat",
|
|
74
|
+
"TargetInfo",
|
|
75
|
+
"TrainingEnvironment",
|
|
76
|
+
"UnsupportedFormatVersionError",
|
|
77
|
+
"UnsupportedModelError",
|
|
78
|
+
"__version__",
|
|
79
|
+
"export",
|
|
80
|
+
"inspect",
|
|
81
|
+
"load",
|
|
82
|
+
]
|
mlbundle/_base.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Shared strict model configuration and bounded field types."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, StringConstraints
|
|
6
|
+
|
|
7
|
+
ShortString = Annotated[str, StringConstraints(min_length=1, max_length=256)]
|
|
8
|
+
Name = Annotated[str, StringConstraints(min_length=1, max_length=1024)]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ContractModel(BaseModel):
|
|
12
|
+
model_config = ConfigDict(
|
|
13
|
+
strict=True,
|
|
14
|
+
extra="forbid",
|
|
15
|
+
frozen=True,
|
|
16
|
+
validate_default=True,
|
|
17
|
+
hide_input_in_errors=True,
|
|
18
|
+
allow_inf_nan=False,
|
|
19
|
+
)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Linear preflight for the expanded size/depth of native shared JSON containers."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .errors import LimitExceededError
|
|
6
|
+
|
|
7
|
+
_SCALARS = (str, int, float, bool, type(None))
|
|
8
|
+
_NUMBERS = {int, float, bool, type(None)}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _sequence_cost(item):
|
|
12
|
+
# C map/set classify flat numeric arrays without Python calls per element.
|
|
13
|
+
if len(item) >= 64 and set(map(type, item)) <= _NUMBERS:
|
|
14
|
+
return 1 + 2 * len(item), ()
|
|
15
|
+
return 1, item
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _child_cost(child: Any, measure) -> tuple[int, int]:
|
|
19
|
+
if type(child) in _SCALARS:
|
|
20
|
+
return (len(child) + 2 if type(child) is str else 1), 0
|
|
21
|
+
return measure(child)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _array_cost(value: Any) -> tuple[int, int, Any]:
|
|
25
|
+
return value.ndim, value.size, value.flat if value.dtype.kind == "O" else ()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def bound_work(value: Any, max_bytes: int, max_depth: int) -> None:
|
|
29
|
+
memo: dict[int, tuple[int, int]] = {}
|
|
30
|
+
active: set[int] = set()
|
|
31
|
+
|
|
32
|
+
def measure(item):
|
|
33
|
+
array = (
|
|
34
|
+
type(item).__module__.split(".")[0] == "numpy"
|
|
35
|
+
and type(item).__name__ == "ndarray"
|
|
36
|
+
)
|
|
37
|
+
if not isinstance(item, (dict, list, tuple)) and not array:
|
|
38
|
+
return (len(item) + 2 if isinstance(item, str) else 1), 0
|
|
39
|
+
identity = id(item)
|
|
40
|
+
if identity in active:
|
|
41
|
+
raise LimitExceededError("Cyclic JSON containers exceed the nesting limit.")
|
|
42
|
+
if identity in memo:
|
|
43
|
+
return memo[identity]
|
|
44
|
+
active.add(identity)
|
|
45
|
+
depth, size = 1, 1
|
|
46
|
+
if array:
|
|
47
|
+
depth, size, children = _array_cost(item)
|
|
48
|
+
elif isinstance(item, dict):
|
|
49
|
+
size += sum(len(key) + 3 for key in item if isinstance(key, str))
|
|
50
|
+
children = item.values()
|
|
51
|
+
else:
|
|
52
|
+
size, children = _sequence_cost(item)
|
|
53
|
+
if size > max_bytes or depth > max_depth:
|
|
54
|
+
raise LimitExceededError("Expanded JSON size or depth exceeds the limit.")
|
|
55
|
+
for child in children:
|
|
56
|
+
child_size, child_depth = _child_cost(child, measure)
|
|
57
|
+
size += child_size + 1
|
|
58
|
+
depth = max(depth, child_depth + 1)
|
|
59
|
+
if size > max_bytes or depth > max_depth:
|
|
60
|
+
raise LimitExceededError(
|
|
61
|
+
"Expanded JSON size or depth exceeds the limit."
|
|
62
|
+
)
|
|
63
|
+
active.remove(identity)
|
|
64
|
+
memo[identity] = size, depth
|
|
65
|
+
return size, depth
|
|
66
|
+
|
|
67
|
+
measure(value)
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Native CBM adapter; exact CatBoost wrappers and no reader-side file I/O."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from tempfile import TemporaryDirectory
|
|
5
|
+
from typing import Any, cast
|
|
6
|
+
|
|
7
|
+
import catboost as cb
|
|
8
|
+
from pydantic import JsonValue
|
|
9
|
+
|
|
10
|
+
from mlbundle.canonical import canonical_value
|
|
11
|
+
from mlbundle.claims import ClassLabels, ModelClaims
|
|
12
|
+
from mlbundle.contract import Library, MemberName, ModelKind
|
|
13
|
+
from mlbundle.declarations import ExportDeclarations
|
|
14
|
+
from mlbundle.errors import InvalidModelError, MLBundleError, UnsupportedModelError
|
|
15
|
+
|
|
16
|
+
from .native import decode
|
|
17
|
+
from .prediction import Predictor
|
|
18
|
+
from .scope import CLASSIFIERS, validate
|
|
19
|
+
from .state import commitment, feature_importances, json_value, snapshot
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Adapter:
|
|
23
|
+
library = Library.CATBOOST
|
|
24
|
+
model_member = MemberName.CBM
|
|
25
|
+
|
|
26
|
+
def recognizes(self, model: object) -> bool:
|
|
27
|
+
return isinstance(model, cb.CatBoost)
|
|
28
|
+
|
|
29
|
+
def prepare_export(self, model: Any, declarations: ExportDeclarations) -> object:
|
|
30
|
+
try:
|
|
31
|
+
validate(model)
|
|
32
|
+
if declarations.class_labels is not None:
|
|
33
|
+
raise UnsupportedModelError(
|
|
34
|
+
reason="CatBoost already carries class labels; class_labels is unsupported."
|
|
35
|
+
)
|
|
36
|
+
self.extract_claims(model)
|
|
37
|
+
return model
|
|
38
|
+
except MLBundleError:
|
|
39
|
+
raise
|
|
40
|
+
except Exception:
|
|
41
|
+
raise UnsupportedModelError(
|
|
42
|
+
reason="CatBoost export preparation failed."
|
|
43
|
+
) from None
|
|
44
|
+
|
|
45
|
+
def serialize(self, model: Any) -> bytes:
|
|
46
|
+
try:
|
|
47
|
+
validate(model)
|
|
48
|
+
# Public save_model accepts paths only; export owns this temporary file.
|
|
49
|
+
with TemporaryDirectory(prefix="mlbundle-catboost-") as directory:
|
|
50
|
+
path = Path(directory) / "model.cbm"
|
|
51
|
+
model.save_model(str(path), format="cbm")
|
|
52
|
+
data = path.read_bytes()
|
|
53
|
+
decode(data)
|
|
54
|
+
return data
|
|
55
|
+
except MLBundleError:
|
|
56
|
+
raise
|
|
57
|
+
except Exception:
|
|
58
|
+
raise UnsupportedModelError(
|
|
59
|
+
reason="CatBoost CBM serialization failed."
|
|
60
|
+
) from None
|
|
61
|
+
|
|
62
|
+
def deserialize(self, data: bytes) -> object:
|
|
63
|
+
try:
|
|
64
|
+
state = decode(data)
|
|
65
|
+
cls = (
|
|
66
|
+
cb.CatBoostClassifier
|
|
67
|
+
if state.loss in CLASSIFIERS
|
|
68
|
+
else cb.CatBoostRegressor
|
|
69
|
+
)
|
|
70
|
+
# blob loading does not restore _init_params. Poisson's default predict
|
|
71
|
+
# selects Exponent from that wrapper field, so bind the validated loss.
|
|
72
|
+
model: Any = cls(loss_function=state.loss)
|
|
73
|
+
model.load_model(blob=data)
|
|
74
|
+
model._n_features_in = state.width
|
|
75
|
+
actual = snapshot(model)
|
|
76
|
+
if commitment(state) != commitment(actual):
|
|
77
|
+
raise InvalidModelError(
|
|
78
|
+
"CatBoost state does not survive native construction."
|
|
79
|
+
)
|
|
80
|
+
validate(model)
|
|
81
|
+
return model
|
|
82
|
+
except MLBundleError:
|
|
83
|
+
raise
|
|
84
|
+
except Exception:
|
|
85
|
+
raise InvalidModelError("Invalid CatBoost CBM payload.") from None
|
|
86
|
+
|
|
87
|
+
def extract_claims(self, model: Any) -> ModelClaims:
|
|
88
|
+
try:
|
|
89
|
+
validate(model)
|
|
90
|
+
state = snapshot(model)
|
|
91
|
+
classifier = type(model) is cb.CatBoostClassifier
|
|
92
|
+
if (
|
|
93
|
+
classifier != (state.loss in CLASSIFIERS)
|
|
94
|
+
or model.n_features_in_ not in (0, state.width)
|
|
95
|
+
or model.tree_count_ != len(state.native["tree_depths"])
|
|
96
|
+
):
|
|
97
|
+
raise InvalidModelError(
|
|
98
|
+
"CatBoost wrapper and native dimensions or kind disagree."
|
|
99
|
+
)
|
|
100
|
+
labels = (
|
|
101
|
+
cast(ClassLabels, canonical_value([model.classes_]))
|
|
102
|
+
if classifier
|
|
103
|
+
else None
|
|
104
|
+
)
|
|
105
|
+
if classifier and (
|
|
106
|
+
labels is None
|
|
107
|
+
or len(labels[0])
|
|
108
|
+
!= (state.dimension if state.loss == "MultiClass" else 2)
|
|
109
|
+
):
|
|
110
|
+
raise InvalidModelError(
|
|
111
|
+
"CatBoost classes and native dimensions disagree."
|
|
112
|
+
)
|
|
113
|
+
loss = model.get_params().get("loss_function")
|
|
114
|
+
if loss is not None and loss.split(":", 1)[0] != state.loss:
|
|
115
|
+
raise InvalidModelError("CatBoost wrapper and native loss disagree.")
|
|
116
|
+
return ModelClaims(
|
|
117
|
+
estimator_class=type(model).__name__,
|
|
118
|
+
kind=ModelKind.CLASSIFIER if classifier else ModelKind.REGRESSOR,
|
|
119
|
+
hyperparameters={
|
|
120
|
+
"parameters": canonical_value(model.get_all_params()),
|
|
121
|
+
"tree_count": model.tree_count_,
|
|
122
|
+
"best_iteration": model.get_best_iteration(),
|
|
123
|
+
"scale_and_bias": canonical_value(model.get_scale_and_bias()),
|
|
124
|
+
"probability_threshold": model.get_probability_threshold()
|
|
125
|
+
if classifier
|
|
126
|
+
else None,
|
|
127
|
+
},
|
|
128
|
+
state_sha256=commitment(state),
|
|
129
|
+
n_features=state.width,
|
|
130
|
+
n_classes=None if labels is None else [len(labels[0])],
|
|
131
|
+
n_outputs=1,
|
|
132
|
+
feature_names=model.feature_names_,
|
|
133
|
+
feature_dtypes=None,
|
|
134
|
+
class_labels=labels,
|
|
135
|
+
)
|
|
136
|
+
except MLBundleError:
|
|
137
|
+
raise
|
|
138
|
+
except Exception:
|
|
139
|
+
raise InvalidModelError("Invalid CatBoost model claims.") from None
|
|
140
|
+
|
|
141
|
+
def extract_structure(self, model: Any) -> dict[str, JsonValue]:
|
|
142
|
+
try:
|
|
143
|
+
validate(model)
|
|
144
|
+
state = snapshot(model)
|
|
145
|
+
return cast(
|
|
146
|
+
dict[str, JsonValue],
|
|
147
|
+
{
|
|
148
|
+
"family": "catboost",
|
|
149
|
+
"native_model": json_value(state.native),
|
|
150
|
+
"feature_importances": feature_importances(model, state.width),
|
|
151
|
+
},
|
|
152
|
+
)
|
|
153
|
+
except MLBundleError:
|
|
154
|
+
raise
|
|
155
|
+
except Exception:
|
|
156
|
+
raise InvalidModelError("Invalid CatBoost model structure.") from None
|
|
157
|
+
|
|
158
|
+
def predictor(self, model: object) -> Predictor:
|
|
159
|
+
return Predictor(model, self.extract_claims(model))
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Bound structure expansion before native parsing or allocating Python lists."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from mlbundle.errors import LimitExceededError
|
|
6
|
+
from mlbundle.limits import STRUCTURE_BYTES
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def structure_budget(native: dict[str, Any]) -> None:
|
|
10
|
+
# Worst finite binary64 JSON token plus separator is 25 bytes. The bound
|
|
11
|
+
# intentionally rejects some compact-value models before list allocation.
|
|
12
|
+
size = 4096
|
|
13
|
+
for value in native.values():
|
|
14
|
+
if hasattr(value, "dtype"):
|
|
15
|
+
size += value.size * (25 if value.dtype.kind == "f" else 12)
|
|
16
|
+
for feature in native["float_features"]:
|
|
17
|
+
size += 256 + 6 * len(feature["name"].encode("utf-8"))
|
|
18
|
+
size += feature["borders"].size * 25
|
|
19
|
+
if size > STRUCTURE_BYTES:
|
|
20
|
+
raise LimitExceededError(
|
|
21
|
+
"CatBoost structure allocation budget exceeds the limit."
|
|
22
|
+
)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Checked views of the closed CatBoost 1.2.10 CBM FlatBuffer schema."""
|
|
2
|
+
|
|
3
|
+
import struct
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from mlbundle.errors import InvalidModelError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Buffer:
|
|
12
|
+
def __init__(self, data: memoryview) -> None:
|
|
13
|
+
self.data = data
|
|
14
|
+
self.work = 0
|
|
15
|
+
|
|
16
|
+
def span(self, position: int, size: int) -> memoryview:
|
|
17
|
+
if position < 0 or size < 0 or size > len(self.data) - position:
|
|
18
|
+
raise InvalidModelError("Truncated CatBoost CBM buffer.")
|
|
19
|
+
return self.data[position : position + size]
|
|
20
|
+
|
|
21
|
+
def number(self, position: int, fmt: str) -> Any:
|
|
22
|
+
return struct.unpack("<" + fmt, self.span(position, struct.calcsize(fmt)))[0]
|
|
23
|
+
|
|
24
|
+
def indirect(self, position: int) -> int:
|
|
25
|
+
offset = self.number(position, "I")
|
|
26
|
+
if offset < 4:
|
|
27
|
+
raise InvalidModelError("Invalid CatBoost CBM offset.")
|
|
28
|
+
target = position + offset
|
|
29
|
+
self.span(target, 4)
|
|
30
|
+
return target
|
|
31
|
+
|
|
32
|
+
def table(self, position: int, fields: int) -> "Table":
|
|
33
|
+
self.work += 1
|
|
34
|
+
if self.work > 200_000:
|
|
35
|
+
raise InvalidModelError("CatBoost CBM table work exceeds the limit.")
|
|
36
|
+
return Table(self, position, fields)
|
|
37
|
+
|
|
38
|
+
def string(self, position: int, cap: int) -> str:
|
|
39
|
+
start = self.indirect(position)
|
|
40
|
+
size = self.number(start, "I")
|
|
41
|
+
if size > cap or self.number(start + 4 + size, "B") != 0:
|
|
42
|
+
raise InvalidModelError("Invalid CatBoost CBM string length.")
|
|
43
|
+
return bytes(self.span(start + 4, size)).decode("utf-8")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Table:
|
|
47
|
+
def __init__(self, buffer: Buffer, position: int, fields: int) -> None:
|
|
48
|
+
self.buffer, self.position = buffer, position
|
|
49
|
+
vtable = position - buffer.number(position, "i")
|
|
50
|
+
size = buffer.number(vtable, "H")
|
|
51
|
+
self.size = buffer.number(vtable + 2, "H")
|
|
52
|
+
if size < 4 or size % 2 or size > 4 + 2 * fields or self.size < 4:
|
|
53
|
+
raise InvalidModelError("Unknown or invalid CatBoost CBM table fields.")
|
|
54
|
+
buffer.span(position, self.size)
|
|
55
|
+
buffer.span(vtable, size)
|
|
56
|
+
self.offsets = [buffer.number(vtable + i, "H") for i in range(4, size, 2)]
|
|
57
|
+
if any(offset and not 4 <= offset < self.size for offset in self.offsets):
|
|
58
|
+
raise InvalidModelError("Invalid CatBoost CBM field offset.")
|
|
59
|
+
|
|
60
|
+
def field(self, index: int, size: int = 4) -> int | None:
|
|
61
|
+
offset = self.offsets[index] if index < len(self.offsets) else 0
|
|
62
|
+
if not offset:
|
|
63
|
+
return None
|
|
64
|
+
if size > self.size - offset:
|
|
65
|
+
raise InvalidModelError("Invalid CatBoost CBM field size.")
|
|
66
|
+
return self.position + offset
|
|
67
|
+
|
|
68
|
+
def scalar(self, index: int, fmt: str, default: Any = 0) -> Any:
|
|
69
|
+
position = self.field(index, struct.calcsize(fmt))
|
|
70
|
+
return default if position is None else self.buffer.number(position, fmt)
|
|
71
|
+
|
|
72
|
+
def string(self, index: int, cap: int) -> str:
|
|
73
|
+
position = self.field(index)
|
|
74
|
+
return "" if position is None else self.buffer.string(position, cap)
|
|
75
|
+
|
|
76
|
+
def child(self, index: int, fields: int) -> "Table":
|
|
77
|
+
position = self.field(index)
|
|
78
|
+
if position is None:
|
|
79
|
+
raise InvalidModelError("Missing CatBoost CBM table.")
|
|
80
|
+
return self.buffer.table(self.buffer.indirect(position), fields)
|
|
81
|
+
|
|
82
|
+
def vector(self, index: int, dtype: str, cap: int) -> np.ndarray:
|
|
83
|
+
position = self.field(index)
|
|
84
|
+
kind = np.dtype(dtype)
|
|
85
|
+
if position is None:
|
|
86
|
+
return np.empty(0, dtype=kind)
|
|
87
|
+
start = self.buffer.indirect(position)
|
|
88
|
+
count = self.buffer.number(start, "I")
|
|
89
|
+
if count > cap:
|
|
90
|
+
raise InvalidModelError("CatBoost CBM vector count exceeds the limit.")
|
|
91
|
+
return np.frombuffer(self.buffer.span(start + 4, count * kind.itemsize), kind)
|
|
92
|
+
|
|
93
|
+
def tables(self, index: int, fields: int, cap: int) -> list["Table"]:
|
|
94
|
+
offsets = self.vector(index, "<u4", cap)
|
|
95
|
+
if not len(offsets):
|
|
96
|
+
return []
|
|
97
|
+
position = self.field(index)
|
|
98
|
+
if position is None:
|
|
99
|
+
raise InvalidModelError("Missing CatBoost CBM vector.")
|
|
100
|
+
start = self.buffer.indirect(position) + 4
|
|
101
|
+
return [
|
|
102
|
+
self.buffer.table(self.buffer.indirect(start + 4 * i), fields)
|
|
103
|
+
for i in range(len(offsets))
|
|
104
|
+
]
|