Skip to content

Commit 917cd7e

Browse files
fulpmhowso-automation
andauthored
24750: Raise HowsoError when Trainee fails to write to file (#543)
Co-authored-by: howso-automation <[email protected]>
1 parent cb88782 commit 917cd7e

17 files changed

+306
-76
lines changed

LICENSE-3RD-PARTY.txt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
Faker
2-
37.12.0
2+
38.2.0
33
MIT License
44
joke2k
55
https://github.com/joke2k/faker
66
Faker is a Python package that generates fake data for you.
7-
/home/runner/.pyenv/versions/3.13.9/lib/python3.13/site-packages/faker-37.12.0.dist-info/licenses/LICENSE.txt
7+
/home/runner/.pyenv/versions/3.13.9/lib/python3.13/site-packages/faker-38.2.0.dist-info/licenses/LICENSE.txt
88
Copyright (c) 2012 Daniele Faraglia
99

1010
Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -149,12 +149,12 @@ DEALINGS IN THE SOFTWARE.
149149

150150

151151
certifi
152-
2025.10.5
152+
2025.11.12
153153
Mozilla Public License 2.0 (MPL 2.0)
154154
Kenneth Reitz
155155
https://github.com/certifi/python-certifi
156156
Python package for providing Mozilla's CA Bundle.
157-
/home/runner/.pyenv/versions/3.13.9/lib/python3.13/site-packages/certifi-2025.10.5.dist-info/licenses/LICENSE
157+
/home/runner/.pyenv/versions/3.13.9/lib/python3.13/site-packages/certifi-2025.11.12.dist-info/licenses/LICENSE
158158
This package contains a modified version of ca-bundle.crt:
159159

160160
ca-bundle.crt -- Bundle of CA Root Certificates
@@ -240,12 +240,12 @@ SOFTWARE.
240240

241241

242242
click
243-
8.3.0
243+
8.3.1
244244
BSD-3-Clause
245245
UNKNOWN
246246
https://github.com/pallets/click/
247247
Composable command line interface toolkit
248-
/home/runner/.pyenv/versions/3.13.9/lib/python3.13/site-packages/click-8.3.0.dist-info/licenses/LICENSE.txt
248+
/home/runner/.pyenv/versions/3.13.9/lib/python3.13/site-packages/click-8.3.1.dist-info/licenses/LICENSE.txt
249249
Copyright 2014 Pallets
250250

251251
Redistribution and use in source and binary forms, with or without

howso/direct/client.py

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -457,12 +457,15 @@ def _auto_persist_trainee(self, trainee_id: str):
457457
if new_file_size <= trainee.file_size * 2 and new_file_size <= trainee.file_size + 10485760:
458458
return
459459

460-
self.amlg.store_entity(
460+
resolved_path = self.resolve_trainee_filepath(trainee_id)
461+
is_persisted = self.amlg.store_entity(
461462
handle=trainee_id,
462-
file_path=self.resolve_trainee_filepath(trainee_id),
463+
file_path=resolved_path,
463464
persist=True,
464465
json_file_params='{"transactional":true,"flatten":true}'
465466
)
467+
if not is_persisted:
468+
warnings.warn(f'Failed to auto persist Trainee "{trainee_id}" to file path: {resolved_path}', UserWarning)
466469
trainee.file_size = self._trainee_size(trainee_id)
467470

468471
def _store_session(self, trainee_id: str, session: Session):
@@ -1084,18 +1087,29 @@ def update_trainee(self, trainee: Mapping | Trainee) -> Trainee:
10841087

10851088
if old_trainee.persistence == 'always' and instance.persistence != 'always':
10861089
# Manually persist the trainee now, turning off transactional mode.
1087-
self.amlg.store_entity(
1088-
handle=instance.id,
1089-
file_path=self.resolve_trainee_filepath(instance.id)
1090-
)
1090+
resolved_path = self.resolve_trainee_filepath(instance.id)
1091+
is_persisted = self.amlg.store_entity(handle=instance.id, file_path=resolved_path)
1092+
if not is_persisted:
1093+
raise HowsoError(
1094+
f'Failed to update persistence of Trainee "{instance.id}", '
1095+
f"could not write Trainee to file path: {resolved_path}",
1096+
code="persist_failed",
1097+
)
10911098
elif instance.persistence == 'always' and old_trainee.persistence != 'always':
10921099
# Manually persist the trainee, turning on transactional mode.
1093-
self.amlg.store_entity(
1100+
resolved_path = self.resolve_trainee_filepath(instance.id)
1101+
is_persisted = self.amlg.store_entity(
10941102
handle=instance.id,
1095-
file_path=self.resolve_trainee_filepath(instance.id),
1103+
file_path=resolved_path,
10961104
persist=True,
1097-
json_file_params='{"transactional":true,"flatten":true}'
1105+
json_file_params='{"transactional":true,"flatten":true}',
10981106
)
1107+
if not is_persisted:
1108+
raise HowsoError(
1109+
f'Failed to update persistence of Trainee "{instance.id}", '
1110+
f"could not write Trainee to file path: {resolved_path}",
1111+
code="persist_failed",
1112+
)
10991113
instance.file_size = self._trainee_size(instance.id)
11001114

11011115
metadata = {
@@ -1577,10 +1591,18 @@ def release_trainee_resources(self, trainee_id: str):
15771591

15781592
if trainee.persistence in ['allow', 'always']:
15791593
# Persist on unload
1580-
self.amlg.store_entity(
1594+
resolved_path = self.resolve_trainee_filepath(trainee_id)
1595+
is_persisted = self.amlg.store_entity(
15811596
handle=trainee_id,
1582-
file_path=self.resolve_trainee_filepath(trainee_id)
1597+
file_path=resolved_path,
15831598
)
1599+
if not is_persisted:
1600+
# If persist fails, do not proceed to delete the trainee from memory
1601+
raise HowsoError(
1602+
f'Failed to release resources for Trainee "{trainee_id}", '
1603+
f'could not write Trainee to file path: {resolved_path}',
1604+
code="persist_failed",
1605+
)
15841606
elif trainee.persistence == "never":
15851607
raise HowsoError(
15861608
"Trainees set to never persist may not have their "
@@ -1618,12 +1640,17 @@ def persist_trainee(self, trainee_id: str):
16181640
"persistence option to enable persistence.")
16191641
transactional = (trainee is not None and trainee.persistence == 'always')
16201642

1621-
self.amlg.store_entity(
1643+
resolved_path = self.resolve_trainee_filepath(trainee_id)
1644+
is_persisted = self.amlg.store_entity(
16221645
handle=trainee_id,
1623-
file_path=self.resolve_trainee_filepath(trainee_id),
1646+
file_path=resolved_path,
16241647
persist=transactional,
16251648
json_file_params='{"transactional":true,"flatten":true}' if transactional else ""
16261649
)
1650+
if not is_persisted:
1651+
raise HowsoError(
1652+
f'Failed to write Trainee "{trainee_id}" to file path: {resolved_path}', code="persist_failed"
1653+
)
16271654

16281655
if transactional:
16291656
assert trainee is not None

howso/direct/tests/test_standalone.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from pathlib import Path
22

33
import pytest
4+
from pytest_mock import MockFixture
45

56
from amalgam.api import Amalgam
67
from howso.client.exceptions import HowsoError
@@ -118,3 +119,36 @@ def test_load_subtrainee_from_memory(client: HowsoDirectClient, tmp_path: Path)
118119
assert df["cases"] == [[1], [2]]
119120
finally:
120121
client.delete_trainee("test")
122+
123+
124+
def test_persistence_fails(mocker: MockFixture, client: HowsoDirectClient) -> None:
125+
"""Test persist raises when unable to write file."""
126+
features = {"x": {"type": "continuous"}}
127+
t1 = client.create_trainee(features=features)
128+
client.train(t1.id, cases=[[1]], features=["x"])
129+
130+
def mock_store_entity(*args, **kwargs):
131+
return False
132+
133+
mocker.patch.object(client.amlg, "store_entity", side_effect=mock_store_entity)
134+
135+
resolved_path = client.resolve_trainee_filepath(t1.id)
136+
137+
with pytest.raises(HowsoError) as error_info:
138+
client.persist_trainee(t1.id)
139+
assert error_info.value.code == "persist_failed"
140+
assert error_info.value.message == f'Failed to write Trainee "{t1.id}" to file path: {resolved_path}'
141+
142+
with pytest.raises(HowsoError) as error_info:
143+
client.release_trainee_resources(t1.id)
144+
assert error_info.value.code == "persist_failed"
145+
assert error_info.value.message == (
146+
f'Failed to release resources for Trainee "{t1.id}", could not write Trainee to file path: {resolved_path}'
147+
)
148+
149+
assert t1.persistence == "allow"
150+
t1_new = t1.to_dict()
151+
t1_new["persistence"] = "always"
152+
with pytest.raises(HowsoError) as error_info:
153+
client.update_trainee(t1_new)
154+
assert error_info.value.code == "persist_failed"

howso/engine/tests/conftest.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
import pmlb
1+
import pandas as pd
2+
from pathlib import Path
23
import pytest
34

45
from howso.utilities import infer_feature_attributes
56

67

78
@pytest.fixture(scope="module", name="data")
89
def load_data():
9-
return pmlb.fetch_data("iris")
10+
filename = Path(Path(__file__).parent, "data/iris.csv")
11+
return pd.read_csv(filename)
1012

1113

1214
@pytest.fixture(scope="module", name="features")

howso/engine/tests/data/iris.csv

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
sepal-length,sepal-width,petal-length,petal-width,target
2+
6.7,3.0,5.2,2.3,2
3+
6.0,2.2,5.0,1.5,2
4+
6.2,2.8,4.8,1.8,2
5+
7.7,3.8,6.7,2.2,2
6+
7.2,3.0,5.8,1.6,2
7+
5.5,2.4,3.8,1.1,1
8+
6.0,2.7,5.1,1.6,1
9+
5.5,2.5,4.0,1.3,1
10+
5.6,2.9,3.6,1.3,1
11+
5.7,2.9,4.2,1.3,1
12+
5.0,3.2,1.2,0.2,0
13+
4.9,3.1,1.5,0.1,0
14+
5.3,3.7,1.5,0.2,0
15+
4.8,3.1,1.6,0.2,0
16+
5.0,3.3,1.4,0.2,0
17+
6.3,3.4,5.6,2.4,2
18+
7.1,3.0,5.9,2.1,2
19+
6.3,2.8,5.1,1.5,2
20+
6.3,2.9,5.6,1.8,2
21+
5.8,2.7,5.1,1.9,2
22+
5.2,2.7,3.9,1.4,1
23+
5.6,3.0,4.1,1.3,1
24+
6.9,3.1,4.9,1.5,1
25+
6.2,2.9,4.3,1.3,1
26+
6.5,2.8,4.6,1.5,1
27+
5.0,3.0,1.6,0.2,0
28+
5.5,3.5,1.3,0.2,0
29+
5.1,3.5,1.4,0.3,0
30+
5.7,3.8,1.7,0.3,0
31+
5.5,4.2,1.4,0.2,0
32+
6.7,3.1,5.6,2.4,2
33+
5.8,2.8,5.1,2.4,2
34+
6.4,3.1,5.5,1.8,2
35+
7.9,3.8,6.4,2.0,2
36+
6.8,3.0,5.5,2.1,2
37+
6.0,3.4,4.5,1.6,1
38+
6.7,3.1,4.7,1.5,1
39+
5.7,2.8,4.1,1.3,1
40+
6.7,3.1,4.4,1.4,1
41+
5.9,3.0,4.2,1.5,1
42+
5.1,3.8,1.9,0.4,0
43+
4.9,3.1,1.5,0.1,0
44+
5.4,3.9,1.3,0.4,0
45+
5.1,3.5,1.4,0.2,0
46+
4.8,3.4,1.9,0.2,0
47+
6.3,3.3,6.0,2.5,2
48+
6.7,3.3,5.7,2.5,2
49+
6.3,2.7,4.9,1.8,2
50+
6.9,3.2,5.7,2.3,2
51+
4.9,2.5,4.5,1.7,2
52+
7.0,3.2,4.7,1.4,1
53+
6.6,2.9,4.6,1.3,1
54+
6.4,2.9,4.3,1.3,1
55+
6.3,2.5,4.9,1.5,1
56+
5.7,2.6,3.5,1.0,1
57+
5.4,3.4,1.5,0.4,0
58+
5.0,3.5,1.3,0.3,0
59+
4.5,2.3,1.3,0.3,0
60+
5.1,3.8,1.5,0.3,0
61+
4.4,3.0,1.3,0.2,0
62+
6.9,3.1,5.1,2.3,2
63+
7.3,2.9,6.3,1.8,2
64+
6.1,2.6,5.6,1.4,2
65+
7.4,2.8,6.1,1.9,2
66+
7.2,3.6,6.1,2.5,2
67+
5.9,3.2,4.8,1.8,1
68+
6.1,2.8,4.7,1.2,1
69+
6.3,3.3,4.7,1.6,1
70+
5.8,2.6,4.0,1.2,1
71+
6.0,2.2,4.0,1.0,1
72+
4.6,3.6,1.0,0.2,0
73+
4.7,3.2,1.6,0.2,0
74+
5.1,3.8,1.6,0.2,0
75+
4.4,3.2,1.3,0.2,0
76+
4.8,3.0,1.4,0.3,0
77+
5.6,2.8,4.9,2.0,2
78+
7.6,3.0,6.6,2.1,2
79+
6.5,3.0,5.5,1.8,2
80+
5.9,3.0,5.1,1.8,2
81+
6.3,2.5,5.0,1.9,2
82+
6.1,2.8,4.0,1.3,1
83+
6.6,3.0,4.4,1.4,1
84+
5.8,2.7,4.1,1.0,1
85+
5.0,2.0,3.5,1.0,1
86+
5.5,2.6,4.4,1.2,1
87+
4.6,3.4,1.4,0.3,0
88+
5.4,3.4,1.7,0.2,0
89+
4.7,3.2,1.3,0.2,0
90+
5.2,4.1,1.5,0.1,0
91+
5.0,3.4,1.5,0.2,0
92+
6.5,3.2,5.1,2.0,2
93+
6.8,3.2,5.9,2.3,2
94+
5.7,2.5,5.0,2.0,2
95+
6.4,2.8,5.6,2.2,2
96+
7.7,2.6,6.9,2.3,2
97+
6.3,2.3,4.4,1.3,1
98+
6.1,3.0,4.6,1.4,1
99+
5.8,2.7,3.9,1.2,1
100+
6.2,2.2,4.5,1.5,1
101+
5.0,2.3,3.3,1.0,1
102+
5.2,3.4,1.4,0.2,0
103+
5.2,3.5,1.5,0.2,0
104+
4.9,3.1,1.5,0.1,0
105+
5.4,3.7,1.5,0.2,0
106+
4.4,2.9,1.4,0.2,0
107+
6.4,3.2,5.3,2.3,2
108+
6.4,2.8,5.6,2.1,2
109+
6.5,3.0,5.8,2.2,2
110+
6.1,3.0,4.9,1.8,2
111+
6.7,2.5,5.8,1.8,2
112+
5.7,2.8,4.5,1.3,1
113+
5.6,2.7,4.2,1.3,1
114+
6.0,2.9,4.5,1.5,1
115+
5.5,2.3,4.0,1.3,1
116+
5.6,3.0,4.5,1.5,1
117+
5.1,3.3,1.7,0.5,0
118+
5.8,4.0,1.2,0.2,0
119+
4.6,3.1,1.5,0.2,0
120+
5.7,4.4,1.5,0.4,0
121+
4.9,3.0,1.4,0.2,0
122+
6.7,3.3,5.7,2.1,2
123+
6.4,2.7,5.3,1.9,2
124+
5.8,2.7,5.1,1.9,2
125+
6.2,3.4,5.4,2.3,2
126+
6.0,3.0,4.8,1.8,2
127+
5.6,2.5,3.9,1.1,1
128+
4.9,2.4,3.3,1.0,1
129+
5.1,2.5,3.0,1.1,1
130+
6.4,3.2,4.5,1.5,1
131+
5.7,3.0,4.2,1.2,1
132+
4.3,3.0,1.1,0.1,0
133+
5.1,3.7,1.5,0.4,0
134+
4.8,3.4,1.6,0.2,0
135+
4.6,3.2,1.4,0.2,0
136+
4.8,3.0,1.4,0.1,0
137+
6.9,3.1,5.4,2.1,2
138+
7.2,3.2,6.0,1.8,2
139+
7.7,3.0,6.1,2.3,2
140+
7.7,2.8,6.7,2.0,2
141+
6.5,3.0,5.2,2.0,2
142+
5.5,2.4,3.7,1.0,1
143+
5.4,3.0,4.5,1.5,1
144+
6.8,2.8,4.8,1.4,1
145+
6.1,2.9,4.7,1.4,1
146+
6.7,3.0,5.0,1.7,1
147+
5.0,3.5,1.6,0.6,0
148+
5.4,3.9,1.7,0.4,0
149+
5.1,3.4,1.5,0.2,0
150+
5.0,3.6,1.4,0.2,0
151+
5.0,3.4,1.6,0.4,0

howso/engine/tests/test_engine.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,21 @@ def test_save_load_warning(self, trainee):
209209

210210
assert load_training_cases == 150
211211

212+
def test_save_raises(self, mocker, trainee):
213+
"""Test save raises when unable to write file."""
214+
215+
def mock_store_entity(*args, **kwargs):
216+
return False
217+
218+
mocker.patch.object(trainee.client.amlg, "store_entity", side_effect=mock_store_entity)
219+
220+
resolved_path = trainee.client.resolve_trainee_filepath("foobar.caml")
221+
222+
with pytest.raises(HowsoError) as error_info:
223+
trainee.save("foobar.caml")
224+
assert error_info.value.code == "persist_failed"
225+
assert error_info.value.message == f'Failed to write Trainee "{trainee.id}" to file path: {resolved_path}'
226+
212227
def test_save_load_bad_load(self):
213228
"""Test bad disk load methods."""
214229
cwd = Path.cwd()

howso/engine/trainee.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -403,10 +403,12 @@ def save(self, file_path: t.Optional[PathLike] = None):
403403
file_name = self.id
404404

405405
if self.id:
406-
self.client.amlg.store_entity(
407-
handle=self.id,
408-
file_path=self.client.resolve_trainee_filepath(file_name, filepath=file_path)
409-
)
406+
resolved_path = self.client.resolve_trainee_filepath(file_name, filepath=file_path)
407+
is_persisted = self.client.amlg.store_entity(handle=self.id, file_path=resolved_path)
408+
if not is_persisted:
409+
raise HowsoError(
410+
f'Failed to write Trainee "{self.id}" to file path: {resolved_path}', code="persist_failed"
411+
)
410412
else:
411413
raise ValueError("Trainee ID is needed for saving.")
412414

0 commit comments

Comments
 (0)