HowdenParser 6.0.1__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,10 +1,11 @@
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
6
8
 
7
-
8
9
  class LlamaParser(BaseParser, name="llamaparser"):
9
10
  def __init__(self,
10
11
  result_type: str,
@@ -57,7 +58,7 @@ class LlamaParser(BaseParser, name="llamaparser"):
57
58
  self.rt = 'markdown_full'
58
59
  elif rt_lower in ("txt", "text"):
59
60
  self.rt = 'text'
60
- elif rt_lower in ("json"):
61
+ elif rt_lower == "json":
61
62
  self.rt = 'items'
62
63
  else:
63
64
  raise NotImplementedError(f"Result type {self.result_type} is not supported.")
@@ -66,7 +67,7 @@ class LlamaParser(BaseParser, name="llamaparser"):
66
67
 
67
68
  logging.info("LlamaParser initialized inside worker thread")
68
69
 
69
- def parse(self, file_path: Path, include_pagenumbers: bool = False) -> Any:
70
+ def parse(self, file_path: Path, include_pagenumbers: bool = False) -> HowdenResult:
70
71
  # Ensure parser is created inside the thread event loop
71
72
  self._lazy_init()
72
73
 
@@ -88,28 +89,36 @@ class LlamaParser(BaseParser, name="llamaparser"):
88
89
  "preserve_layout_alignment_across_pages": self.preserve_layout_alignment_across_pages,
89
90
  }
90
91
  },
91
- 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"]
92
101
  )
93
102
 
94
103
  if self.rt == 'markdown':
95
104
  if include_pagenumbers:
96
- return "\n".join(
97
- page_break(page.markdown, idx) for idx, page in enumerate(documents.markdown.pages, start=1))
105
+ result = "\n".join(page_break(page.markdown, idx) for idx, page in enumerate(documents.markdown.pages, start=1))
98
106
  else:
99
- return "\n".join(page.markdown for page in documents.markdown.pages)
107
+ result = "\n".join(page.markdown for page in documents.markdown.pages)
100
108
  elif self.rt == 'markdown_full':
101
- return documents.markdown_full
109
+ result = documents.markdown_full
102
110
  elif self.rt == 'text':
103
111
  if include_pagenumbers:
104
- return "\n".join(page_break(page.text, idx) for idx, page in enumerate(documents.text.pages, start=1))
112
+ result = "\n".join(page_break(page.text, idx) for idx, page in enumerate(documents.text.pages, start=1))
105
113
  else:
106
- return "\n".join(page.text for page in documents.text.pages)
114
+ result = "\n".join(page.text for page in documents.text.pages)
107
115
  elif self.rt == 'items':
108
- return documents.model_dump()
116
+ result = documents.model_dump()
109
117
  else:
110
118
  raise NotImplementedError(f"Result type {self.result_type} is not supported.")
119
+ return HowdenResult(result,usage)
111
120
 
112
- def __call__(self, file_path: Path) -> str:
121
+ def __call__(self, file_path: Path) -> HowdenResult:
113
122
  return self.parse(file_path)
114
123
 
115
124
  def write_json_hyperparameter(self, folder_file_path: Path) -> None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: HowdenParser
3
- Version: 6.0.1
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,44 +1,46 @@
1
- [project]
2
- name = "HowdenParser"
3
- version = "6.0.1"
4
- description = "A simple configuration manager with Pydantic and JSON export."
5
- readme = "README.md"
6
- requires-python = ">=3.12,<3.14"
7
- authors = [
8
- { name = "JesperThoftIllemannJ", email = "jesper.jaeger@howdendanmark.dk" },
9
- ]
10
- keywords = [
11
- "config",
12
- "configuration",
13
- "pydantic",
14
- "json",
15
- ]
16
- homepage = "https://github.com/yourusername/config"
17
- repository = "https://github.com/yourusername/config"
18
- documentation = "https://github.com/yourusername/config"
19
- dependencies = [
20
- "mistralai (>=1.9.3,<2.0.0)",
21
- "llama-cloud (>=1.0)",
22
- "pypdf2 (>=3.0.1,<4.0.0)",
23
- "pdf2image (>=1.17.0,<2.0.0)",
24
- "langchain (>=1.1.3,<2.0.0)",
25
- "pytest (>=9.0.2,<10.0.0)",
26
- "tomli-w (>=1.2.0,<2.0.0)",
27
- "dotenv (>=0.9.9,<0.10.0)",
28
- ]
29
-
30
- [project.license]
31
- text = "MIT"
32
-
33
- [build-system]
34
- requires = [
35
- "poetry-core>=2.0.0,<3.0.0",
36
- ]
37
- build-backend = "poetry.core.masonry.api"
38
-
39
- [dependency-groups]
40
- dev = [
41
- "toml (>=0.10.2,<0.11.0)",
42
- "tomli-w (>=1.2.0,<2.0.0)",
43
- "howdenconfig (>=1.0.6,<2.0.0)",
44
- ]
1
+ [project]
2
+ name = "HowdenParser"
3
+ version = "7.0.0"
4
+ description = "A simple configuration manager with Pydantic and JSON export."
5
+ readme = "README.md"
6
+ requires-python = ">=3.12,<3.14"
7
+ authors = [
8
+ { name = "JesperThoftIllemannJ", email = "jesper.jaeger@howdendanmark.dk" },
9
+ ]
10
+ keywords = [
11
+ "config",
12
+ "configuration",
13
+ "pydantic",
14
+ "json",
15
+ ]
16
+ homepage = "https://github.com/yourusername/config"
17
+ repository = "https://github.com/yourusername/config"
18
+ documentation = "https://github.com/yourusername/config"
19
+ dependencies = [
20
+ "mistralai (>=1.9.3,<2.0.0)",
21
+ "llama-cloud==2.16.0",
22
+ "pypdf2 (>=3.0.1,<4.0.0)",
23
+ "pdf2image (>=1.17.0,<2.0.0)",
24
+ "langchain (>=1.1.3,<2.0.0)",
25
+ "pytest (>=9.0.2,<10.0.0)",
26
+ "tomli-w (>=1.2.0,<2.0.0)",
27
+ "dotenv (>=0.9.9,<0.10.0)",
28
+ "howdencommonobjects>=1.1.0",
29
+ ]
30
+
31
+ [project.license]
32
+ text = "MIT"
33
+
34
+ [build-system]
35
+ requires = [
36
+ "poetry-core>=2.0.0,<3.0.0",
37
+ ]
38
+ build-backend = "poetry.core.masonry.api"
39
+
40
+ [dependency-groups]
41
+ dev = [
42
+ "toml (>=0.10.2,<0.11.0)",
43
+ "tomli-w (>=1.2.0,<2.0.0)",
44
+ "howdenconfig (>=1.0.6,<2.0.0)",
45
+ "howdencommonobjects",
46
+ ]
File without changes