sitcod3 commited on
Commit
7658255
·
verified ·
1 Parent(s): 3be79c6

Upload folder using huggingface_hub

Browse files
Files changed (2) hide show
  1. app/ml/runtime.py +4 -1
  2. tests/test_ml_runtime.py +33 -22
app/ml/runtime.py CHANGED
@@ -115,7 +115,7 @@ _model_lock = threading.Lock()
115
 
116
 
117
  def get_model() -> Optional[EmailRiskModel]:
118
- """Process-wide model, built only when the flag and repo are configured."""
119
  global _model
120
  if not settings.ml_disposable_enabled or not settings.ml_model_repo:
121
  return None
@@ -126,4 +126,7 @@ def get_model() -> Optional[EmailRiskModel]:
126
  token=settings.ml_hf_token,
127
  batch_size=settings.ml_batch_size,
128
  )
 
 
 
129
  return _model
 
115
 
116
 
117
  def get_model() -> Optional[EmailRiskModel]:
118
+ """Process-wide model, built AND loaded when flag + repo are configured."""
119
  global _model
120
  if not settings.ml_disposable_enabled or not settings.ml_model_repo:
121
  return None
 
126
  token=settings.ml_hf_token,
127
  batch_size=settings.ml_batch_size,
128
  )
129
+ # Load at construction: without this the model fail-opens to None
130
+ # on every predict and the feature is silently dead (found live).
131
+ _model.load()
132
  return _model
tests/test_ml_runtime.py CHANGED
@@ -14,9 +14,6 @@ class RuntimeLoaderTests(unittest.TestCase):
14
  """
15
 
16
  def test_load_uses_the_injected_factory(self):
17
- # Dependency injection: the factory builds the pipeline; the runtime
18
- # never imports transformers itself. This is what makes the ML layer
19
- # testable without patching a lazy module boundary.
20
  fake_pipe = MagicMock()
21
  calls = {}
22
 
@@ -41,8 +38,6 @@ class RuntimeLoaderTests(unittest.TestCase):
41
  self.assertIsNone(m._pipeline)
42
 
43
  def test_load_uses_pytorch_transformers_by_default(self):
44
- # The production factory loads from transformers (PyTorch - the ONNX
45
- # export produced garbage predictions, so it was abandoned).
46
  from app.ml.runtime import default_pipeline_factory
47
 
48
  m = EmailRiskModel(repo="user/model", token="t")
@@ -61,7 +56,6 @@ class RuntimeLoaderTests(unittest.TestCase):
61
  out = m.predict(["x [SEP] tempmail.example"])
62
 
63
  self.assertEqual(out, [("DISPOSABLE", 0.93)])
64
- # Batching is passed through - 1-row calls are a performance bug.
65
  fake.assert_called_once()
66
  self.assertIn("batch_size", fake.call_args.kwargs)
67
 
@@ -80,40 +74,57 @@ class RuntimeLoaderTests(unittest.TestCase):
80
  self.assertEqual(m.predict(["a [SEP] b.test"])[0][0], "DISPOSABLE")
81
 
82
  def test_predict_accepts_plain_string_batch_return(self):
83
- # transformers may return a plain list of dicts OR nested batch lists.
84
  m = EmailRiskModel(repo="user/model", token="t")
85
  fake = MagicMock(return_value=[[{"label": "NORMAL", "score": 0.7}]])
86
  m._pipeline = fake
87
  m._loaded = True
88
  self.assertEqual(m.predict(["a [SEP] b.test"]), [("NORMAL", 0.7)])
89
 
90
- def test_config_defaults_are_off(self):
91
- from app.config import settings
92
 
93
- self.assertFalse(settings.ml_disposable_enabled)
94
- self.assertEqual(settings.ml_model_repo, "")
 
95
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
- class GetModelTests(unittest.TestCase):
98
  def test_returns_none_when_disabled(self):
99
- with patch("app.ml.runtime.settings") as fake_settings:
100
- fake_settings.ml_disposable_enabled = False
101
- fake_settings.ml_model_repo = "user/model"
102
  self.assertIsNone(get_model())
103
 
104
  def test_returns_none_when_no_repo(self):
105
- with patch("app.ml.runtime.settings") as fake_settings:
106
- fake_settings.ml_disposable_enabled = True
107
- fake_settings.ml_model_repo = ""
108
  self.assertIsNone(get_model())
109
 
110
  def test_returns_model_when_enabled_and_configured(self):
111
- with patch("app.ml.runtime.settings") as fake_settings:
112
- fake_settings.ml_disposable_enabled = True
113
- fake_settings.ml_model_repo = "user/model"
 
114
  m = get_model()
115
  self.assertIsInstance(m, EmailRiskModel)
116
- self.assertEqual(m.repo, "user/model")
 
 
 
 
 
 
 
 
 
 
117
 
118
 
119
  if __name__ == "__main__":
 
14
  """
15
 
16
  def test_load_uses_the_injected_factory(self):
 
 
 
17
  fake_pipe = MagicMock()
18
  calls = {}
19
 
 
38
  self.assertIsNone(m._pipeline)
39
 
40
  def test_load_uses_pytorch_transformers_by_default(self):
 
 
41
  from app.ml.runtime import default_pipeline_factory
42
 
43
  m = EmailRiskModel(repo="user/model", token="t")
 
56
  out = m.predict(["x [SEP] tempmail.example"])
57
 
58
  self.assertEqual(out, [("DISPOSABLE", 0.93)])
 
59
  fake.assert_called_once()
60
  self.assertIn("batch_size", fake.call_args.kwargs)
61
 
 
74
  self.assertEqual(m.predict(["a [SEP] b.test"])[0][0], "DISPOSABLE")
75
 
76
  def test_predict_accepts_plain_string_batch_return(self):
 
77
  m = EmailRiskModel(repo="user/model", token="t")
78
  fake = MagicMock(return_value=[[{"label": "NORMAL", "score": 0.7}]])
79
  m._pipeline = fake
80
  m._loaded = True
81
  self.assertEqual(m.predict(["a [SEP] b.test"]), [("NORMAL", 0.7)])
82
 
 
 
83
 
84
+ class GetModelTests(unittest.TestCase):
85
+ def setUp(self):
86
+ from app.ml import runtime
87
 
88
+ self.runtime = runtime
89
+ runtime._model = None # reset the process-wide singleton
90
+
91
+ def tearDown(self):
92
+ self.runtime._model = None
93
+
94
+ def _patch_settings(self, enabled, repo):
95
+ return (
96
+ patch.object(self.runtime.settings, "ml_disposable_enabled", enabled),
97
+ patch.object(self.runtime.settings, "ml_model_repo", repo),
98
+ )
99
 
 
100
  def test_returns_none_when_disabled(self):
101
+ p1, p2 = self._patch_settings(False, "user/model")
102
+ with p1, p2:
 
103
  self.assertIsNone(get_model())
104
 
105
  def test_returns_none_when_no_repo(self):
106
+ p1, p2 = self._patch_settings(True, "")
107
+ with p1, p2:
 
108
  self.assertIsNone(get_model())
109
 
110
  def test_returns_model_when_enabled_and_configured(self):
111
+ p1, p2 = self._patch_settings(True, "user/model")
112
+ with p1, p2, patch.object(
113
+ EmailRiskModel, "load", return_value=True
114
+ ) as load_mock:
115
  m = get_model()
116
  self.assertIsInstance(m, EmailRiskModel)
117
+ # The model must actually be LOADED - an unloaded model fail-opens to
118
+ # None on every predict, silently disabling the feature (found live).
119
+ self.assertTrue(load_mock.called)
120
+
121
+ def test_load_failure_still_returns_model_but_fail_open(self):
122
+ p1, p2 = self._patch_settings(True, "user/model")
123
+ with p1, p2, patch.object(EmailRiskModel, "load", return_value=False):
124
+ m = get_model()
125
+ self.assertIsInstance(m, EmailRiskModel)
126
+ self.assertFalse(m.loaded)
127
+ self.assertIsNone(m.predict(["a [SEP] b.test"]))
128
 
129
 
130
  if __name__ == "__main__":