sitcod3 commited on
Commit
cb964d1
·
verified ·
1 Parent(s): cf368ae

Upload folder using huggingface_hub

Browse files
Files changed (2) hide show
  1. app/validator/engine.py +17 -0
  2. tests/test_engine_ml.py +46 -1
app/validator/engine.py CHANGED
@@ -226,6 +226,23 @@ class EmailValidationEngine:
226
  # ---- Layer 4: Disposable Detection ---------------------------
227
  disposable = check_disposable(normalized_email, domain)
228
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
  if not deep:
230
  # Fast path: skip SMTP for quick validation
231
  elapsed = (time.monotonic() - start_time) * 1000
 
226
  # ---- Layer 4: Disposable Detection ---------------------------
227
  disposable = check_disposable(normalized_email, domain)
228
 
229
+ # ML advisory (single path): unknown domains only, fail-open. Mirrors
230
+ # the batch pipeline so both surfaces agree on the same address.
231
+ ml_model = get_model()
232
+ if (
233
+ ml_model is not None
234
+ and not domain_is_known(domain)
235
+ and deep
236
+ ):
237
+ try:
238
+ preds = ml_model.predict([f"john.smith [SEP] {domain}"])
239
+ except Exception:
240
+ preds = None
241
+ if preds:
242
+ disposable = disposable.model_copy(
243
+ update={"ml_label": preds[0][0], "ml_confidence": preds[0][1]}
244
+ )
245
+
246
  if not deep:
247
  # Fast path: skip SMTP for quick validation
248
  elapsed = (time.monotonic() - start_time) * 1000
tests/test_engine_ml.py CHANGED
@@ -46,7 +46,10 @@ class FakeProvider:
46
 
47
  def _engine_with_ml(flag=True, repo="user/model", predict=None):
48
  engine = EmailValidationEngine(mailbox=FakeProvider(), cache=NullCache())
49
- if predict is not None:
 
 
 
50
  fake_model = MagicMock()
51
  fake_model.predict = MagicMock(side_effect=predict)
52
  fake_model.load = MagicMock(return_value=True)
@@ -79,6 +82,13 @@ def _stop(engine):
79
 
80
 
81
  class EngineMLTests(unittest.IsolatedAsyncioTestCase):
 
 
 
 
 
 
 
82
  def tearDown(self):
83
  for e in getattr(self, "_engines", []):
84
  _stop(e)
@@ -165,5 +175,40 @@ class EngineMLTests(unittest.IsolatedAsyncioTestCase):
165
  self.assertEqual(results[1].disposable.ml_label, "NORMAL")
166
 
167
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  if __name__ == "__main__":
169
  unittest.main()
 
46
 
47
  def _engine_with_ml(flag=True, repo="user/model", predict=None):
48
  engine = EmailValidationEngine(mailbox=FakeProvider(), cache=NullCache())
49
+ # The get_model patcher must honor the flag: with the flag off the REAL
50
+ # get_model returns None (that IS the production contract), so the double
51
+ # must not force a model into a disabled engine.
52
+ if predict is not None and flag:
53
  fake_model = MagicMock()
54
  fake_model.predict = MagicMock(side_effect=predict)
55
  fake_model.load = MagicMock(return_value=True)
 
82
 
83
 
84
  class EngineMLTests(unittest.IsolatedAsyncioTestCase):
85
+ def setUp(self):
86
+ self._engines = []
87
+
88
+ def tearDown(self):
89
+ # Stop settings/model patchers so they never leak into other tests.
90
+ for e in self._engines:
91
+ _stop(e)
92
  def tearDown(self):
93
  for e in getattr(self, "_engines", []):
94
  _stop(e)
 
175
  self.assertEqual(results[1].disposable.ml_label, "NORMAL")
176
 
177
 
178
+ async def test_single_validate_also_applies_the_ml_signal(self):
179
+ # The single endpoint runs engine.validate() - it must get the same
180
+ # advisory signal as bulk, or the two surfaces disagree on one address.
181
+ engine = _engine_with_ml(
182
+ flag=True, predict=[[("DISPOSABLE", 0.93)]]
183
+ )
184
+ self._engines.append(engine)
185
+ with patch("app.validator.engine.check_dns", new=AsyncMock(return_value=_dns())):
186
+ result = await engine.validate("a@brand-new-disposable-xyz.tk", deep=True)
187
+
188
+ self.assertEqual(result.disposable.ml_label, "DISPOSABLE")
189
+ self.assertEqual(
190
+ result.scoring.breakdown.get("ML_DISPOSABLE_SUSPECT"), -5
191
+ )
192
+
193
+ async def test_single_validate_flag_off_unchanged(self):
194
+ engine = _engine_with_ml(flag=False, predict=[[("DISPOSABLE", 0.93)]])
195
+ self._engines.append(engine)
196
+ with patch("app.validator.engine.check_dns", new=AsyncMock(return_value=_dns())):
197
+ result = await engine.validate("a@brand-new-disposable-xyz.tk", deep=True)
198
+
199
+ self.assertIsNone(result.disposable.ml_label)
200
+
201
+ async def test_single_validate_known_domain_skips_ml(self):
202
+ engine = _engine_with_ml(
203
+ flag=True, predict=[AssertionError("must not run")]
204
+ )
205
+ self._engines.append(engine)
206
+ with patch("app.validator.engine.check_dns", new=AsyncMock(return_value=_dns())):
207
+ result = await engine.validate("a@mailinator.com", deep=True)
208
+
209
+ self.assertEqual(result.status, ValidationStatus.INVALID)
210
+ self.assertIsNone(result.disposable.ml_label)
211
+
212
+
213
  if __name__ == "__main__":
214
  unittest.main()