sinonym 0.2.5__tar.gz → 0.2.7__tar.gz

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.
Files changed (64) hide show
  1. {sinonym-0.2.5 → sinonym-0.2.7}/PKG-INFO +2 -1
  2. {sinonym-0.2.5 → sinonym-0.2.7}/pyproject.toml +2 -1
  3. sinonym-0.2.7/sinonym/timo/__init__.py +0 -0
  4. sinonym-0.2.7/sinonym/timo/config.yaml +12 -0
  5. sinonym-0.2.7/sinonym/timo/integration_test.py +70 -0
  6. sinonym-0.2.7/sinonym/timo/interface.py +244 -0
  7. sinonym-0.2.7/tests/test_timo_interface.py +87 -0
  8. {sinonym-0.2.5 → sinonym-0.2.7}/.gitignore +0 -0
  9. {sinonym-0.2.5 → sinonym-0.2.7}/LICENSE +0 -0
  10. {sinonym-0.2.5 → sinonym-0.2.7}/README.md +0 -0
  11. {sinonym-0.2.5 → sinonym-0.2.7}/scripts/README.md +0 -0
  12. {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/__init__.py +0 -0
  13. {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/chinese_names_data.py +0 -0
  14. {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/coretypes/__init__.py +0 -0
  15. {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/coretypes/config.py +0 -0
  16. {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/coretypes/results.py +0 -0
  17. {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/data/README.md +0 -0
  18. {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/data/acl_2025_authors.txt +0 -0
  19. {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/data/chinese_japanese_classifier.joblib +0 -0
  20. {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/data/chinese_japanese_classifier.skops +0 -0
  21. {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/data/familyname_orcid.csv +0 -0
  22. {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/data/givenname_orcid.csv +0 -0
  23. {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/data/model_features.json +0 -0
  24. {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/detector.py +0 -0
  25. {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/ml_model_components.py +0 -0
  26. {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/patterns/__init__.py +0 -0
  27. {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/patterns/compiled_patterns.py +0 -0
  28. {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/patterns/regex_builders.py +0 -0
  29. {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/resources.py +0 -0
  30. {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/services/__init__.py +0 -0
  31. {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/services/batch_analysis.py +0 -0
  32. {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/services/cache.py +0 -0
  33. {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/services/ethnicity.py +0 -0
  34. {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/services/formatting.py +0 -0
  35. {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/services/initialization.py +0 -0
  36. {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/services/normalization.py +0 -0
  37. {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/services/parsing.py +0 -0
  38. {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/services/process_pool.py +0 -0
  39. {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/text_processing/__init__.py +0 -0
  40. {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/text_processing/compound_detector.py +0 -0
  41. {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/text_processing/text_normalizer.py +0 -0
  42. {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/text_processing/text_preprocessor.py +0 -0
  43. {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/utils/__init__.py +0 -0
  44. {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/utils/string_manipulation.py +0 -0
  45. {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/utils/thread_cache.py +0 -0
  46. {sinonym-0.2.5 → sinonym-0.2.7}/tests/_fail_log.py +0 -0
  47. {sinonym-0.2.5 → sinonym-0.2.7}/tests/conftest.py +0 -0
  48. {sinonym-0.2.5 → sinonym-0.2.7}/tests/test_acl.py +0 -0
  49. {sinonym-0.2.5 → sinonym-0.2.7}/tests/test_all_chinese_inputs.py +0 -0
  50. {sinonym-0.2.5 → sinonym-0.2.7}/tests/test_basic_chinese_names.py +0 -0
  51. {sinonym-0.2.5 → sinonym-0.2.7}/tests/test_batch.py +0 -0
  52. {sinonym-0.2.5 → sinonym-0.2.7}/tests/test_compound_names.py +0 -0
  53. {sinonym-0.2.5 → sinonym-0.2.7}/tests/test_compound_surname_formats.py +0 -0
  54. {sinonym-0.2.5 → sinonym-0.2.7}/tests/test_middle_names.py +0 -0
  55. {sinonym-0.2.5 → sinonym-0.2.7}/tests/test_misc.py +0 -0
  56. {sinonym-0.2.5 → sinonym-0.2.7}/tests/test_mixed_production_cases.py +0 -0
  57. {sinonym-0.2.5 → sinonym-0.2.7}/tests/test_mixed_scripts.py +0 -0
  58. {sinonym-0.2.5 → sinonym-0.2.7}/tests/test_ml_japanese_detection.py +0 -0
  59. {sinonym-0.2.5 → sinonym-0.2.7}/tests/test_multiprocess_pool.py +0 -0
  60. {sinonym-0.2.5 → sinonym-0.2.7}/tests/test_name_formatting.py +0 -0
  61. {sinonym-0.2.5 → sinonym-0.2.7}/tests/test_non_chinese_rejection.py +0 -0
  62. {sinonym-0.2.5 → sinonym-0.2.7}/tests/test_performance.py +0 -0
  63. {sinonym-0.2.5 → sinonym-0.2.7}/tests/test_regional_variants.py +0 -0
  64. {sinonym-0.2.5 → sinonym-0.2.7}/tests/test_regression_proposals.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sinonym
3
- Version: 0.2.5
3
+ Version: 0.2.7
4
4
  Summary: Chinese Name Detection and Normalization Module
5
5
  Project-URL: Homepage, https://github.com/allenai/sinonym
6
6
  Project-URL: Repository, https://github.com/allenai/sinonym
@@ -21,6 +21,7 @@ Classifier: Topic :: Text Processing :: Linguistic
21
21
  Requires-Python: >=3.10
22
22
  Requires-Dist: joblib>=1.3
23
23
  Requires-Dist: numpy>=1.24
24
+ Requires-Dist: pydantic<2,>=1.10
24
25
  Requires-Dist: pypinyin>=0.44.0
25
26
  Requires-Dist: requests>=2.31
26
27
  Requires-Dist: scikit-learn<2,>=1.2
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sinonym"
7
- version = "0.2.5"
7
+ version = "0.2.7"
8
8
  description = "Chinese Name Detection and Normalization Module"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -27,6 +27,7 @@ keywords = ["chinese", "names", "nlp", "romanization", "pinyin"]
27
27
  dependencies = [
28
28
  "joblib>=1.3",
29
29
  "numpy>=1.24",
30
+ "pydantic>=1.10,<2",
30
31
  "pypinyin>=0.44.0",
31
32
  "requests>=2.31",
32
33
  "scikit-learn>=1.2,<2",
File without changes
@@ -0,0 +1,12 @@
1
+ config_version: 0.0.1
2
+ model_variants:
3
+ sinonym_v1:
4
+ instance: sinonym.timo.interface.Instance
5
+ prediction: sinonym.timo.interface.Prediction
6
+ predictor: sinonym.timo.interface.Predictor
7
+ predictor_config: sinonym.timo.interface.PredictorConfig
8
+ artifacts_s3_path: null
9
+ python_version: "3.10"
10
+ cuda: False
11
+ integration_test: sinonym.timo.integration_test.TestIntegration
12
+ docker_run_commands: []
@@ -0,0 +1,70 @@
1
+ import unittest
2
+
3
+ from sinonym.timo.interface import Instance, Prediction, Predictor, PredictorConfig
4
+
5
+
6
+ class TestIntegration(unittest.TestCase):
7
+ @classmethod
8
+ def setUpClass(cls):
9
+ cls.predictor = Predictor(config=PredictorConfig(), artifacts_dir=".")
10
+
11
+ def test_chinese_name(self):
12
+ results = self.predictor.predict_batch([Instance(name="Li Wei")])
13
+ self.assertEqual(len(results), 1)
14
+ self.assertIsInstance(results[0], Prediction)
15
+ self.assertTrue(results[0].success)
16
+ self.assertIsNotNone(results[0].given_name)
17
+ self.assertIsNotNone(results[0].surname)
18
+
19
+ def test_non_chinese_name(self):
20
+ results = self.predictor.predict_batch([Instance(name="John Smith")])
21
+ self.assertEqual(len(results), 1)
22
+ self.assertFalse(results[0].success)
23
+ self.assertIsNotNone(results[0].error_message)
24
+
25
+ def test_batch_superset_output(self):
26
+ instances = [Instance(name="Li Wei"), Instance(name="Wang Weiming")]
27
+ results = self.predictor.predict_batch(instances)
28
+ self.assertEqual(len(results), 2)
29
+ for r in results:
30
+ self.assertIsInstance(r, Prediction)
31
+ self.assertTrue(r.success)
32
+ # superset: every Prediction carries confidence + format_pattern
33
+ self.assertIsNotNone(r.confidence)
34
+ self.assertIsNotNone(r.format_pattern)
35
+ # shared batch pattern replicated onto each row
36
+ self.assertEqual(
37
+ results[0].format_pattern.dominant_format,
38
+ results[1].format_pattern.dominant_format,
39
+ )
40
+
41
+ def test_predict_batch_empty(self):
42
+ self.assertEqual(self.predictor.predict_batch([]), [])
43
+
44
+ def test_score_name_batch(self):
45
+ names = ["Li Wei", "Wang Weiming"]
46
+ summary = self.predictor.score_name_batch(names)
47
+ self.assertEqual(summary.names, names)
48
+ self.assertEqual(len(summary.results), 2)
49
+ self.assertEqual(len(summary.confidences), 2)
50
+ self.assertIsNotNone(summary.format_pattern.dominant_format)
51
+
52
+ def test_score_name_batch_tuned_threshold(self):
53
+ names = ["Li Wei", "Wang Weiming"]
54
+ summary = self.predictor.score_name_batch(names, format_threshold=0.9)
55
+ self.assertEqual(len(summary.results), 2)
56
+ self.assertEqual(
57
+ summary.format_pattern.threshold_met,
58
+ summary.format_pattern.confidence >= 0.9,
59
+ )
60
+
61
+ def test_detect_batch_format(self):
62
+ pattern = self.predictor.detect_batch_format(["Zhang Wei", "Li Ming", "Wang Xiaoli"])
63
+ self.assertIn(
64
+ pattern.dominant_format, {"surname_first", "given_first", "mixed"}
65
+ )
66
+
67
+ def test_analyze_name_batch_full(self):
68
+ result = self.predictor.analyze_name_batch(["Li Wei", "Wang Weiming"])
69
+ self.assertEqual(len(result.results), 2)
70
+ self.assertEqual(len(result.individual_analyses), 2)
@@ -0,0 +1,244 @@
1
+ from typing import List, Optional
2
+
3
+ from pydantic import BaseModel, BaseSettings, Field
4
+
5
+ from sinonym.detector import ChineseNameDetector
6
+
7
+
8
+ class Instance(BaseModel):
9
+ name: str = Field(description="Name string to detect/normalize as Chinese")
10
+
11
+
12
+ class FormatPattern(BaseModel):
13
+ """Batch-level order detection (surname-first vs given-first)."""
14
+
15
+ dominant_format: str = Field(description="surname_first | given_first | mixed")
16
+ confidence: float = Field(description="dominant_count / total_count")
17
+ surname_first_count: int
18
+ given_first_count: int
19
+ total_count: int
20
+ threshold_met: bool = Field(description="confidence >= format_threshold")
21
+
22
+
23
+ class Prediction(BaseModel):
24
+ success: bool = Field(description="Whether the name was recognized as Chinese")
25
+ error_message: Optional[str] = Field(default=None, description="Reason for failure")
26
+ given_name: Optional[str] = Field(default=None)
27
+ surname: Optional[str] = Field(default=None)
28
+ middle_name: Optional[str] = Field(default=None)
29
+ confidence: Optional[float] = Field(
30
+ default=None, description="per-name confidence (softmax over candidate scores)"
31
+ )
32
+ format_pattern: Optional[FormatPattern] = Field(
33
+ default=None, description="shared batch order pattern (same on every row)"
34
+ )
35
+
36
+
37
+ class Candidate(BaseModel):
38
+ surname_tokens: List[str]
39
+ given_tokens: List[str]
40
+ score: float
41
+ format: str = Field(description="surname_first | given_first | mixed")
42
+ original_compound_format: Optional[str] = None
43
+
44
+
45
+ class IndividualAnalysis(BaseModel):
46
+ """Per-name analysis, pre batch-override."""
47
+
48
+ raw_name: str
49
+ candidates: List[Candidate]
50
+ best_candidate: Optional[Candidate] = None
51
+ confidence: float = Field(description="softmax over candidate scores for best candidate")
52
+
53
+
54
+ class BatchPrediction(BaseModel):
55
+ """Full result of analyze_name_batch."""
56
+
57
+ names: List[str]
58
+ results: List[Prediction]
59
+ format_pattern: FormatPattern
60
+ individual_analyses: List[IndividualAnalysis]
61
+ improvements: List[int] = Field(description="indices of names changed by batch context")
62
+
63
+
64
+ class BatchSummary(BaseModel):
65
+ """Trimmed analyze_name_batch result: drops candidates, keeps per-name confidence only."""
66
+
67
+ names: List[str]
68
+ results: List[Prediction]
69
+ format_pattern: FormatPattern
70
+ confidences: List[float] = Field(
71
+ description="per-name confidence from individual_analyses, aligned with results"
72
+ )
73
+
74
+
75
+ class PredictorConfig(BaseSettings):
76
+ pass
77
+
78
+
79
+ class Predictor:
80
+ _config: PredictorConfig
81
+ _artifacts_dir: str
82
+
83
+ def __init__(self, config: PredictorConfig, artifacts_dir: str):
84
+ self._config = config
85
+ self._artifacts_dir = artifacts_dir
86
+ self._detector = ChineseNameDetector()
87
+
88
+ # ---- converters -------------------------------------------------------
89
+
90
+ def _to_prediction(self, parse_result) -> Prediction:
91
+ return Prediction(
92
+ success=parse_result.success,
93
+ error_message=parse_result.error_message,
94
+ given_name=parse_result.parsed.given_name if parse_result.parsed else None,
95
+ surname=parse_result.parsed.surname if parse_result.parsed else None,
96
+ middle_name=(
97
+ parse_result.parsed.middle_name
98
+ if parse_result.parsed and parse_result.parsed.middle_name
99
+ else None
100
+ ),
101
+ )
102
+
103
+ def _to_format_pattern(self, pattern) -> FormatPattern:
104
+ return FormatPattern(
105
+ dominant_format=pattern.dominant_format.value,
106
+ confidence=pattern.confidence,
107
+ surname_first_count=pattern.surname_first_count,
108
+ given_first_count=pattern.given_first_count,
109
+ total_count=pattern.total_count,
110
+ threshold_met=pattern.threshold_met,
111
+ )
112
+
113
+ def _to_candidate(self, candidate) -> Candidate:
114
+ return Candidate(
115
+ surname_tokens=list(candidate.surname_tokens),
116
+ given_tokens=list(candidate.given_tokens),
117
+ score=candidate.score,
118
+ format=candidate.format.value,
119
+ original_compound_format=candidate.original_compound_format,
120
+ )
121
+
122
+ def _to_individual_analysis(self, analysis) -> IndividualAnalysis:
123
+ return IndividualAnalysis(
124
+ raw_name=analysis.raw_name,
125
+ candidates=[self._to_candidate(c) for c in analysis.candidates],
126
+ best_candidate=(
127
+ self._to_candidate(analysis.best_candidate)
128
+ if analysis.best_candidate is not None
129
+ else None
130
+ ),
131
+ confidence=analysis.confidence,
132
+ )
133
+
134
+ def _to_batch_prediction(self, batch_result) -> BatchPrediction:
135
+ return BatchPrediction(
136
+ names=list(batch_result.names),
137
+ results=[self._to_prediction(r) for r in batch_result.results],
138
+ format_pattern=self._to_format_pattern(batch_result.format_pattern),
139
+ individual_analyses=[
140
+ self._to_individual_analysis(a) for a in batch_result.individual_analyses
141
+ ],
142
+ improvements=list(batch_result.improvements),
143
+ )
144
+
145
+ def _to_batch_summary(self, batch_result) -> BatchSummary:
146
+ return BatchSummary(
147
+ names=list(batch_result.names),
148
+ results=[self._to_prediction(r) for r in batch_result.results],
149
+ format_pattern=self._to_format_pattern(batch_result.format_pattern),
150
+ confidences=[a.confidence for a in batch_result.individual_analyses],
151
+ )
152
+
153
+ # ---- timo entrypoint --------------------------------------------------
154
+
155
+ def predict_batch(self, instances: List[Instance]) -> List[Prediction]:
156
+ """timo HTTP entrypoint. Analyzes the whole batch jointly.
157
+
158
+ Names are processed together (cross-batch order detection), returning one
159
+ Prediction per name (index-aligned) with surname/given_name/middle_name,
160
+ per-name `confidence`, and the shared `format_pattern` (same on every row).
161
+ """
162
+ if not instances:
163
+ return []
164
+
165
+ names = [i.name for i in instances]
166
+ batch_result = self._detector.analyze_name_batch(names)
167
+ pattern = self._to_format_pattern(batch_result.format_pattern)
168
+
169
+ predictions = []
170
+ for parse_result, analysis in zip(
171
+ batch_result.results, batch_result.individual_analyses
172
+ ):
173
+ prediction = self._to_prediction(parse_result)
174
+ prediction.confidence = analysis.confidence
175
+ prediction.format_pattern = pattern
176
+ predictions.append(prediction)
177
+ return predictions
178
+
179
+ # ---- exposed detector functions --------------------------------------
180
+
181
+ @staticmethod
182
+ def _batch_kwargs(format_threshold, minimum_batch_size=...) -> dict:
183
+ """Forward only caller-set tuning params; let sinonym own the defaults."""
184
+ kw = {}
185
+ if format_threshold is not None:
186
+ kw["format_threshold"] = format_threshold
187
+ if minimum_batch_size is not ... and minimum_batch_size is not None:
188
+ kw["minimum_batch_size"] = minimum_batch_size
189
+ return kw
190
+
191
+ def analyze_name_batch(
192
+ self,
193
+ names: List[str],
194
+ format_threshold: Optional[float] = None,
195
+ minimum_batch_size: Optional[int] = None,
196
+ ) -> BatchPrediction:
197
+ batch_result = self._detector.analyze_name_batch(
198
+ names, **self._batch_kwargs(format_threshold, minimum_batch_size)
199
+ )
200
+ return self._to_batch_prediction(batch_result)
201
+
202
+ def process_name_batch(
203
+ self,
204
+ names: List[str],
205
+ format_threshold: Optional[float] = None,
206
+ minimum_batch_size: Optional[int] = None,
207
+ ) -> List[Prediction]:
208
+ results = self._detector.process_name_batch(
209
+ names, **self._batch_kwargs(format_threshold, minimum_batch_size)
210
+ )
211
+ return [self._to_prediction(r) for r in results]
212
+
213
+ def detect_batch_format(
214
+ self,
215
+ names: List[str],
216
+ format_threshold: Optional[float] = None,
217
+ ) -> FormatPattern:
218
+ pattern = self._detector.detect_batch_format(
219
+ names, **self._batch_kwargs(format_threshold)
220
+ )
221
+ return self._to_format_pattern(pattern)
222
+
223
+ def process_name_batch_multiprocess(
224
+ self,
225
+ names: List[str],
226
+ max_workers: Optional[int] = None,
227
+ chunk_size: int = 64,
228
+ ) -> List[Prediction]:
229
+ results = self._detector.process_name_batch_multiprocess(
230
+ names, max_workers=max_workers, chunk_size=chunk_size
231
+ )
232
+ return [self._to_prediction(r) for r in results]
233
+
234
+ def score_name_batch(
235
+ self,
236
+ names: List[str],
237
+ format_threshold: Optional[float] = None,
238
+ minimum_batch_size: Optional[int] = None,
239
+ ) -> BatchSummary:
240
+ """analyze_name_batch trimmed to names, results, format_pattern, per-name confidence."""
241
+ batch_result = self._detector.analyze_name_batch(
242
+ names, **self._batch_kwargs(format_threshold, minimum_batch_size)
243
+ )
244
+ return self._to_batch_summary(batch_result)
@@ -0,0 +1,87 @@
1
+ import unittest
2
+
3
+ from sinonym.timo.interface import Instance, Prediction, Predictor, PredictorConfig
4
+
5
+
6
+ class TestTimoInterface(unittest.TestCase):
7
+ @classmethod
8
+ def setUpClass(cls):
9
+ cls.predictor = Predictor(config=PredictorConfig(), artifacts_dir=".")
10
+
11
+ def test_single_chinese_name(self):
12
+ results = self.predictor.predict_batch([Instance(name="Li Wei")])
13
+ self.assertEqual(len(results), 1)
14
+ r = results[0]
15
+ self.assertTrue(r.success)
16
+ self.assertIsNotNone(r.surname)
17
+ self.assertIsNotNone(r.given_name)
18
+ self.assertIsNotNone(r.confidence)
19
+ self.assertIsNone(r.error_message)
20
+
21
+ def test_chinese_name_characters(self):
22
+ results = self.predictor.predict_batch([Instance(name="巩俐")])
23
+ self.assertTrue(results[0].success)
24
+ self.assertIsNotNone(results[0].surname)
25
+
26
+ def test_chinese_name_comma_format(self):
27
+ results = self.predictor.predict_batch([Instance(name="Zhang, Ming")])
28
+ r = results[0]
29
+ self.assertTrue(r.success)
30
+ self.assertIsNotNone(r.surname)
31
+ self.assertIsNotNone(r.given_name)
32
+
33
+ def test_non_chinese_name(self):
34
+ results = self.predictor.predict_batch([Instance(name="John Smith")])
35
+ r = results[0]
36
+ self.assertFalse(r.success)
37
+ self.assertIsNotNone(r.error_message)
38
+ self.assertIsNone(r.given_name)
39
+ self.assertIsNone(r.surname)
40
+
41
+ def test_empty_string(self):
42
+ results = self.predictor.predict_batch([Instance(name="")])
43
+ self.assertFalse(results[0].success)
44
+
45
+ def test_whitespace_only(self):
46
+ results = self.predictor.predict_batch([Instance(name=" ")])
47
+ self.assertFalse(results[0].success)
48
+
49
+ def test_batch_with_context(self):
50
+ instances = [
51
+ Instance(name="Li Wei"),
52
+ Instance(name="Wang Weiming"),
53
+ Instance(name="John Smith"),
54
+ ]
55
+ results = self.predictor.predict_batch(instances)
56
+ self.assertEqual(len(results), 3)
57
+ self.assertTrue(results[0].success)
58
+ self.assertTrue(results[1].success)
59
+ self.assertFalse(results[2].success)
60
+
61
+ def test_prediction_is_pydantic_model(self):
62
+ results = self.predictor.predict_batch([Instance(name="Li Wei")])
63
+ r = results[0]
64
+ self.assertIsInstance(r, Prediction)
65
+ as_dict = r.dict()
66
+ self.assertIn("success", as_dict)
67
+ self.assertIn("error_message", as_dict)
68
+ self.assertIn("given_name", as_dict)
69
+ self.assertIn("surname", as_dict)
70
+ self.assertIn("middle_name", as_dict)
71
+ self.assertIn("confidence", as_dict)
72
+ self.assertIn("format_pattern", as_dict)
73
+
74
+ def test_prediction_json_serialization(self):
75
+ results = self.predictor.predict_batch([Instance(name="Li Wei")])
76
+ json_str = results[0].json()
77
+ self.assertIn('"success": true', json_str)
78
+ self.assertIn('"confidence":', json_str)
79
+
80
+ def test_compound_surname(self):
81
+ results = self.predictor.predict_batch([Instance(name="Ouyang Ming")])
82
+ if results[0].success:
83
+ self.assertIsNotNone(results[0].surname)
84
+
85
+ def test_empty_batch(self):
86
+ results = self.predictor.predict_batch([])
87
+ self.assertEqual(len(results), 0)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes