HowdenParser 2.0.3__tar.gz → 3.0.0__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.
@@ -4,8 +4,8 @@ import logging
4
4
  from pathlib import Path
5
5
  from PyPDF2 import PdfReader
6
6
  import dotenv
7
- import logging
8
- from inspect import signature
7
+ from typing import overload, Literal
8
+ from inspect import signature, Signature
9
9
 
10
10
  dotenv.load_dotenv()
11
11
  logger = logging.getLogger(__name__)
@@ -29,6 +29,13 @@ class BaseParser(ABC):
29
29
  class Parser(BaseParser):
30
30
  """Factory + registry interface for all parsers."""
31
31
 
32
+ def __new__(cls, config_or_dict: "Parameter | dict | str" = None, **kwargs) -> BaseParser:
33
+ """
34
+ Factory entrypoint.
35
+ Allows calling Parser(...) directly to create the correct subclass.
36
+ """
37
+ return cls._create(config_or_dict, **kwargs)
38
+
32
39
  @classmethod
33
40
  def available_parsers(cls) -> None:
34
41
  """Print registered parsers and their init arguments."""
@@ -37,53 +44,128 @@ class Parser(BaseParser):
37
44
  for name, parser_cls in BaseParser._registry.items():
38
45
  sig = inspect.signature(parser_cls.__init__)
39
46
  result[name] = [p for p in sig.parameters if p != "self"]
40
- result.pop('', None)
47
+ result.pop("", None)
41
48
  for key, values in result.items():
42
49
  print(f"{key} with parameters: {values}")
43
50
 
51
+ # ---- Overloads for IDE autocomplete ----
52
+ @overload
53
+ @classmethod
54
+ def _create(
55
+ cls,
56
+ *,
57
+ provider_and_model: Literal["llamaparser:"],
58
+ result_type: str,
59
+ mode: bool,
60
+ ) -> "LlamaParser": ...
61
+
62
+ @overload
63
+ @classmethod
64
+ def _create(
65
+ cls,
66
+ *,
67
+ provider_and_model: Literal["mistralocr:ocr-large", "mistralocr:ocr-small"],
68
+ ) -> "MistralOCRParser": ...
69
+
70
+ @overload
71
+ @classmethod
72
+ def _create(
73
+ cls,
74
+ *,
75
+ provider_and_model: Literal["langchain:gpt-3.5-turbo", "langchain:gpt-4"],
76
+ ) -> "LangChainParser": ...
77
+
78
+ @overload
44
79
  @classmethod
45
- def create(cls, config_or_dict: "Parameter | dict", **kwargs) -> BaseParser:
80
+ def _create(
81
+ cls,
82
+ *,
83
+ provider_and_model: Literal[
84
+ "huggingface:microsoft/trocr-base-handwritten",
85
+ "huggingface:microsoft/trocr-large-printed",
86
+ ],
87
+ ) -> "HuggingFaceParser": ...
88
+
89
+ # ---- Implementation ----
90
+ @classmethod
91
+ def _create(cls, config_or_dict: "Parameter | dict | str" = None, **kwargs) -> BaseParser:
46
92
  """
47
- Dynamically create parser instances from a config object or dict.
48
- Only passes arguments accepted by the parser constructor.
93
+ Dynamically create parser instances.
94
+ Supports: Parameter, dict, str (provider_and_model), or kwargs.
49
95
  """
50
- # Convert Parameter -> dict if needed
51
- if hasattr(config_or_dict, "model_dump"):
96
+
97
+ if config_or_dict is None:
98
+ config_dict = {}
99
+ elif hasattr(config_or_dict, "model_dump"):
52
100
  config_dict = config_or_dict.model_dump()
53
101
  elif isinstance(config_or_dict, dict):
54
102
  config_dict = config_or_dict
103
+ elif isinstance(config_or_dict, str): # shorthand
104
+ config_dict = {"provider_and_model": config_or_dict}
55
105
  else:
56
- raise TypeError("Expected Parameter instance or dict for config_or_dict")
57
-
58
- if "provider_and_model" not in kwargs and "provider_and_model" not in config_dict:
59
- raise ValueError("provider_and_model must be specified")
106
+ raise TypeError("Expected Parameter instance, dict, str, or None for config_or_dict")
60
107
 
61
- # Merge dict + kwargs (kwargs take precedence)
62
108
  merged_args = {**config_dict, **kwargs}
63
109
 
64
- # Extract provider/model info
65
- provider_and_model = merged_args.get("provider_and_model")
66
- provider, model = provider_and_model.split(":")
67
- provider = provider.lower()
68
- model = model.lower()
110
+ if "provider_and_model" not in merged_args:
111
+ raise ValueError("provider_and_model must be specified, e.g. 'llamaparser:'")
112
+
113
+ provider_and_model = str(merged_args.get("provider_and_model")).strip()
114
+
115
+ if ":" not in provider_and_model:
116
+ raise ValueError("provider_and_model must include a colon, e.g. 'llamaparser:'")
117
+
118
+ provider, model = provider_and_model.split(":", 1)
119
+ provider = (provider or "").strip().lower()
120
+ model = (model or "").strip().lower()
121
+
122
+ if not provider:
123
+ raise ValueError(f"Invalid provider_and_model '{provider_and_model}': provider part is empty.")
69
124
 
70
125
  if provider not in BaseParser._registry:
71
126
  raise ValueError(f"Unknown parser '{provider}'. Available: {list(BaseParser._registry)}")
72
127
 
73
128
  parser_cls = BaseParser._registry[provider]
74
129
 
75
- # Inspect constructor and only pass valid arguments
130
+ # --- Filter valid constructor args ---
76
131
  sig = signature(parser_cls.__init__)
77
132
  valid_args = {k: v for k, v in merged_args.items() if k in sig.parameters and k != "self"}
78
133
 
79
- # Auto-insert 'model' if required
134
+ # Auto-fill common args
80
135
  if "model" in sig.parameters and "model" not in valid_args:
81
136
  valid_args["model"] = model
82
-
83
- # Auto-insert 'provider_and_model' if required
84
137
  if "provider_and_model" in sig.parameters and "provider_and_model" not in valid_args:
85
138
  valid_args["provider_and_model"] = provider_and_model
86
139
 
140
+ # --- Check required args ---
141
+ required_params = [
142
+ p.name
143
+ for p in sig.parameters.values()
144
+ if p.name != "self"
145
+ and p.default is Signature.empty
146
+ and p.kind in (p.POSITIONAL_OR_KEYWORD, p.KEYWORD_ONLY)
147
+ ]
148
+ missing = [name for name in required_params if name not in valid_args]
149
+
150
+ # Special rule: for llamaparser we allow empty `model`
151
+ if provider != "llamaparser" and not model:
152
+ missing.append("model")
153
+
154
+ if missing:
155
+ example = ""
156
+ if provider == "llamaparser":
157
+ example = "Example: Parser('llamaparser:', result_type='md', mode=True)"
158
+ elif provider == "mistralocr":
159
+ example = "Example: Parser('mistralocr:ocr-large')"
160
+ elif provider == "huggingface":
161
+ example = "Example: Parser('huggingface:microsoft/trocr-base-handwritten')"
162
+ elif provider == "langchain":
163
+ example = "Example: Parser('langchain:gpt-3.5-turbo')"
164
+
165
+ raise TypeError(
166
+ f"{parser_cls.__name__} cannot be created because it is missing required argument(s): {', '.join(missing)}. {example}"
167
+ )
168
+
87
169
  return parser_cls(**valid_args)
88
170
 
89
171
  @abstractmethod
@@ -93,24 +175,21 @@ class Parser(BaseParser):
93
175
 
94
176
  # --- Parsers ---
95
177
  class MistralOCRParser(BaseParser, name="mistralocr"):
96
- def __init__(self,provider_and_model:str) -> None:
178
+ def __init__(self, provider_and_model: str) -> None:
97
179
  from mistralai import Mistral
98
-
99
180
  self.model = provider_and_model.split(":")[1]
100
181
  self.current_cost: float = 0.0
101
182
  self.total_cost_euro: float = 0.0
102
-
103
183
  api_key = os.getenv("MISTRAL-OCR-API-TOKEN")
104
184
  if not api_key:
105
185
  raise EnvironmentError("Missing MISTRAL-OCR-API-TOKEN in .env file.")
106
-
107
186
  self.client = Mistral(api_key=api_key)
108
187
 
109
188
  def parse(self, file_path: Path) -> str:
110
189
  def upload_pdf(filename):
111
190
  uploaded_pdf = self.client.files.upload(
112
191
  file={"file_name": filename, "content": open(filename, "rb")},
113
- purpose="ocr"
192
+ purpose="ocr",
114
193
  )
115
194
  signed_url = self.client.files.get_signed_url(file_id=uploaded_pdf.id)
116
195
  return signed_url.url
@@ -120,14 +199,11 @@ class MistralOCRParser(BaseParser, name="mistralocr"):
120
199
  document={"type": "document_url", "document_url": upload_pdf(file_path)},
121
200
  include_image_base64=True,
122
201
  )
123
-
124
202
  self.current_cost = 1 / 1000 * self._count_pages(file_path)
125
203
  self.total_cost_euro += self.current_cost
126
-
127
204
  return "\n".join(doc.markdown for doc in ocr_response.pages)
128
205
 
129
206
  def __call__(self, file_path: Path) -> str:
130
- # When instance is called, it delegates to parse
131
207
  return self.parse(file_path)
132
208
 
133
209
  @staticmethod
@@ -147,27 +223,22 @@ class LangChainParser(BaseParser, name="langchain"):
147
223
  return {"source": "LangChain", "output": response}
148
224
 
149
225
  def __call__(self, file_path: Path) -> str:
150
- # When instance is called, it delegates to parse
151
226
  return self.parse(file_path)
152
227
 
153
228
 
154
229
  class LlamaParser(BaseParser, name="llamaparser"):
155
230
  def __init__(self, result_type: str, mode: bool, provider_and_model) -> None:
156
231
  logging.info("Initializing LlamaParser...")
157
-
158
232
  self.result_type = result_type
159
233
  self.mode = mode
160
234
  self.provider_and_model = provider_and_model
161
235
 
162
236
  from llama_parse import LlamaParse, ResultType
163
-
164
237
  if result_type.lower() in ("md", "markdown"):
165
238
  result_type = ResultType.MD
166
-
167
239
  api_key = os.getenv("LLAMA-PARSER-API-TOKEN")
168
240
  if not api_key:
169
241
  raise EnvironmentError("Missing LLAMA-PARSER-API-TOKEN in .env file.")
170
-
171
242
  self._parser = LlamaParse(api_key=api_key, result_type=result_type, premium_mode=self.mode)
172
243
 
173
244
  def parse(self, file_path: Path) -> str:
@@ -175,24 +246,19 @@ class LlamaParser(BaseParser, name="llamaparser"):
175
246
  return "\n".join(doc.text for doc in documents)
176
247
 
177
248
  def __call__(self, file_path: Path) -> str:
178
- # When instance is called, it delegates to parse
179
249
  return self.parse(file_path)
180
250
 
181
251
 
182
252
  class HuggingFaceParser(BaseParser, name="huggingface"):
183
253
  def __init__(self, provider_and_model: str) -> None:
184
254
  from transformers import TrOCRProcessor, VisionEncoderDecoderModel
185
-
186
255
  api_key = os.getenv("HF-API-TOKEN")
187
256
  if not api_key:
188
257
  raise EnvironmentError("Missing HF-API-TOKEN in .env file.")
189
-
190
258
  model_name = provider_and_model.split(":")[1]
191
259
  logger.info(f"Loading Hugging Face OCR model: {model_name}")
192
-
193
260
  self.processor = TrOCRProcessor.from_pretrained(model_name, token=api_key)
194
261
  self.model = VisionEncoderDecoderModel.from_pretrained(model_name, token=api_key)
195
-
196
262
  logger.info("Model and processor loaded successfully.")
197
263
 
198
264
  def parse(self, file_path: Path) -> str:
@@ -208,9 +274,6 @@ class HuggingFaceParser(BaseParser, name="huggingface"):
208
274
  generated_ids = self.model.generate(pixel_values)
209
275
  text = self.processor.batch_decode(generated_ids, skip_special_tokens=True)[0]
210
276
  all_text += text + "\n"
211
- logger.debug(f"OCR text (page {i}): {text[:100]}...") # preview first 100 chars
212
-
277
+ logger.debug(f"OCR text (page {i}): {text[:100]}...")
213
278
  logger.info("OCR completed for all pages.")
214
279
  return all_text
215
-
216
-
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: HowdenParser
3
- Version: 2.0.3
3
+ Version: 3.0.0
4
4
  Summary: A simple configuration manager with Pydantic and JSON export.
5
5
  License: MIT
6
6
  Keywords: config,configuration,pydantic,json
@@ -14,7 +14,7 @@ build-backend = "poetry.core.masonry.api"
14
14
 
15
15
  [tool.poetry]
16
16
  name = "HowdenParser"
17
- version = "2.0.3"
17
+ version = "3.0.0"
18
18
  description = "A simple configuration manager with Pydantic and JSON export."
19
19
  authors = [ "JesperThoftIllemannJ <jesper.jaeger@howdendanmark.dk>",]
20
20
  readme = "README.md"
File without changes