HowdenParser 6.0.0__tar.gz → 7.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.
@@ -1,12 +1,13 @@
1
1
  from abc import ABC, abstractmethod
2
2
  import logging
3
3
  import dotenv
4
- from typing import overload, Literal
4
+ from typing import overload, Literal, Any, cast
5
5
  from inspect import signature, Signature
6
- from typing import Any
7
6
  import json
8
7
  import hashlib
9
8
  from pathlib import Path
9
+ import re
10
+
10
11
  dotenv.load_dotenv()
11
12
  logger = logging.getLogger(__name__)
12
13
 
@@ -26,7 +27,7 @@ class BaseParser(ABC):
26
27
  BaseParser._registry.pop("", None)
27
28
 
28
29
  @abstractmethod
29
- def parse(self, text: str, include_pagenumbers: bool = False):
30
+ def parse(self, text: Path, include_pagenumbers: bool = False):
30
31
  pass
31
32
 
32
33
  def make_serializable(self, obj: Any) -> Any:
@@ -70,12 +71,52 @@ class BaseParser(ABC):
70
71
  class Parser(BaseParser):
71
72
  """Factory + registry interface for all parsers."""
72
73
 
73
- def __new__(cls, config_or_dict: "Parameter | dict | str" = None, **kwargs) -> BaseParser:
74
- """
75
- Factory entrypoint.
76
- Allows calling Parser(...) directly to create the correct subclass.
77
- """
78
- return cls._create(config_or_dict, **kwargs)
74
+ def __new__(cls, *args, **kwargs) -> "Parser":
75
+ """Factory entrypoint — returns the correct concrete parser subclass."""
76
+ return cls._create(*args, **kwargs)
77
+
78
+ # ---- Overloads so IDE validates kwargs per provider ----
79
+
80
+ @overload
81
+ def __init__(
82
+ self,
83
+ *,
84
+ provider_and_model: Literal["llamaparser:"],
85
+ tier: str,
86
+ version: str,
87
+ result_type: str,
88
+ preserve_layout_alignment_across_pages: bool,
89
+ merge_continued_tables: bool,
90
+ ) -> None: ...
91
+
92
+ @overload
93
+ def __init__(
94
+ self,
95
+ config_or_dict: Literal["mistralocr:ocr-large", "mistralocr:ocr-small"],
96
+ ) -> None: ...
97
+
98
+ @overload
99
+ def __init__(
100
+ self,
101
+ *,
102
+ provider_and_model: Literal["mistralocr:ocr-large", "mistralocr:ocr-small"],
103
+ ) -> None: ...
104
+
105
+ @overload
106
+ def __init__(
107
+ self,
108
+ config_or_dict: Literal["langchain:gpt-3.5-turbo", "langchain:gpt-4"],
109
+ ) -> None: ...
110
+
111
+ @overload
112
+ def __init__(
113
+ self,
114
+ *,
115
+ provider_and_model: Literal["langchain:gpt-3.5-turbo", "langchain:gpt-4"],
116
+ ) -> None: ...
117
+
118
+ def __init__(self, *_args, **_kwargs) -> None:
119
+ super().__init__() # Never called — __new__ always returns a subclass instance
79
120
 
80
121
  @classmethod
81
122
  def available_parsers(cls) -> None:
@@ -89,39 +130,17 @@ class Parser(BaseParser):
89
130
  for key, values in result.items():
90
131
  print(f"{key} with parameters: {values}")
91
132
 
92
- # ---- Overloads for IDE autocomplete ----
93
- @overload
94
- @classmethod
95
- def _create(
96
- cls,
97
- *,
98
- provider_and_model: Literal["llamaparser:"],
99
- tier: str,
100
- version: str,
101
- result_type: str,
102
- preserve_layout_alignment_across_pages: bool,
103
- merge_continued_tables: bool,
104
- ) -> "LlamaParser": ...
105
-
106
- @overload
107
133
  @classmethod
108
- def _create(
109
- cls,
110
- *,
111
- provider_and_model: Literal["mistralocr:ocr-large", "mistralocr:ocr-small"],
112
- ) -> "MistralOCRParser": ...
134
+ def _validate_provider_and_model(cls,provider_and_model: str) -> None:
135
+ if not re.match(r"^.+:", provider_and_model):
136
+ raise ValueError(
137
+ "provider_and_model must include a provider before the colon, e.g. 'llamaparser:'"
138
+ )
113
139
 
114
- @overload
115
- @classmethod
116
- def _create(
117
- cls,
118
- *,
119
- provider_and_model: Literal["langchain:gpt-3.5-turbo", "langchain:gpt-4"],
120
- ) -> "LangChainParser": ...
121
140
 
122
141
  # ---- Implementation ----
123
142
  @classmethod
124
- def _create(cls, config_or_dict: "Parameter | dict | str" = None, **kwargs) -> BaseParser:
143
+ def _create(cls, config_or_dict: dict | str = None, **kwargs) -> "Parser":
125
144
  """
126
145
  Dynamically create parser instances.
127
146
  Supports: Parameter, dict, str (provider_and_model), or kwargs.
@@ -145,6 +164,8 @@ class Parser(BaseParser):
145
164
 
146
165
  provider_and_model = str(merged_args.get("provider_and_model")).strip()
147
166
 
167
+ Parser._validate_provider_and_model(provider_and_model)
168
+
148
169
  if ":" not in provider_and_model:
149
170
  raise ValueError("provider_and_model must include a colon, e.g. 'llamaparser:'")
150
171
 
@@ -197,8 +218,7 @@ class Parser(BaseParser):
197
218
  f"{parser_cls.__name__} cannot be created because it is missing required argument(s): {', '.join(missing)}. {example}"
198
219
  )
199
220
 
200
- return parser_cls(**valid_args)
221
+ return cast("Parser", cast(Any, parser_cls)(**valid_args))
201
222
 
202
- @abstractmethod
203
- def parse(self, text: str, include_pagenumbers: bool = False):
204
- pass
223
+ def parse(self, text: Path, include_pagenumbers: bool = False):
224
+ raise NotImplementedError
@@ -1,5 +1,7 @@
1
1
  import os
2
2
  import logging
3
+ from HowdenCommonObjects.howden_result import HowdenResult
4
+ from HowdenCommonObjects.usage import Usage
3
5
  from pathlib import Path
4
6
  from ..parser import BaseParser
5
7
  from typing import Any
@@ -7,8 +9,8 @@ from typing import Any
7
9
  class LlamaParser(BaseParser, name="llamaparser"):
8
10
  def __init__(self,
9
11
  result_type: str,
10
- tier: str, # One of: ["fast", "cost_effective", "agentic", "agentic_plus"]
11
- version: str, # one of: ["latest","2026-03-12","2026-03-11"...] (many not listed)
12
+ tier: str, # One of: ["fast", "cost_effective", "agentic", "agentic_plus"]
13
+ version: str, # one of: ["latest","2026-03-12","2026-03-11"...] (many not listed)
12
14
  provider_and_model: str,
13
15
  merge_continued_tables: bool,
14
16
  preserve_layout_alignment_across_pages: bool,
@@ -48,12 +50,15 @@ class LlamaParser(BaseParser, name="llamaparser"):
48
50
  if not api_key:
49
51
  raise EnvironmentError("Missing LLAMA-PARSER-API-TOKEN in .env file.")
50
52
 
51
- rt_lower = str(self.result_type).lower() # Possible values for LlamaCloud expand: ["markdown", "text", "metadata", "items"]
52
- if rt_lower in ("md","markdown"):
53
+ rt_lower = str(
54
+ self.result_type).lower() # Possible values for LlamaCloud expand: ["markdown", "markdown_full", "text", "metadata", "items"]
55
+ if rt_lower in ("md", "markdown"):
53
56
  self.rt = 'markdown'
54
- elif rt_lower in ("txt","text"):
57
+ elif rt_lower in ("md_full", "markdown_full"):
58
+ self.rt = 'markdown_full'
59
+ elif rt_lower in ("txt", "text"):
55
60
  self.rt = 'text'
56
- elif rt_lower in ("json"):
61
+ elif rt_lower == "json":
57
62
  self.rt = 'items'
58
63
  else:
59
64
  raise NotImplementedError(f"Result type {self.result_type} is not supported.")
@@ -62,9 +67,10 @@ class LlamaParser(BaseParser, name="llamaparser"):
62
67
 
63
68
  logging.info("LlamaParser initialized inside worker thread")
64
69
 
65
- def parse(self, file_path: Path, include_pagenumbers: bool = False) -> Any:
70
+ def parse(self, file_path: Path, include_pagenumbers: bool = False) -> HowdenResult:
66
71
  # Ensure parser is created inside the thread event loop
67
72
  self._lazy_init()
73
+
68
74
  def page_break(text: str, idx: int) -> str:
69
75
  return f"<PAGE_NUMBER {idx}>{text}</PAGE_NUMBER {idx}>"
70
76
 
@@ -83,22 +89,37 @@ class LlamaParser(BaseParser, name="llamaparser"):
83
89
  "preserve_layout_alignment_across_pages": self.preserve_layout_alignment_across_pages,
84
90
  }
85
91
  },
86
- expand=[self.rt],
92
+ expand=[self.rt,"job_metadata","usage"],
93
+ )
94
+
95
+ usage = Usage(
96
+ type="parser",
97
+ provider=self.provider_and_model,
98
+ model=self.version + " " + self.tier,
99
+ operation=self.name,
100
+ pages = documents.job_metadata["pdf-pages"]
87
101
  )
88
102
 
89
103
  if self.rt == 'markdown':
90
- if include_pagenumbers: return "\n".join(page_break(page.markdown, idx) for idx, page in enumerate(documents.markdown.pages, start=1))
91
- else: return "\n".join(page.markdown for page in documents.markdown.pages)
104
+ if include_pagenumbers:
105
+ result = "\n".join(page_break(page.markdown, idx) for idx, page in enumerate(documents.markdown.pages, start=1))
106
+ else:
107
+ result = "\n".join(page.markdown for page in documents.markdown.pages)
108
+ elif self.rt == 'markdown_full':
109
+ result = documents.markdown_full
92
110
  elif self.rt == 'text':
93
- if include_pagenumbers: return "\n".join(page_break(page.text, idx) for idx, page in enumerate(documents.text.pages, start=1))
94
- else: return "\n".join(page.text for page in documents.text.pages)
111
+ if include_pagenumbers:
112
+ result = "\n".join(page_break(page.text, idx) for idx, page in enumerate(documents.text.pages, start=1))
113
+ else:
114
+ result = "\n".join(page.text for page in documents.text.pages)
95
115
  elif self.rt == 'items':
96
- return documents.model_dump()
116
+ result = documents.model_dump()
97
117
  else:
98
118
  raise NotImplementedError(f"Result type {self.result_type} is not supported.")
119
+ return HowdenResult(result,usage)
99
120
 
100
- def __call__(self, file_path: Path) -> str:
121
+ def __call__(self, file_path: Path) -> HowdenResult:
101
122
  return self.parse(file_path)
102
123
 
103
124
  def write_json_hyperparameter(self, folder_file_path: Path) -> None:
104
- self.write_parameters(self._input_params, folder_file_path)
125
+ self.write_parameters(self._input_params, folder_file_path)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: HowdenParser
3
- Version: 6.0.0
3
+ Version: 7.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
@@ -12,8 +12,9 @@ Classifier: Programming Language :: Python :: 3
12
12
  Classifier: Programming Language :: Python :: 3.12
13
13
  Classifier: Programming Language :: Python :: 3.13
14
14
  Requires-Dist: dotenv (>=0.9.9,<0.10.0)
15
+ Requires-Dist: howdencommonobjects (>=1.1.0)
15
16
  Requires-Dist: langchain (>=1.1.3,<2.0.0)
16
- Requires-Dist: llama-cloud (>=1.0)
17
+ Requires-Dist: llama-cloud (==2.16.0)
17
18
  Requires-Dist: mistralai (>=1.9.3,<2.0.0)
18
19
  Requires-Dist: pdf2image (>=1.17.0,<2.0.0)
19
20
  Requires-Dist: pypdf2 (>=3.0.1,<4.0.0)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "HowdenParser"
3
- version = "6.0.0"
3
+ version = "7.0.0"
4
4
  description = "A simple configuration manager with Pydantic and JSON export."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12,<3.14"
@@ -18,13 +18,14 @@ repository = "https://github.com/yourusername/config"
18
18
  documentation = "https://github.com/yourusername/config"
19
19
  dependencies = [
20
20
  "mistralai (>=1.9.3,<2.0.0)",
21
- "llama-cloud (>=1.0)",
21
+ "llama-cloud==2.16.0",
22
22
  "pypdf2 (>=3.0.1,<4.0.0)",
23
23
  "pdf2image (>=1.17.0,<2.0.0)",
24
24
  "langchain (>=1.1.3,<2.0.0)",
25
25
  "pytest (>=9.0.2,<10.0.0)",
26
26
  "tomli-w (>=1.2.0,<2.0.0)",
27
27
  "dotenv (>=0.9.9,<0.10.0)",
28
+ "howdencommonobjects>=1.1.0",
28
29
  ]
29
30
 
30
31
  [project.license]
@@ -41,4 +42,5 @@ dev = [
41
42
  "toml (>=0.10.2,<0.11.0)",
42
43
  "tomli-w (>=1.2.0,<2.0.0)",
43
44
  "howdenconfig (>=1.0.6,<2.0.0)",
45
+ "howdencommonobjects",
44
46
  ]
File without changes