structflo-ner 0.2.2__tar.gz → 0.2.3__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 (51) hide show
  1. {structflo_ner-0.2.2 → structflo_ner-0.2.3}/.github/workflows/ci.yml +10 -3
  2. structflo_ner-0.2.3/PKG-INFO +397 -0
  3. structflo_ner-0.2.3/README.md +384 -0
  4. structflo_ner-0.2.3/coverage.xml +620 -0
  5. structflo_ner-0.2.3/images/fast-viz.png +0 -0
  6. structflo_ner-0.2.3/images/local-gen-pandas.png +0 -0
  7. structflo_ner-0.2.3/images/local-gen-viz.png +0 -0
  8. structflo_ner-0.2.3/images/local-tb-viz.png +0 -0
  9. structflo_ner-0.2.3/images/struct-flo-ner.png +0 -0
  10. structflo_ner-0.2.3/notebooks/01_quickstart.ipynb +844 -0
  11. structflo_ner-0.2.3/notebooks/02_fast_ner.ipynb +686 -0
  12. {structflo_ner-0.2.2 → structflo_ner-0.2.3}/pyproject.toml +3 -1
  13. {structflo_ner-0.2.2 → structflo_ner-0.2.3}/structflo/ner/__init__.py +4 -7
  14. {structflo_ner-0.2.2 → structflo_ner-0.2.3}/structflo/ner/_display.py +1 -0
  15. {structflo_ner-0.2.2 → structflo_ner-0.2.3}/structflo/ner/_entities.py +10 -0
  16. {structflo_ner-0.2.2 → structflo_ner-0.2.3}/structflo/ner/fast/_loader.py +2 -2
  17. {structflo_ner-0.2.2 → structflo_ner-0.2.3}/structflo/ner/fast/_matcher.py +2 -2
  18. {structflo_ner-0.2.2 → structflo_ner-0.2.3}/structflo/ner/fast/extractor.py +1 -0
  19. structflo_ner-0.2.3/structflo/ner/fast/gazetteers/accession_number.yml +47824 -0
  20. {structflo_ner-0.2.2 → structflo_ner-0.2.3}/structflo/ner/fast/gazetteers/disease.yml +2 -0
  21. structflo_ner-0.2.3/structflo/ner/fast/gazetteers/functional_category.yml +46 -0
  22. structflo_ner-0.2.3/structflo/ner/fast/gazetteers/gene_name.yml +37157 -0
  23. {structflo_ner-0.2.2 → structflo_ner-0.2.3}/structflo/ner/fast/gazetteers/product.yml +1 -1
  24. structflo_ner-0.2.3/structflo/ner/fast/gazetteers/screening_method.yml +131 -0
  25. structflo_ner-0.2.3/structflo/ner/fast/gazetteers/strain.yml +42 -0
  26. {structflo_ner-0.2.2 → structflo_ner-0.2.3}/tests/test_entities.py +1 -0
  27. {structflo_ner-0.2.2 → structflo_ner-0.2.3}/tests/test_fast.py +1 -4
  28. {structflo_ner-0.2.2 → structflo_ner-0.2.3}/uv.lock +139 -7
  29. structflo_ner-0.2.2/PKG-INFO +0 -140
  30. structflo_ner-0.2.2/README.md +0 -126
  31. structflo_ner-0.2.2/notebooks/01_quickstart.ipynb +0 -262
  32. structflo_ner-0.2.2/notebooks/02_fast_ner.ipynb +0 -416
  33. structflo_ner-0.2.2/structflo/ner/fast/gazetteers/accession_number.yml +0 -18
  34. structflo_ner-0.2.2/structflo/ner/fast/gazetteers/functional_category.yml +0 -27
  35. structflo_ner-0.2.2/structflo/ner/fast/gazetteers/gene_name.yml +0 -75
  36. structflo_ner-0.2.2/structflo/ner/fast/gazetteers/screening_method.yml +0 -38
  37. {structflo_ner-0.2.2 → structflo_ner-0.2.3}/.github/workflows/publish.yml +0 -0
  38. {structflo_ner-0.2.2 → structflo_ner-0.2.3}/.gitignore +0 -0
  39. {structflo_ner-0.2.2 → structflo_ner-0.2.3}/Makefile +0 -0
  40. {structflo_ner-0.2.2 → structflo_ner-0.2.3}/structflo/ner/_examples.py +0 -0
  41. {structflo_ner-0.2.2 → structflo_ner-0.2.3}/structflo/ner/_mapping.py +0 -0
  42. {structflo_ner-0.2.2 → structflo_ner-0.2.3}/structflo/ner/_prompts.py +0 -0
  43. {structflo_ner-0.2.2 → structflo_ner-0.2.3}/structflo/ner/extractor.py +0 -0
  44. {structflo_ner-0.2.2 → structflo_ner-0.2.3}/structflo/ner/fast/README.md +0 -0
  45. {structflo_ner-0.2.2 → structflo_ner-0.2.3}/structflo/ner/fast/__init__.py +0 -0
  46. {structflo_ner-0.2.2 → structflo_ner-0.2.3}/structflo/ner/fast/_normalize.py +0 -0
  47. {structflo_ner-0.2.2 → structflo_ner-0.2.3}/structflo/ner/fast/gazetteers/compound_name.yml +0 -0
  48. {structflo_ner-0.2.2 → structflo_ner-0.2.3}/structflo/ner/fast/gazetteers/target.yml +0 -0
  49. {structflo_ner-0.2.2 → structflo_ner-0.2.3}/structflo/ner/profiles.py +0 -0
  50. {structflo_ner-0.2.2 → structflo_ner-0.2.3}/tests/__init__.py +0 -0
  51. {structflo_ner-0.2.2 → structflo_ner-0.2.3}/tests/test_extractor.py +0 -0
@@ -19,7 +19,7 @@ jobs:
19
19
  run: uv python install 3.12
20
20
 
21
21
  - name: Install dependencies
22
- run: uv sync --dev --extra fast --extra dataframe
22
+ run: uv sync --dev --extra dataframe
23
23
 
24
24
  - name: Lint
25
25
  run: uv run ruff check structflo/ tests/
@@ -27,5 +27,12 @@ jobs:
27
27
  - name: Format
28
28
  run: uv run ruff format --check structflo/ tests/
29
29
 
30
- - name: Test
31
- run: uv run pytest -q
30
+ - name: Test with coverage
31
+ run: uv run pytest -q --cov=structflo --cov-report=xml
32
+
33
+ - name: Upload coverage to Codecov
34
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
35
+ uses: codecov/codecov-action@v4
36
+ with:
37
+ files: coverage.xml
38
+ token: ${{ secrets.CODECOV_TOKEN }}
@@ -0,0 +1,397 @@
1
+ Metadata-Version: 2.4
2
+ Name: structflo-ner
3
+ Version: 0.2.3
4
+ Summary: Drug discovery NER wrapper around LangExtract — zero-config entity extraction for chemistry and biology.
5
+ License: Apache-2.0
6
+ Requires-Python: >=3.10
7
+ Requires-Dist: langextract>=1.1.1
8
+ Requires-Dist: pyyaml>=6.0
9
+ Requires-Dist: rapidfuzz>=3.0
10
+ Provides-Extra: dataframe
11
+ Requires-Dist: pandas>=1.5; extra == 'dataframe'
12
+ Description-Content-Type: text/markdown
13
+
14
+
15
+ <h1 align="center">structflo.ner</h1>
16
+ <p align="center">
17
+ <img src="./images/struct-flo-ner.png" alt="structflo.ner logo" width="600">
18
+ </p>
19
+
20
+ <p align="center">
21
+ <a href="https://pepy.tech/projects/structflo-ner"><img src="https://static.pepy.tech/personalized-badge/structflo-ner?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads" alt="PyPI Downloads"></a>
22
+ <a href="https://github.com/structflo/structflo-ner/actions"><img src="https://img.shields.io/github/actions/workflow/status/structflo/structflo-ner/ci.yml?label=tests" alt="Tests"></a>
23
+ <a href="https://codecov.io/gh/structflo/structflo-ner"><img src="https://codecov.io/gh/structflo/structflo-ner/branch/main/graph/badge.svg" alt="Coverage"></a>
24
+ <a href="https://github.com/structflo/structflo-ner/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-Apache%202.0-green.svg" alt="License"></a>
25
+ <a href="https://www.linkedin.com/in/sidxz/"><img src="https://img.shields.io/badge/LinkedIn-blue?logo=linkedin&logoColor=white" alt="LinkedIn"></a>
26
+ <a href="https://github.com/sidxz/"><img src="https://img.shields.io/badge/GitHub-black?logo=github&logoColor=white" alt="GitHub"></a>
27
+ </p>
28
+
29
+ <p align="center">
30
+ Zero-config Named Entity Recognition for drug discovery, chemistry, and biological sciences.
31
+ </p>
32
+
33
+
34
+
35
+ <p align="center">
36
+ <a href="#installation">Installation</a> &bull;
37
+ <a href="#llm-powered-extraction">LLM Extraction</a> &bull;
38
+ <a href="#fast-dictionary-based-ner">Fast NER</a> &bull;
39
+ <a href="#profiles">Profiles</a> &bull;
40
+ <a href="#visualization">Visualization</a> &bull;
41
+ <a href="#notebooks">Notebooks</a>
42
+ </p>
43
+
44
+ ---
45
+
46
+ **structflo.ner** is a lightweight NER library specialized for pharmaceutical and biological sciences. It uses [LangExtract](https://github.com/langextract/langextract) and fuzzy based tools to deliver **zero-configuration** entity extraction.
47
+
48
+ It ships with two extraction engines:
49
+
50
+ | | `NERExtractor` | `FastNERExtractor` |
51
+ | ----------------- | ----------------------------- | ---------------------------------- |
52
+ | Approach | LLM-powered (Gemini, Ollama) | Dictionary-based (YAML gazetteers) |
53
+ | Speed | ~10-60s per abstract | ~0.4-1s per abstract |
54
+ | Novel entities | Discovers new entities | Known terms only |
55
+ | Context awareness | Full contextual understanding | String matching (exact + fuzzy) |
56
+ | Cost | API costs or local GPU | Free (no API calls) |
57
+ | Setup | API key or Ollama | Zero config |
58
+ | Output format | `NERResult` | `NERResult` (identical) |
59
+
60
+ ## Installation
61
+
62
+ ```bash
63
+ pip install structflo-ner
64
+ ```
65
+
66
+ ```bash
67
+ # or with uv
68
+ uv add structflo-ner
69
+ ```
70
+
71
+ Install optional extras as needed:
72
+
73
+ ```bash
74
+ pip install "structflo-ner[dataframe]" # pandas DataFrame support
75
+ pip install "structflo-ner[fast]" # fast dictionary-based NER (rapidfuzz)
76
+ ```
77
+
78
+ ## LLM-Powered Extraction
79
+
80
+ ### Cloud model (Gemini)
81
+
82
+ The default model is `gemini-2.5-flash`. Pass your API key or set the `GEMINI_API_KEY` environment variable.
83
+
84
+ ```python
85
+ from structflo.ner import NERExtractor
86
+
87
+ extractor = NERExtractor(api_key="YOUR_GEMINI_KEY")
88
+
89
+ result = extractor.extract(
90
+ "Gefitinib (ZD1839) is a first-generation EGFR tyrosine kinase inhibitor "
91
+ "with IC50 = 0.033 µM, approved for non-small cell lung cancer (NSCLC). "
92
+ "Its SMILES is COc1cc2ncnc(Nc3ccc(F)c(Cl)c3)c2cc1OCCCN1CCOCC1."
93
+ )
94
+ ```
95
+
96
+ ### Local models via Ollama
97
+
98
+ Run extraction entirely on your own hardware — no API key needed:
99
+
100
+ ```python
101
+ extractor = NERExtractor(
102
+ model_id="qwen2.5:72b",
103
+ model_url="http://localhost:11434",
104
+ )
105
+ text = ("Gefitinib (ZD1839) is a first-generation EGFR inhibitor with IC50 = 0.033 µM approved for NSCLC."
106
+ "Its SMILES is COc1cc2ncnc(Nc3ccc(F)c(Cl)c3)c2cc1OCCCN1CCOCC1.")
107
+ result = extractor.extract(text)
108
+ result
109
+ ```
110
+ Any model served by [Ollama](https://ollama.com/) works gemma, llama, mistral, qwen, deepseek, etc.
111
+
112
+
113
+ Render results as color-coded, interactive HTML directly in Jupyter notebooks:
114
+ ![Results](images/local-gen-viz.png)
115
+
116
+
117
+ To get a PANDAS dataframe.
118
+ ```python
119
+ result.to_dataframe()
120
+ ```
121
+ ![Results](images/local-gen-pandas.png)
122
+
123
+
124
+ ### TB specific extractor pass in the profile=TB
125
+ ```python
126
+ from structflo.ner import NERExtractor, TB
127
+
128
+ extractor = NERExtractor(
129
+ model_id="qwen2.5:72b",
130
+ model_url="http://localhost:11434",
131
+ profile=TB,
132
+
133
+ ```
134
+ ```python
135
+ text = (
136
+ "Bedaquiline (TMC207) is a diarylquinoline that inhibits the "
137
+ "mycobacterial ATP synthase subunit c encoded by atpE (Rv1305). "
138
+ "It shows potent activity against Mycobacterium tuberculosis "
139
+ "including MDR-TB and XDR-TB. This compound was identified through "
140
+ "whole-cell screening and targets the energy metabolism pathway."
141
+ )
142
+ result = extractor.extract(text)
143
+ result
144
+ ```
145
+
146
+ ![Results](images/local-tb-viz.png)
147
+
148
+ ```python
149
+ # Flat list of all entities
150
+ for entity in result.all_entities():
151
+ print(f"{entity.entity_type:20s} | {entity.text}")
152
+ ```
153
+ ```
154
+ compound_name | Bedaquiline
155
+ compound_name | TMC207
156
+ target | ATP synthase subunit c
157
+ disease | MDR-TB
158
+ disease | XDR-TB
159
+ accession_number | Rv1305
160
+ functional_category | energy metabolism pathway
161
+ screening_method | whole-cell screening
162
+ ```
163
+
164
+ ### Batch extraction
165
+
166
+ Pass a list of texts to extract from multiple documents at once:
167
+
168
+ ```python
169
+ texts = [
170
+ "Imatinib inhibits BCR-ABL with IC50 = 0.6 µM in CML.",
171
+ "Trastuzumab targets HER2 in breast cancer patients.",
172
+ "Remdesivir (GS-5734) is an antiviral with EC50 = 0.77 µM against SARS-CoV-2.",
173
+ ]
174
+
175
+ results = extractor.extract(texts)
176
+ ```
177
+
178
+ ```
179
+ --- Text 1 ---
180
+ compound_name | Imatinib
181
+ target | BCR-ABL
182
+ disease | CML
183
+ bioactivity | IC50 = 0.6 µM
184
+
185
+ --- Text 2 ---
186
+ compound_name | Trastuzumab
187
+ target | HER2
188
+ disease | breast cancer
189
+
190
+ --- Text 3 ---
191
+ compound_name | Remdesivir
192
+ compound_name | GS-5734
193
+ disease | SARS-CoV-2
194
+ bioactivity | EC50 = 0.77 µM
195
+ ```
196
+
197
+ ## Fast Dictionary-Based NER (Mode 2)
198
+
199
+ `FastNERExtractor` uses curated YAML gazetteers with a three-phase matching strategy for deterministic, high-speed extraction when LLMs are not available.
200
+ These run extremely fast, however they are fuzzy based matches to predefined patterns and so it does not understands context.
201
+ ```python
202
+ from structflo.ner.fast import FastNERExtractor
203
+
204
+ fast = FastNERExtractor()
205
+
206
+ text = (
207
+ "Bedaquiline (TMC207) is a diarylquinoline that inhibits the "
208
+ "mycobacterial ATP synthase subunit c encoded by atpE (Rv1305). "
209
+ "It shows potent activity against Mycobacterium tuberculosis "
210
+ "including MDR-TB and XDR-TB. This compound was identified through "
211
+ "whole-cell screening and targets the energy metabolism pathway."
212
+ )
213
+
214
+ result = fast.extract(text)
215
+ result
216
+ ```
217
+
218
+ ![Results](images/fast-viz.png)
219
+
220
+ ### How matching works
221
+
222
+ | Phase | Method | What it catches |
223
+ | ----- | ------------------ | --------------------------------------------------------------------------------------------------------- |
224
+ | 1 | **Exact match** | Case-sensitive and normalized dictionary lookups with word-boundary enforcement |
225
+ | 1b | **Regex patterns** | Auto-derived patterns from accession number seeds (Rv tags, UniProt, PDB, etc.) |
226
+ | 2 | **Fuzzy match** | Typos and minor variants via [rapidfuzz](https://github.com/rapidfuzz/rapidfuzz) (configurable threshold) |
227
+
228
+ ```python
229
+ # Fuzzy matching catches typos
230
+ result = fast.extract("Bedaquilne showed activity against TB")
231
+ # "Bedaquilne" -> canonical: "Bedaquiline" (method: fuzzy)
232
+
233
+ # Disable fuzzy matching for strict mode
234
+ strict = FastNERExtractor(fuzzy_threshold=0)
235
+ ```
236
+
237
+ ### Built-in gazetteers
238
+
239
+ The fast extractor ships with curated gazetteers for TB drug discovery:
240
+
241
+ | Gazetteer | Examples |
242
+ | --------------------- | ---------------------------------------------- |
243
+ | `accession_number` | Rv1305, B586_RS00005 |
244
+ | `gene_name` | atpE, InhA, DprE1 |
245
+ | `screening_method` | whole-cell screening, fragment-based screening |
246
+ | `target` | InhA, DprE1, MmpL3 |
247
+ | `compound_name` | Bedaquiline, Delamanid, Pretomanid |
248
+ | `functional_category` | DNA replication, cell wall biosynthesis |
249
+ | `strain` | M. tuberculosis H37Rv |
250
+ | `product` | enoyl-ACP reductase, ATP synthase subunit c |
251
+ | `disease` | TB, MDR-TB, XDR-TB |
252
+
253
+ ### Custom gazetteers
254
+
255
+ Extend the built-in dictionaries with your own terms:
256
+
257
+ ```python
258
+ custom = FastNERExtractor(
259
+ extra_gazetteers={
260
+ "target": ["MyNovelTarget", "KinaseX"],
261
+ "compound_name": ["CompoundABC"],
262
+ }
263
+ )
264
+ ```
265
+
266
+ Or drop a new YAML file into the gazetteers directory — the filename (without `.yml`) maps to an entity type.
267
+
268
+ ### Performance
269
+
270
+ ```
271
+ Single abstract: ~393 ms
272
+ 8 abstracts: ~862 ms
273
+ ```
274
+
275
+ ## Profiles
276
+
277
+ Profiles control which entity types are extracted. Use them to focus the model on specific categories.
278
+
279
+ ### Built-in profiles
280
+
281
+ | Profile | Entity classes |
282
+ | ---------------- | --------------------------------------------------------------------------------------------------------------- |
283
+ | `FULL` (default) | compounds, targets, diseases, bioactivities, assays, mechanisms |
284
+ | `CHEMISTRY` | compound names, SMILES, CAS numbers, molecular formulas |
285
+ | `BIOLOGY` | targets, gene names, protein names |
286
+ | `BIOACTIVITY` | bioactivity measurements, assays |
287
+ | `DISEASE` | diseases and clinical indications |
288
+ | `TB` | TB drug discovery (compounds, targets, diseases, accessions, strains, screening methods, functional categories) |
289
+
290
+ ```python
291
+ from structflo.ner import NERExtractor, CHEMISTRY
292
+
293
+ extractor = NERExtractor(api_key="YOUR_GEMINI_KEY")
294
+ result = extractor.extract(text, profile=CHEMISTRY)
295
+ ```
296
+
297
+ ### Merging profiles
298
+
299
+ Combine multiple profiles for broader extraction:
300
+
301
+ ```python
302
+ from structflo.ner import CHEMISTRY, BIOLOGY
303
+
304
+ combined = CHEMISTRY.merge(BIOLOGY)
305
+ result = extractor.extract(text, profile=combined)
306
+ # Profile: chemistry+biology
307
+ # Entity classes: compound_name, smiles, cas_number, molecular_formula, target, gene_name, protein_name
308
+ ```
309
+
310
+ ### Custom profiles
311
+
312
+ Define your own extraction schema:
313
+
314
+ ```python
315
+ from structflo.ner import NERExtractor, EntityProfile
316
+
317
+ my_profile = EntityProfile(
318
+ name="kinase_inhibitors",
319
+ entity_classes=["compound_name", "smiles", "target", "bioactivity"],
320
+ prompt="Extract kinase inhibitor names, SMILES, targets, and potency values.",
321
+ examples=my_examples,
322
+ )
323
+ result = extractor.extract(text, profile=my_profile)
324
+ ```
325
+
326
+ ## Working with Results
327
+
328
+ Both extractors return identical `NERResult` objects:
329
+
330
+ ```python
331
+ # Typed entity lists
332
+ result.compounds # [ChemicalEntity(...)]
333
+ result.targets # [TargetEntity(...)]
334
+ result.diseases # [DiseaseEntity(...)]
335
+ result.bioactivities # [BioactivityEntity(...)]
336
+ result.assays # [...]
337
+ result.mechanisms # [...]
338
+ result.accessions # [AccessionEntity(...)]
339
+
340
+ # Flat list of all entities
341
+ result.all_entities()
342
+
343
+ # Export to pandas DataFrame
344
+ df = result.to_dataframe()
345
+
346
+ # Serialize to dict (JSON-friendly)
347
+ result.to_dict()
348
+ ```
349
+
350
+
351
+ ## Notebooks
352
+
353
+ Explore worked examples in the [`notebooks/`](notebooks/) directory:
354
+
355
+ | Notebook | Description |
356
+ | ---------------------------------------------------- | ------------------------------------------------------------------------------- |
357
+ | [01_quickstart.ipynb](notebooks/01_quickstart.ipynb) | End-to-end extraction with cloud and local models, profiles, batch extraction |
358
+ | [02_fast_ner.ipynb](notebooks/02_fast_ner.ipynb) | Fast dictionary-based NER — matching strategies, custom gazetteers, performance |
359
+
360
+ ## Contributing
361
+
362
+ Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
363
+
364
+ ```bash
365
+ # clone and install dev dependencies
366
+ git clone https://github.com/structflo/structflo-ner.git
367
+ cd structflo-ner
368
+ pip install -e ".[dataframe]" --group dev
369
+
370
+ # run tests
371
+ pytest
372
+
373
+ # lint
374
+ ruff check .
375
+ ruff format .
376
+ ```
377
+
378
+ ## Citation
379
+
380
+ If you use structflo.ner in your research, please cite:
381
+
382
+ <details>
383
+ <summary>BibTeX</summary>
384
+
385
+ ```bibtex
386
+ @software{structflo_ner,
387
+ title = {structflo.ner: Zero-config NER for Drug Discovery},
388
+ url = {https://github.com/structflo/structflo-ner},
389
+ year = {2026}
390
+ }
391
+ ```
392
+
393
+ </details>
394
+
395
+ ## License
396
+
397
+ This project is licensed under the [Apache License 2.0](LICENSE).