Instructions to use SlayerLab/NERGAL with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use SlayerLab/NERGAL with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("token-classification", model="SlayerLab/NERGAL")# Load model directly from transformers import AutoTokenizer, AutoModelForTokenClassification tokenizer = AutoTokenizer.from_pretrained("SlayerLab/NERGAL") model = AutoModelForTokenClassification.from_pretrained("SlayerLab/NERGAL", device_map="auto") - Notebooks
- Google Colab
- Kaggle
Release 1.0.1: prefix-only glued-email trim.
Browse filesSame epoch-5 weights; 841-dev union FP 133→123 with no gold loss.
Co-authored-by: Cursor <cursoragent@cursor.com>
- CHANGELOG.md +24 -0
- README.md +21 -9
- hybrid.json +19 -1
- nergal.py +2 -1
- scrub_pii.py +47 -2
- test_nergal.py +6 -2
CHANGELOG.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# NERGAL versions
|
| 2 |
+
|
| 3 |
+
Semver for this island:
|
| 4 |
+
|
| 5 |
+
- **MAJOR** — API, threshold, or weight recipe changes
|
| 6 |
+
- **MINOR** — new capability, same API
|
| 7 |
+
- **PATCH** — rules or card fix, same weights and API
|
| 8 |
+
|
| 9 |
+
Accuracy is 841-dev, union at 0.95, 354 gold spans. A version that changes those numbers must update `hybrid.json` `eval` and the tables below.
|
| 10 |
+
|
| 11 |
+
## 1.0.1
|
| 12 |
+
|
| 13 |
+
Prefix-only glued-email trim. Cluster-gated title-case 5–11 letter prefixes are dropped from the redaction when the remainder is already a lowercase-local email. Same epoch-5 weights. Lost gold 0. New clean-passage damage 0.
|
| 14 |
+
|
| 15 |
+
| Version | Whole /354 | Residual | Rules FP | Union FP | Char P | Char R | What changed |
|
| 16 |
+
|---|---:|---:|---:|---:|---:|---:|---|
|
| 17 |
+
| 1.0.0 | 323 | 25 | 133 | 133 | 97.76% | 95.95% | First Hub snapshot |
|
| 18 |
+
| 1.0.1 | 323 | 25 | 98 | 123 | 97.93% | 95.95% | Prefix-only glued-email trim |
|
| 19 |
+
|
| 20 |
+
Rules SHA `4dcc441c…`. Seven emails trimmed, 35 prefix characters dropped; the model still covers 25 of those positions. Suffix glue is unchanged.
|
| 21 |
+
|
| 22 |
+
## 1.0.0
|
| 23 |
+
|
| 24 |
+
First Hub snapshot. Seed `202609160`, epoch 5, rules `547c0428…`. Union FP 133 was the glued-email regex floor. This seed added no new false characters versus the historical incumbent.
|
README.md
CHANGED
|
@@ -13,7 +13,7 @@ tags:
|
|
| 13 |
- hybrid
|
| 14 |
---
|
| 15 |
|
| 16 |
-
# NERGAL
|
| 17 |
|
| 18 |
**Named Entity Recognition with Grounded Additive Labels**
|
| 19 |
|
|
@@ -23,10 +23,20 @@ SlayerLab hybrid PII cleaner for Polish. Not a chat model. Not a drop-in `pipeli
|
|
| 23 |
|
| 24 |
Python rules do the identifiers they can prove. A transformer NER head adds phone and other PII spans the regex misses. The cleaner **unions** the two on the original text, then replaces hits with `[Telefon]` or `[PII]`.
|
| 25 |
|
| 26 |
-
- **
|
|
|
|
| 27 |
- **Additive labels:** XLM-RoBERTa-large token classifier, BIO tags `phone` / `pii`, threshold 0.95
|
| 28 |
- **This snapshot:** seed `202609160`, **epoch 5** of a seven-epoch schedule
|
| 29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
## 841-dev
|
| 31 |
|
| 32 |
Tables use one development split: 841 passages, 215 with gold PII, **354 spans** (169 phone, 185 other PII). It is the `dev` side of a 4,500-passage labelled tranche (3,655 train / 841 dev). Sources match Dynaword (EUR-Lex, HPLT, Wikipedia, parliamentary and government text, plus smaller news/literary slices). Labels mix unchanged silver with human review.
|
|
@@ -57,19 +67,19 @@ Fresh XLM-R, seven epochs. 133 epoch/threshold combinations. Epoch 5 at 0.95 was
|
|
| 57 |
|
| 58 |
## Compared with other systems
|
| 59 |
|
| 60 |
-
Same 841-dev split, threshold **0.95**. **Naked** is the transformer alone. **∪ regex** is that model unioned with the
|
| 61 |
|
| 62 |
| System | Mode | Whole /354 | Residual | False chars | Char P | Char R |
|
| 63 |
|---|---|---:|---:|---:|---:|---:|
|
| 64 |
-
| Regex (`scrub_pii`) | rules | 245 | 73 |
|
| 65 |
| GLiNER 2.5-multi zero-shot | naked | 81 | 188 | 970 | 56.98% | 21.22% |
|
| 66 |
| GLiNER 2.5-multi zero-shot | ∪ regex | 262 | 63 | 1,103 | 82.36% | 85.02% |
|
| 67 |
| Historical GLiNER email12 | naked | 249 | 70 | 10 | 99.81% | 85.39% |
|
| 68 |
| Historical GLiNER email12 | ∪ regex | 289 | 51 | 143 | 97.54% | 93.48% |
|
| 69 |
| XLM-R epoch 5 | naked | 298 | 42 | 57 | 98.96% | 89.38% |
|
| 70 |
-
| **NERGAL
|
| 71 |
|
| 72 |
-
Zero-shot [GLiNER 2.5](https://huggingface.co/fastino/gliner2.5-multi-v1) is not competitive here, especially on non-phone PII (7/185 whole vs 160 naked / 179 union
|
| 73 |
|
| 74 |
Trained GLiNER, HerBERT-large, and XLM-R-large were also compared on this split (plot above). GLiNER’s 0.50 coverage lead is the precision collapse in that figure; no GLiNER or HerBERT operating point passed the content-preservation gate.
|
| 75 |
|
|
@@ -77,13 +87,15 @@ Trained GLiNER, HerBERT-large, and XLM-R-large were also compared on this split
|
|
| 77 |
|
| 78 |
| Seed | Whole /354 | False chars | New false vs historical union |
|
| 79 |
|---|---:|---:|---:|
|
| 80 |
-
| 202609160 (this
|
| 81 |
| 202609161 | 322 | 134 | 1 |
|
| 82 |
| 202609162 | 316 | 151 | 18 |
|
| 83 |
|
|
|
|
|
|
|
| 84 |
## Load
|
| 85 |
|
| 86 |
-
This repo is the PII island:
|
| 87 |
|
| 88 |
```python
|
| 89 |
from pathlib import Path
|
|
@@ -98,6 +110,6 @@ nergal = Nergal.from_pretrained(root, local_files_only=True)
|
|
| 98 |
masked, counts = nergal.scrub(text)
|
| 99 |
```
|
| 100 |
|
| 101 |
-
`hybrid.json` records threshold 0.95
|
| 102 |
|
| 103 |
Base weights: [`FacebookAI/xlm-roberta-large`](https://huggingface.co/FacebookAI/xlm-roberta-large) revision `c23d21b0620b635a76227c604d44e43a9f0ee389` (MIT).
|
|
|
|
| 13 |
- hybrid
|
| 14 |
---
|
| 15 |
|
| 16 |
+
# NERGAL 1.0.1
|
| 17 |
|
| 18 |
**Named Entity Recognition with Grounded Additive Labels**
|
| 19 |
|
|
|
|
| 23 |
|
| 24 |
Python rules do the identifiers they can prove. A transformer NER head adds phone and other PII spans the regex misses. The cleaner **unions** the two on the original text, then replaces hits with `[Telefon]` or `[PII]`.
|
| 25 |
|
| 26 |
+
- **Version:** `1.0.1` (`hybrid.json`, `CHANGELOG.md`)
|
| 27 |
+
- **Ground:** `scrub_pii` regex (SHA256 `4dcc441c…`)
|
| 28 |
- **Additive labels:** XLM-RoBERTa-large token classifier, BIO tags `phone` / `pii`, threshold 0.95
|
| 29 |
- **This snapshot:** seed `202609160`, **epoch 5** of a seven-epoch schedule
|
| 30 |
|
| 31 |
+
## Versions
|
| 32 |
+
|
| 33 |
+
841-dev, union at 0.95, 354 gold spans. Same weights and API is a patch; new capability is minor; API, threshold, or weight recipe is major. A release that changes these numbers updates `hybrid.json` `eval` and this table.
|
| 34 |
+
|
| 35 |
+
| Version | Whole /354 | Residual | Rules FP | Union FP | Char P | Char R | What changed |
|
| 36 |
+
|---|---:|---:|---:|---:|---:|---:|---|
|
| 37 |
+
| 1.0.0 | 323 | 25 | 133 | 133 | 97.76% | 95.95% | First Hub snapshot |
|
| 38 |
+
| **1.0.1** | **323** | **25** | **98** | **123** | **97.93%** | **95.95%** | Prefix-only glued-email trim |
|
| 39 |
+
|
| 40 |
## 841-dev
|
| 41 |
|
| 42 |
Tables use one development split: 841 passages, 215 with gold PII, **354 spans** (169 phone, 185 other PII). It is the `dev` side of a 4,500-passage labelled tranche (3,655 train / 841 dev). Sources match Dynaword (EUR-Lex, HPLT, Wikipedia, parliamentary and government text, plus smaller news/literary slices). Labels mix unchanged silver with human review.
|
|
|
|
| 67 |
|
| 68 |
## Compared with other systems
|
| 69 |
|
| 70 |
+
Same 841-dev split, threshold **0.95**. **Naked** is the transformer alone. **∪ regex** is that model unioned with the current rules. Character scores are gold vs masked characters.
|
| 71 |
|
| 72 |
| System | Mode | Whole /354 | Residual | False chars | Char P | Char R |
|
| 73 |
|---|---|---:|---:|---:|---:|---:|
|
| 74 |
+
| Regex (`scrub_pii`) | rules | 245 | 73 | 98 | 98.04% | 81.01% |
|
| 75 |
| GLiNER 2.5-multi zero-shot | naked | 81 | 188 | 970 | 56.98% | 21.22% |
|
| 76 |
| GLiNER 2.5-multi zero-shot | ∪ regex | 262 | 63 | 1,103 | 82.36% | 85.02% |
|
| 77 |
| Historical GLiNER email12 | naked | 249 | 70 | 10 | 99.81% | 85.39% |
|
| 78 |
| Historical GLiNER email12 | ∪ regex | 289 | 51 | 143 | 97.54% | 93.48% |
|
| 79 |
| XLM-R epoch 5 | naked | 298 | 42 | 57 | 98.96% | 89.38% |
|
| 80 |
+
| **NERGAL 1.0.1** | **∪ regex** | **323** | **25** | **123** | **97.93%** | **95.95%** |
|
| 81 |
|
| 82 |
+
Zero-shot [GLiNER 2.5](https://huggingface.co/fastino/gliner2.5-multi-v1) is not competitive here, especially on non-phone PII (7/185 whole vs 160 naked / 179 union). Fine-tuned historical GLiNER is the precise naked baseline (10 false characters) and still trails XLM-R on coverage. NERGAL is XLM-R epoch 5 plus the regex: 144/169 phone, 179/185 other PII. Exact-span precision 87.50%, recall 88.98%, F1 88.24%.
|
| 83 |
|
| 84 |
Trained GLiNER, HerBERT-large, and XLM-R-large were also compared on this split (plot above). GLiNER’s 0.50 coverage lead is the precision collapse in that figure; no GLiNER or HerBERT operating point passed the content-preservation gate.
|
| 85 |
|
|
|
|
| 87 |
|
| 88 |
| Seed | Whole /354 | False chars | New false vs historical union |
|
| 89 |
|---|---:|---:|---:|
|
| 90 |
+
| 202609160 (this snapshot) | 323 | 123 | 0 |
|
| 91 |
| 202609161 | 322 | 134 | 1 |
|
| 92 |
| 202609162 | 316 | 151 | 18 |
|
| 93 |
|
| 94 |
+
Seed 160 is the published weights. 161 and 162 were confirmation runs of the same recipe.
|
| 95 |
+
|
| 96 |
## Load
|
| 97 |
|
| 98 |
+
This repo is the PII island: `scrub_pii.py` plus `nergal.py`. `pipeline("token-classification")` will not match. Regex runs on the original text, the model adds spans at 0.95, then the two are unioned and replaced with `[Telefon]` / `[PII]`.
|
| 99 |
|
| 100 |
```python
|
| 101 |
from pathlib import Path
|
|
|
|
| 110 |
masked, counts = nergal.scrub(text)
|
| 111 |
```
|
| 112 |
|
| 113 |
+
`hybrid.json` records version `1.0.1`, threshold 0.95, gap ids `250002` / `250003`, and the 841-dev `eval` block. `test_nergal.py` is synthetic (no corpus text). From this snapshot: `python -m unittest test_nergal`.
|
| 114 |
|
| 115 |
Base weights: [`FacebookAI/xlm-roberta-large`](https://huggingface.co/FacebookAI/xlm-roberta-large) revision `c23d21b0620b635a76227c604d44e43a9f0ee389` (MIT).
|
hybrid.json
CHANGED
|
@@ -1,11 +1,29 @@
|
|
| 1 |
{
|
| 2 |
"full_name": "Named Entity Recognition with Grounded Additive Labels",
|
| 3 |
"hub_id": "SlayerLab/NERGAL",
|
|
|
|
| 4 |
"mode": "rules_union",
|
| 5 |
"epoch": 5,
|
| 6 |
"seed": 202609160,
|
| 7 |
"threshold": 0.95,
|
| 8 |
-
"rules_sha256": "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
"weights_sha256": "063ee5f9782c1b4d99e838be5a836328718e1372e213d5b87098b371b5c162af",
|
| 10 |
"gaps": [
|
| 11 |
"[PII_SPACE]",
|
|
|
|
| 1 |
{
|
| 2 |
"full_name": "Named Entity Recognition with Grounded Additive Labels",
|
| 3 |
"hub_id": "SlayerLab/NERGAL",
|
| 4 |
+
"version": "1.0.1",
|
| 5 |
"mode": "rules_union",
|
| 6 |
"epoch": 5,
|
| 7 |
"seed": 202609160,
|
| 8 |
"threshold": 0.95,
|
| 9 |
+
"rules_sha256": "4dcc441c39b653b1e8360928c9e36f6299c1f17e81e7566dd2d252cc5dbbb03c",
|
| 10 |
+
"eval": {
|
| 11 |
+
"split": "841-dev",
|
| 12 |
+
"gold_entities": 354,
|
| 13 |
+
"whole_entities": 323,
|
| 14 |
+
"residual_passages": 25,
|
| 15 |
+
"union_fp": 123,
|
| 16 |
+
"rules_fp": 98,
|
| 17 |
+
"character_precision": 0.9793,
|
| 18 |
+
"character_recall": 0.9595,
|
| 19 |
+
"exact_precision": 0.875,
|
| 20 |
+
"exact_recall": 0.8898,
|
| 21 |
+
"exact_f1": 0.8824,
|
| 22 |
+
"phone_whole": 144,
|
| 23 |
+
"phone_gold": 169,
|
| 24 |
+
"pii_whole": 179,
|
| 25 |
+
"pii_gold": 185
|
| 26 |
+
},
|
| 27 |
"weights_sha256": "063ee5f9782c1b4d99e838be5a836328718e1372e213d5b87098b371b5c162af",
|
| 28 |
"gaps": [
|
| 29 |
"[PII_SPACE]",
|
nergal.py
CHANGED
|
@@ -17,12 +17,13 @@ import scrub_pii
|
|
| 17 |
from scrub_pii import PHONE_TAG, PII_TAG
|
| 18 |
|
| 19 |
HUB_ID = 'SlayerLab/NERGAL'
|
|
|
|
| 20 |
GAPS = ['[PII_SPACE]', '[PII_BREAK]']
|
| 21 |
GAP_IDS = [250002, 250003]
|
| 22 |
BIO_LABELS = ['O', 'B-phone', 'I-phone', 'B-pii', 'I-pii']
|
| 23 |
LABELS = ['phone', 'pii']
|
| 24 |
THRESHOLD = 0.95
|
| 25 |
-
RULES_SHA = '
|
| 26 |
|
| 27 |
|
| 28 |
def sha(path):
|
|
|
|
| 17 |
from scrub_pii import PHONE_TAG, PII_TAG
|
| 18 |
|
| 19 |
HUB_ID = 'SlayerLab/NERGAL'
|
| 20 |
+
VERSION = '1.0.1'
|
| 21 |
GAPS = ['[PII_SPACE]', '[PII_BREAK]']
|
| 22 |
GAP_IDS = [250002, 250003]
|
| 23 |
BIO_LABELS = ['O', 'B-phone', 'I-phone', 'B-pii', 'I-pii']
|
| 24 |
LABELS = ['phone', 'pii']
|
| 25 |
THRESHOLD = 0.95
|
| 26 |
+
RULES_SHA = '4dcc441c39b653b1e8360928c9e36f6299c1f17e81e7566dd2d252cc5dbbb03c'
|
| 27 |
|
| 28 |
|
| 29 |
def sha(path):
|
scrub_pii.py
CHANGED
|
@@ -11,7 +11,9 @@ valid check digit. Passport
|
|
| 11 |
variants include one-letter and diplomatic IDs. Foreign IBANs use country
|
| 12 |
lengths and MOD-97. ePUAP paths require a label; e-Doreczenia uses its AE:PL form.
|
| 13 |
Wrapped email domains, local parts hyphenated across one line break and small
|
| 14 |
-
extraction gaps around @/hyphens are supported.
|
|
|
|
|
|
|
| 15 |
Explicit phone extensions and terminal suffix ranges are included; room numbers are not. Labelled numeric
|
| 16 |
PINs (including URL pin= values) map to [PII] before phone detection.
|
| 17 |
Contact/helpline headings cover consecutive descriptive phone-list entries;
|
|
@@ -509,9 +511,51 @@ def _phone_ok(raw: str, short: bool = False) -> bool:
|
|
| 509 |
or (short and 9 <= len(d) <= 12 and d.startswith('0')))
|
| 510 |
|
| 511 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 512 |
def _replace_checked(text: str, pattern: re.Pattern, tag: str, ok,
|
| 513 |
label: re.Pattern | None = None, *, mask=_tag) -> tuple[str, int]:
|
| 514 |
n = 0
|
|
|
|
| 515 |
|
| 516 |
def _sub(m):
|
| 517 |
nonlocal n
|
|
@@ -522,8 +566,9 @@ def _replace_checked(text: str, pattern: re.Pattern, tag: str, ok,
|
|
| 522 |
if not ok(m.group(0)) or (pattern not in (_EMAIL_RE, _EMAIL_WRAP_RE, _EMAIL_FRAGMENT_RE)
|
| 523 |
and _is_amount(text, m.start(), m.end())):
|
| 524 |
return m.group(0)
|
|
|
|
| 525 |
n += 1
|
| 526 |
-
return
|
| 527 |
|
| 528 |
return pattern.sub(_sub, text), n
|
| 529 |
|
|
|
|
| 11 |
variants include one-letter and diplomatic IDs. Foreign IBANs use country
|
| 12 |
lengths and MOD-97. ePUAP paths require a label; e-Doreczenia uses its AE:PL form.
|
| 13 |
Wrapped email domains, local parts hyphenated across one line break and small
|
| 14 |
+
extraction gaps around @/hyphens are supported. A title-case alphabetic prefix
|
| 15 |
+
of 5–11 letters is dropped from the redaction when the remainder is already a
|
| 16 |
+
complete lowercase-local email and the same passage has at least two such glues.
|
| 17 |
Explicit phone extensions and terminal suffix ranges are included; room numbers are not. Labelled numeric
|
| 18 |
PINs (including URL pin= values) map to [PII] before phone detection.
|
| 19 |
Contact/helpline headings cover consecutive descriptive phone-list entries;
|
|
|
|
| 511 |
or (short and 9 <= len(d) <= 12 and d.startswith('0')))
|
| 512 |
|
| 513 |
|
| 514 |
+
def _trim_glued_email_prefix(text: str, start: int, end: int) -> int:
|
| 515 |
+
"""Drop a title-case letter prefix when the remainder is already a complete email.
|
| 516 |
+
|
| 517 |
+
Prefix-only: do not touch dotted locals or suffix glue. Leftmost 5–11 letters.
|
| 518 |
+
Remainder local must be lowercase letters, length at least 5.
|
| 519 |
+
"""
|
| 520 |
+
frag = text[start:end]
|
| 521 |
+
at = frag.find('@')
|
| 522 |
+
if at < 0:
|
| 523 |
+
return start
|
| 524 |
+
loc = frag[:at]
|
| 525 |
+
if '.' in loc or '-' in loc or sum(c.isupper() for c in loc) != 1:
|
| 526 |
+
return start
|
| 527 |
+
for i in range(5, min(12, at)):
|
| 528 |
+
prefix, rest = frag[:i], frag[i:]
|
| 529 |
+
if not prefix.isalpha() or not (prefix[0].isupper() and prefix[1:].islower()):
|
| 530 |
+
continue
|
| 531 |
+
r_at = rest.find('@')
|
| 532 |
+
rloc = rest[:r_at] if r_at >= 0 else ''
|
| 533 |
+
if r_at < 5 or not rloc.isalpha() or not rloc.islower():
|
| 534 |
+
continue
|
| 535 |
+
if _EMAIL_RE.fullmatch(rest):
|
| 536 |
+
return start + i
|
| 537 |
+
return start
|
| 538 |
+
|
| 539 |
+
|
| 540 |
+
def _glued_email_trim_starts(text: str) -> dict[int, int]:
|
| 541 |
+
"""Map match starts to trimmed starts when a passage has two or more glues.
|
| 542 |
+
|
| 543 |
+
A lone title-case local is a real mailbox (Lostandfound@…). Two or more
|
| 544 |
+
CELEX-shaped prefix+mailbox tokens in one passage are column glue.
|
| 545 |
+
"""
|
| 546 |
+
starts = {}
|
| 547 |
+
for pattern in (_EMAIL_WRAP_RE, _EMAIL_RE):
|
| 548 |
+
for m in pattern.finditer(text):
|
| 549 |
+
new = _trim_glued_email_prefix(text, m.start(), m.end())
|
| 550 |
+
if new != m.start():
|
| 551 |
+
starts.setdefault(m.start(), new)
|
| 552 |
+
return starts if len(starts) >= 2 else {}
|
| 553 |
+
|
| 554 |
+
|
| 555 |
def _replace_checked(text: str, pattern: re.Pattern, tag: str, ok,
|
| 556 |
label: re.Pattern | None = None, *, mask=_tag) -> tuple[str, int]:
|
| 557 |
n = 0
|
| 558 |
+
trims = _glued_email_trim_starts(text) if pattern in (_EMAIL_RE, _EMAIL_WRAP_RE) else {}
|
| 559 |
|
| 560 |
def _sub(m):
|
| 561 |
nonlocal n
|
|
|
|
| 566 |
if not ok(m.group(0)) or (pattern not in (_EMAIL_RE, _EMAIL_WRAP_RE, _EMAIL_FRAGMENT_RE)
|
| 567 |
and _is_amount(text, m.start(), m.end())):
|
| 568 |
return m.group(0)
|
| 569 |
+
start = trims.get(m.start(), m.start())
|
| 570 |
n += 1
|
| 571 |
+
return text[m.start():start] + mask(start, m.end(), tag)
|
| 572 |
|
| 573 |
return pattern.sub(_sub, text), n
|
| 574 |
|
test_nergal.py
CHANGED
|
@@ -5,14 +5,18 @@ import unittest
|
|
| 5 |
from pathlib import Path
|
| 6 |
|
| 7 |
HERE = Path(__file__).resolve().parent
|
| 8 |
-
RULES_SHA = '
|
| 9 |
|
| 10 |
|
| 11 |
class NergalTests(unittest.TestCase):
|
| 12 |
def test_card_and_rules_hash(self):
|
| 13 |
-
from nergal import GAP_IDS, GAPS, HUB_ID, RULES_SHA as PINNED, THRESHOLD
|
| 14 |
card = json.loads((HERE / 'hybrid.json').read_text())
|
| 15 |
self.assertEqual(HUB_ID, 'SlayerLab/NERGAL')
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
self.assertEqual(GAPS, card['gaps'])
|
| 17 |
self.assertEqual(GAP_IDS, card['gap_ids'])
|
| 18 |
self.assertEqual(THRESHOLD, card['threshold'])
|
|
|
|
| 5 |
from pathlib import Path
|
| 6 |
|
| 7 |
HERE = Path(__file__).resolve().parent
|
| 8 |
+
RULES_SHA = '4dcc441c39b653b1e8360928c9e36f6299c1f17e81e7566dd2d252cc5dbbb03c'
|
| 9 |
|
| 10 |
|
| 11 |
class NergalTests(unittest.TestCase):
|
| 12 |
def test_card_and_rules_hash(self):
|
| 13 |
+
from nergal import GAP_IDS, GAPS, HUB_ID, RULES_SHA as PINNED, THRESHOLD, VERSION
|
| 14 |
card = json.loads((HERE / 'hybrid.json').read_text())
|
| 15 |
self.assertEqual(HUB_ID, 'SlayerLab/NERGAL')
|
| 16 |
+
self.assertEqual(VERSION, '1.0.1')
|
| 17 |
+
self.assertEqual(card['version'], VERSION)
|
| 18 |
+
self.assertEqual(card['eval']['union_fp'], 123)
|
| 19 |
+
self.assertEqual(card['eval']['rules_fp'], 98)
|
| 20 |
self.assertEqual(GAPS, card['gaps'])
|
| 21 |
self.assertEqual(GAP_IDS, card['gap_ids'])
|
| 22 |
self.assertEqual(THRESHOLD, card['threshold'])
|