Skip to content

Commit 8bdd56e

Browse files
authored
fix: remove runtime dependency installation (#136)
* fix: remove runtime dependency installation * fix: preserve explicit spaCy model behavior
1 parent 76cc5ef commit 8bdd56e

13 files changed

Lines changed: 214 additions & 130 deletions

File tree

.github/workflows/ci.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,14 @@ jobs:
6060
if: matrix.install-profile == 'nlp'
6161
run: |
6262
pip install -e ".[test,cli,nlp]" -r requirements-test.txt
63-
python -m spacy download en_core_web_sm
63+
python -m spacy download en_core_web_lg
6464
6565
- name: Install dependencies (nlp-advanced)
6666
if: matrix.install-profile == 'nlp-advanced'
6767
run: |
6868
pip install -e ".[test,cli,nlp,nlp-advanced]" -r requirements-test.txt
69-
python -m spacy download en_core_web_sm
69+
python -m spacy download en_core_web_lg
70+
datafog download-model urchade/gliner_multi_pii-v1 --engine gliner
7071
7172
- name: Run tests (core)
7273
if: matrix.install-profile == 'core'

.github/workflows/release.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,8 @@ jobs:
128128
python -m pip install --upgrade pip
129129
pip install -e ".[all,test]"
130130
pip install -r requirements-test.txt
131-
pip install https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.7.1/en_core_web_sm-3.7.1.tar.gz
131+
python -m spacy download en_core_web_lg
132+
datafog download-model urchade/gliner_multi_pii-v1 --engine gliner
132133
133134
- name: Run tests with segfault protection
134135
run: |

datafog/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ def download_model(
181181
Download a model for specified engine.
182182
183183
Examples:
184-
spaCy: datafog download-model en_core_web_sm --engine spacy
184+
spaCy: datafog download-model en_core_web_lg --engine spacy
185185
GLiNER: datafog download-model urchade/gliner_multi_pii-v1 --engine gliner
186186
"""
187187
if engine == "spacy":

datafog/models/spacy_nlp.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
from .annotator import AnnotationResult, AnnotatorRequest
1515

16+
DEFAULT_SPACY_MODEL = "en_core_web_lg"
17+
1618

1719
class SpacyAnnotator:
1820
"""
@@ -22,14 +24,18 @@ class SpacyAnnotator:
2224
Supports various NLP tasks including entity recognition and model management.
2325
"""
2426

25-
def __init__(self, model_name: str = "en_core_web_lg"):
27+
def __init__(self, model_name: str = DEFAULT_SPACY_MODEL):
2628
self.model_name = model_name
2729
self.nlp = None
2830

2931
def load_model(self):
30-
if not spacy.util.is_package(self.model_name):
31-
spacy.cli.download(self.model_name)
32-
self.nlp = spacy.load(self.model_name)
32+
try:
33+
self.nlp = spacy.load(self.model_name)
34+
except OSError as exc:
35+
raise ImportError(
36+
f"spaCy model {self.model_name!r} is not installed. "
37+
f"Download it explicitly with: datafog download-model {self.model_name} --engine spacy"
38+
) from exc
3339

3440
def annotate_text(self, text: str, language: str = "en") -> List[AnnotationResult]:
3541
if not self.nlp:
@@ -72,6 +78,12 @@ def list_models() -> List[str]:
7278
return spacy.util.get_installed_models()
7379

7480
@staticmethod
75-
def list_entities() -> List[str]:
76-
nlp = spacy.load("en_core_web_lg")
81+
def list_entities(model_name: str = DEFAULT_SPACY_MODEL) -> List[str]:
82+
try:
83+
nlp = spacy.load(model_name)
84+
except OSError as exc:
85+
raise ImportError(
86+
f"spaCy model {model_name!r} is not installed. "
87+
f"Download it explicitly with: datafog download-model {model_name} --engine spacy"
88+
) from exc
7789
return [ent for ent in nlp.pipe_labels["ner"]]

datafog/processing/image_processing/donut_processor.py

Lines changed: 31 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,10 @@
66
from images of documents.
77
"""
88

9-
import importlib
10-
import importlib.util
119
import json
1210
import logging
1311
import os
1412
import re
15-
import subprocess
16-
import sys
1713
from typing import TYPE_CHECKING, Any
1814

1915
from .image_downloader import ImageDownloader
@@ -43,13 +39,12 @@ def __init__(self, model_path="naver-clova-ix/donut-base-finetuned-cord-v2"):
4339
self.model_path = model_path
4440
self.downloader = ImageDownloader()
4541

46-
def ensure_installed(self, package_name):
47-
try:
48-
importlib.import_module(package_name)
49-
except ImportError:
50-
subprocess.check_call(
51-
[sys.executable, "-m", "pip", "install", package_name]
52-
)
42+
@staticmethod
43+
def _missing_dependency_message(package_name: str) -> str:
44+
return (
45+
f"Donut OCR requires {package_name}. "
46+
"Install with: pip install datafog[nlp-advanced,ocr]"
47+
)
5348

5449
def preprocess_image(self, image: "Image.Image") -> Any:
5550
import numpy as np
@@ -86,40 +81,40 @@ async def extract_text_from_image(self, image: "Image.Image") -> str:
8681
"PYTEST_DONUT=yes is set, running actual OCR in test environment"
8782
)
8883

89-
# Only import torch and transformers when actually needed and not in test environment
9084
try:
91-
# Check if torch is available before trying to import it
92-
try:
93-
# Try to find the module without importing it
94-
spec = importlib.util.find_spec("torch")
95-
if spec is None:
96-
# If we're in a test that somehow bypassed the IN_TEST_ENV check,
97-
# still return a mock result instead of failing
98-
logging.warning("torch module not found, returning mock result")
99-
return json.dumps({"text": "Mock OCR text (torch not available)"})
100-
101-
# Ensure dependencies are installed
102-
self.ensure_installed("torch")
103-
self.ensure_installed("transformers")
104-
except ImportError:
105-
# If importlib.util is not available, fall back to direct try/except
106-
pass
107-
108-
# Import dependencies only when needed
10985
try:
11086
import torch
87+
except ImportError as exc:
88+
raise ImportError(self._missing_dependency_message("torch")) from exc
89+
90+
try:
11191
from transformers import DonutProcessor as TransformersDonutProcessor
11292
from transformers import VisionEncoderDecoderModel
11393
except ImportError as e:
114-
logging.warning(f"Import error: {e}, returning mock result")
115-
return json.dumps({"text": f"Mock OCR text (import error: {e})"})
94+
raise ImportError(
95+
self._missing_dependency_message("transformers")
96+
) from e
11697

11798
# Preprocess the image
11899
image_np = self.preprocess_image(image)
119100

120101
# Initialize model components
121-
processor = TransformersDonutProcessor.from_pretrained(self.model_path)
122-
model = VisionEncoderDecoderModel.from_pretrained(self.model_path)
102+
try:
103+
processor = TransformersDonutProcessor.from_pretrained(
104+
self.model_path,
105+
local_files_only=True,
106+
)
107+
model = VisionEncoderDecoderModel.from_pretrained(
108+
self.model_path,
109+
local_files_only=True,
110+
)
111+
except OSError as exc:
112+
raise RuntimeError(
113+
f"Donut model {self.model_path!r} is not available locally. "
114+
"Download it explicitly before using Donut OCR, or pass a local "
115+
"model path."
116+
) from exc
117+
123118
device = "cuda" if torch.cuda.is_available() else "cpu"
124119
model.to(device)
125120
model.eval()
@@ -153,6 +148,8 @@ async def extract_text_from_image(self, image: "Image.Image") -> str:
153148
result = processor.token2json(sequence)
154149
return json.dumps(result)
155150

151+
except (ImportError, RuntimeError):
152+
raise
156153
except Exception as e:
157154
logging.error(f"Error in extract_text_from_image: {e}")
158155
# Return a placeholder in case of error

datafog/processing/spark_processing/pyspark_udfs.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,16 @@
22
PySpark UDFs for PII annotation and related utilities.
33
44
This module provides functions for PII (Personally Identifiable Information) annotation
5-
using SpaCy models in a PySpark environment. It includes utilities for installing
6-
dependencies, creating and broadcasting PII annotator UDFs, and performing PII annotation
7-
on text data.
5+
using SpaCy models in a PySpark environment. It includes utilities for validating
6+
dependencies, creating and broadcasting PII annotator UDFs, and performing PII
7+
annotation on text data.
88
"""
99

1010
import importlib
11-
import subprocess
12-
import sys
1311

1412
PII_ANNOTATION_LABELS = ["DATE_TIME", "LOC", "NRP", "ORG", "PER"]
1513
MAXIMAL_STRING_SIZE = 1000000
14+
DEFAULT_SPACY_MODEL = "en_core_web_lg"
1615

1716

1817
def pii_annotator(text: str, broadcasted_nlp) -> list[list[str]]:
@@ -45,7 +44,7 @@ def pii_annotator(text: str, broadcasted_nlp) -> list[list[str]]:
4544

4645

4746
def broadcast_pii_annotator_udf(
48-
spark_session=None, spacy_model: str = "en_core_web_lg"
47+
spark_session=None, spacy_model: str = DEFAULT_SPACY_MODEL
4948
):
5049
"""Broadcast PII annotator across Spark cluster and create UDF"""
5150
ensure_installed("pyspark")
@@ -69,5 +68,14 @@ def broadcast_pii_annotator_udf(
6968
def ensure_installed(package_name):
7069
try:
7170
importlib.import_module(package_name)
72-
except ImportError:
73-
subprocess.check_call([sys.executable, "-m", "pip", "install", package_name])
71+
except ImportError as exc:
72+
if package_name == "pyspark":
73+
extra = "distributed"
74+
elif package_name == "spacy":
75+
extra = "nlp"
76+
else:
77+
extra = "all"
78+
raise ImportError(
79+
f"{package_name} is required for Spark PII UDF support. "
80+
f"Install with: pip install datafog[{extra}]"
81+
) from exc

datafog/processing/text_processing/gliner_annotator.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,18 @@ def create(
7979

8080
try:
8181
# Load the GLiNER model
82-
model = GLiNER.from_pretrained(model_name)
82+
model = GLiNER.from_pretrained(model_name, local_files_only=True)
8383
logging.info(f"Successfully loaded GLiNER model: {model_name}")
8484

8585
return cls(model=model, entity_types=entity_types, model_name=model_name)
8686

8787
except Exception as e:
8888
logging.error(f"Failed to load GLiNER model {model_name}: {str(e)}")
89-
raise
89+
raise RuntimeError(
90+
f"GLiNER model {model_name!r} is not available locally. "
91+
"Download it explicitly with: "
92+
f"datafog download-model {model_name} --engine gliner"
93+
) from e
9094

9195
def annotate(self, text: str) -> Dict[str, List[str]]:
9296
"""

datafog/processing/text_processing/spacy_pii_annotator.py

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -24,39 +24,34 @@
2424
"WORK_OF_ART",
2525
]
2626
MAXIMAL_STRING_SIZE = 1000000
27+
DEFAULT_SPACY_MODEL = "en_core_web_lg"
2728

2829

2930
class SpacyPIIAnnotator(BaseModel):
3031
model_config = ConfigDict(arbitrary_types_allowed=True)
3132

3233
nlp: Any
34+
model_name: str = DEFAULT_SPACY_MODEL
3335

3436
@classmethod
35-
def create(cls) -> "SpacyPIIAnnotator":
36-
import spacy
37-
37+
def create(cls, model_name: str = DEFAULT_SPACY_MODEL) -> "SpacyPIIAnnotator":
3838
try:
39-
nlp = spacy.load("en_core_web_lg")
40-
except OSError:
41-
import subprocess
42-
import sys
39+
import spacy
40+
except ImportError as exc:
41+
raise ImportError(
42+
"SpaCy engine requires the nlp extra. "
43+
"Install with: pip install datafog[nlp]"
44+
) from exc
4345

44-
interpreter_location = sys.executable
45-
subprocess.run(
46-
[
47-
interpreter_location,
48-
"-m",
49-
"pip",
50-
"install",
51-
"--no-deps",
52-
"--no-cache-dir",
53-
"https://github.com/explosion/spacy-models/releases/download/en_core_web_lg-3.7.1/en_core_web_lg-3.7.1-py3-none-any.whl",
54-
],
55-
check=True,
56-
)
57-
nlp = spacy.load("en_core_web_lg")
46+
try:
47+
nlp = spacy.load(model_name)
48+
except OSError as exc:
49+
raise ImportError(
50+
f"spaCy model {model_name!r} is not installed. "
51+
f"Download it explicitly with: datafog download-model {model_name} --engine spacy"
52+
) from exc
5853

59-
return cls(nlp=nlp)
54+
return cls(nlp=nlp, model_name=model_name)
6055

6156
def annotate(self, text: str) -> Dict[str, List[str]]:
6257
try:

datafog/services/spark_service.py

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,26 @@
11
"""
22
Spark service for data processing and analysis.
33
4-
Provides a wrapper around PySpark functionality, including session creation,
5-
JSON reading, and package management.
4+
Provides a wrapper around PySpark functionality, including session creation and
5+
JSON reading.
66
"""
77

88
import importlib
99
import os
10-
import subprocess
11-
import sys
1210
from typing import List
1311

1412

1513
class SparkService:
1614
"""
1715
Manages Spark operations and dependencies.
1816
19-
Initializes a Spark session, handles imports, and provides methods for
20-
data reading and package installation.
17+
Initializes a Spark session, handles imports, and provides methods for data
18+
reading.
2119
"""
2220

2321
def __init__(self, master=None):
2422
self.master = master
2523

26-
# Ensure pyspark is installed first
2724
self.ensure_installed("pyspark")
2825

2926
# Now import necessary modules after ensuring pyspark is installed
@@ -84,16 +81,8 @@ def read_json(self, path: str) -> List[dict]:
8481
def ensure_installed(self, package_name):
8582
try:
8683
importlib.import_module(package_name)
87-
except ImportError:
88-
print(f"Installing {package_name}...")
89-
try:
90-
subprocess.check_call(
91-
[sys.executable, "-m", "pip", "install", package_name]
92-
)
93-
print(f"{package_name} installed successfully.")
94-
except subprocess.CalledProcessError as e:
95-
print(f"Failed to install {package_name}: {e}")
96-
raise ImportError(
97-
f"Could not install {package_name}. "
98-
f"Please install it manually with 'pip install {package_name}'."
99-
)
84+
except ImportError as exc:
85+
raise ImportError(
86+
f"{package_name} is required for Spark support. "
87+
"Install with: pip install datafog[distributed]"
88+
) from exc

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
distributed_deps = [
4646
"pandas>=2.0.0",
4747
"numpy>=1.24.0",
48+
"pyspark>=3.5.0",
4849
]
4950

5051
web_deps = [

0 commit comments

Comments
 (0)