HowdenParser 5.0.0__tar.gz → 5.2.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.
@@ -3,6 +3,9 @@ import logging
3
3
  import dotenv
4
4
  from typing import overload, Literal
5
5
  from inspect import signature, Signature
6
+ from typing import Any
7
+ import json
8
+ import hashlib
6
9
 
7
10
  dotenv.load_dotenv()
8
11
  logger = logging.getLogger(__name__)
@@ -26,6 +29,30 @@ class BaseParser(ABC):
26
29
  def parse(self, text: str, include_pagenumbers: bool = False):
27
30
  pass
28
31
 
32
+ def make_serializable(self, obj: Any) -> Any:
33
+ if obj is None or isinstance(obj, (str, int, float, bool)):
34
+ return obj
35
+ if isinstance(obj, (list, tuple, set)):
36
+ return [self.make_serializable(v) for v in obj]
37
+ if isinstance(obj, dict):
38
+ return {k: self.make_serializable(v) for k, v in sorted(obj.items())}
39
+ if hasattr(obj, "__dict__"):
40
+ return self.make_serializable(vars(obj))
41
+ return f"{type(obj).__name__}:{repr(obj)}"
42
+
43
+ def compute_hash(self, parameter:dict) -> str:
44
+
45
+ attrs = {
46
+ k: v
47
+ for k, v in parameter.items()
48
+ if k != "hash"
49
+ and not k.startswith("_")
50
+ and k not in ("client", "provider")
51
+ }
52
+ serializable = self.make_serializable(attrs)
53
+ attrs_str = json.dumps(serializable, sort_keys=True, ensure_ascii=True)
54
+ hashed = hashlib.sha256(attrs_str.encode()).hexdigest()[:8]
55
+ return hashed
29
56
 
30
57
  class Parser(BaseParser):
31
58
  """Factory + registry interface for all parsers."""
@@ -0,0 +1,88 @@
1
+ import os
2
+ import logging
3
+ from pathlib import Path
4
+ from ..parser import BaseParser
5
+
6
+ class LlamaParser(BaseParser, name="llamaparser"):
7
+ def __init__(self, result_type: str, model: str, parse_mode: str, provider_and_model: str,
8
+ merge_tables_across_pages_in_markdown: bool, preserve_layout_alignment_across_pages: bool,
9
+ hide_footers: bool, hide_headers: bool) -> None:
10
+
11
+ super().__init__()
12
+ # Capture only input parameters
13
+ self._input_params = {
14
+ k: v
15
+ for k, v in locals().items()
16
+ if k not in ("self", "__class__", "__len__")
17
+ }
18
+
19
+ logging.info("Configuring LlamaParser (lazy init enabled)...")
20
+
21
+ # Store configuration only
22
+ self.result_type = result_type
23
+ self.model = model
24
+ self.parse_mode = parse_mode
25
+ self.provider_and_model = provider_and_model
26
+ self.merge_tables_across_pages_in_markdown = merge_tables_across_pages_in_markdown
27
+ self.preserve_layout_alignment_across_pages = preserve_layout_alignment_across_pages
28
+ self.hide_footers = hide_footers
29
+ self.hide_headers = hide_headers
30
+ self.hashed = self.compute_hash(self._input_params)
31
+ # DO NOT CREATE LlamaParse HERE
32
+ # It creates asyncio objects bound to the wrong loop
33
+ self._parser = None
34
+
35
+ def _lazy_init(self):
36
+ """Initialize LlamaParse inside the worker thread event loop."""
37
+ if self._parser is not None:
38
+ return
39
+
40
+ from llama_parse import LlamaParse, ResultType
41
+
42
+ api_key = os.getenv("LLAMA-PARSER-API-TOKEN")
43
+ if not api_key:
44
+ raise EnvironmentError("Missing LLAMA-PARSER-API-TOKEN in .env file.")
45
+
46
+ rt = self.result_type
47
+ if rt.lower() in ("md", "markdown"):
48
+ rt = ResultType.MD
49
+
50
+ # Construct the parser INSIDE the worker thread
51
+ self._parser = LlamaParse(
52
+ api_key=api_key,
53
+ result_type=rt,
54
+ model=self.model,
55
+ parse_mode=self.parse_mode,
56
+ merge_tables_across_pages_in_markdown=self.merge_tables_across_pages_in_markdown,
57
+ preserve_layout_alignment_across_pages=self.preserve_layout_alignment_across_pages,
58
+ hide_footers=self.hide_footers,
59
+ hide_headers=self.hide_headers,
60
+ )
61
+
62
+ logging.info("LlamaParser initialized inside worker thread")
63
+
64
+ def parse(self, file_path: Path, include_pagenumbers: bool = False) -> str:
65
+ # Ensure parser is created inside the thread event loop
66
+ self._lazy_init()
67
+
68
+ # Standard calls
69
+ if not include_pagenumbers:
70
+ documents = self._parser.load_data(str(file_path))
71
+ return "\n".join(doc.text for doc in documents)
72
+
73
+ # With page numbers
74
+ documents = self._parser.parse(file_path)
75
+
76
+ if self.result_type.lower() in ("md", "markdown"):
77
+ return "\n".join(
78
+ f"<PAGE_NUMBER {idx}>{page.md}</PAGE_NUMBER {idx}>"
79
+ for idx, page in enumerate(documents.pages, start=1)
80
+ )
81
+
82
+ return "\n".join(
83
+ f"<PAGE_NUMBER {idx}>{page.text}</PAGE_NUMBER {idx}>"
84
+ for idx, page in enumerate(documents.pages, start=1)
85
+ )
86
+
87
+ def __call__(self, file_path: Path) -> str:
88
+ return self.parse(file_path)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: HowdenParser
3
- Version: 5.0.0
3
+ Version: 5.2.0
4
4
  Summary: A simple configuration manager with Pydantic and JSON export.
5
5
  License: MIT
6
6
  Keywords: config,configuration,pydantic,json
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "HowdenParser"
3
- version = "5.0.0"
3
+ version = "5.2.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"
@@ -1,64 +0,0 @@
1
- import os
2
- import logging
3
- from pathlib import Path
4
- from ..parser import BaseParser
5
-
6
- class LlamaParser(BaseParser, name="llamaparser"):
7
- def __init__(self, result_type: str,
8
- model: str,
9
- parse_mode: str,
10
- provider_and_model: str,
11
- merge_tables_across_pages_in_markdown: bool,
12
- preserve_layout_alignment_across_pages: bool,
13
- hide_footers: bool,
14
- hide_headers: bool
15
- ) -> None:
16
- logging.info("Initializing LlamaParser...")
17
- self.result_type = result_type
18
- self.model = model
19
- self.parse_mode = parse_mode
20
- self.provider_and_model = provider_and_model
21
- self.merge_tables_across_pages_in_markdown = merge_tables_across_pages_in_markdown
22
- self.preserve_layout_alignment_across_pages = preserve_layout_alignment_across_pages
23
- self.hide_footers=hide_footers
24
- self.hide_headers=hide_headers
25
-
26
- from llama_parse import LlamaParse, ResultType
27
- if result_type.lower() in ("md", "markdown"):
28
- result_type = ResultType.MD
29
- api_key = os.getenv("LLAMA-PARSER-API-TOKEN")
30
- if not api_key:
31
- raise EnvironmentError("Missing LLAMA-PARSER-API-TOKEN in .env file.")
32
- self._parser = LlamaParse(api_key=api_key,
33
- result_type=result_type,
34
- model=self.model,
35
- parse_mode=self.parse_mode,
36
- merge_tables_across_pages_in_markdown=self.merge_tables_across_pages_in_markdown,
37
- preserve_layout_alignment_across_pages=self.preserve_layout_alignment_across_pages,
38
- hide_footers=self.hide_footers,
39
- hide_headers=self.hide_headers)
40
-
41
- def parse(self, file_path: Path, include_pagenumbers: bool=False) -> str:
42
- """
43
- Parse the document at file_path.
44
- Args:
45
- file_path (Path): Path to the document to be parsed.
46
- include_pagenumbers (bool): Whether to include page numbers in the output. If True, page numbers will be wrapped around each page's content, like so: <PAGE_NUMBER 1>...content...</PAGE_NUMBER 1>
47
- """
48
-
49
- if not include_pagenumbers:
50
- documents = self._parser.load_data(str(file_path))
51
-
52
- result = "\n".join(doc.text for doc in documents)
53
- else:
54
- documents = self._parser.parse(file_path)
55
-
56
- if self.result_type.lower() in ("md", "markdown"):
57
- result = "\n".join(f"<PAGE_NUMBER {idx}>{page.md}</PAGE_NUMBER {idx}>" for idx, page in enumerate(documents.pages, start=1))
58
- else:
59
- result = "\n".join(f"<PAGE_NUMBER {idx}>{page.text}</PAGE_NUMBER {idx}>" for idx, page in enumerate(documents.pages, start=1))
60
-
61
- return result
62
-
63
- def __call__(self, file_path: Path) -> str:
64
- return self.parse(file_path)
File without changes