attachments 0.2.6__tar.gz → 0.3.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.
Files changed (33) hide show
  1. {attachments-0.2.6/src/attachments.egg-info → attachments-0.3.0}/PKG-INFO +9 -43
  2. {attachments-0.2.6 → attachments-0.3.0}/README.md +8 -42
  3. {attachments-0.2.6 → attachments-0.3.0}/pyproject.toml +1 -1
  4. {attachments-0.2.6 → attachments-0.3.0}/src/attachments/core.py +35 -6
  5. {attachments-0.2.6 → attachments-0.3.0}/src/attachments/parsers.py +21 -0
  6. {attachments-0.2.6 → attachments-0.3.0/src/attachments.egg-info}/PKG-INFO +9 -43
  7. {attachments-0.2.6 → attachments-0.3.0}/tests/test_core_functionality.py +50 -0
  8. {attachments-0.2.6 → attachments-0.3.0}/LICENSE +0 -0
  9. {attachments-0.2.6 → attachments-0.3.0}/setup.cfg +0 -0
  10. {attachments-0.2.6 → attachments-0.3.0}/src/attachments/__init__.py +0 -0
  11. {attachments-0.2.6 → attachments-0.3.0}/src/attachments/audio_processing.py +0 -0
  12. {attachments-0.2.6 → attachments-0.3.0}/src/attachments/config.py +0 -0
  13. {attachments-0.2.6 → attachments-0.3.0}/src/attachments/detectors.py +0 -0
  14. {attachments-0.2.6 → attachments-0.3.0}/src/attachments/exceptions.py +0 -0
  15. {attachments-0.2.6 → attachments-0.3.0}/src/attachments/image_processing.py +0 -0
  16. {attachments-0.2.6 → attachments-0.3.0}/src/attachments/office_contact_sheet.py +0 -0
  17. {attachments-0.2.6 → attachments-0.3.0}/src/attachments/renderers.py +0 -0
  18. {attachments-0.2.6 → attachments-0.3.0}/src/attachments/utils.py +0 -0
  19. {attachments-0.2.6 → attachments-0.3.0}/src/attachments.egg-info/SOURCES.txt +0 -0
  20. {attachments-0.2.6 → attachments-0.3.0}/src/attachments.egg-info/dependency_links.txt +0 -0
  21. {attachments-0.2.6 → attachments-0.3.0}/src/attachments.egg-info/requires.txt +0 -0
  22. {attachments-0.2.6 → attachments-0.3.0}/src/attachments.egg-info/top_level.txt +0 -0
  23. {attachments-0.2.6 → attachments-0.3.0}/tests/test_audio_processing.py +0 -0
  24. {attachments-0.2.6 → attachments-0.3.0}/tests/test_base.py +0 -0
  25. {attachments-0.2.6 → attachments-0.3.0}/tests/test_document_parsing.py +0 -0
  26. {attachments-0.2.6 → attachments-0.3.0}/tests/test_html_parsing.py +0 -0
  27. {attachments-0.2.6 → attachments-0.3.0}/tests/test_image_processing.py +0 -0
  28. {attachments-0.2.6 → attachments-0.3.0}/tests/test_indexing_slicing.py +0 -0
  29. {attachments-0.2.6 → attachments-0.3.0}/tests/test_markdown_rendering.py +0 -0
  30. {attachments-0.2.6 → attachments-0.3.0}/tests/test_office_contact_sheet.py +0 -0
  31. {attachments-0.2.6 → attachments-0.3.0}/tests/test_pdf_parsing.py +0 -0
  32. {attachments-0.2.6 → attachments-0.3.0}/tests/test_pptx_parsing.py +0 -0
  33. {attachments-0.2.6 → attachments-0.3.0}/tests/test_simple.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: attachments
3
- Version: 0.2.6
3
+ Version: 0.3.0
4
4
  Summary: A Python library to handle various file types for LLMs.
5
5
  Author-email: Maxim Rivest <mrive052@gmail.com>
6
6
  License-Expression: MIT
@@ -79,15 +79,11 @@ from attachments import Attachments
79
79
 
80
80
  pdf_attachment = Attachments("https://github.com/microsoft/markitdown/raw/refs/heads/main/packages/markitdown/tests/test_files/test.pdf")
81
81
 
82
- prompt = f"""
83
- Analyze the following documents:
84
- {pdf_attachment}
85
- """
86
-
87
- content = [{"type": "input_image","image_url": image} for image in pdf_attachment.images] + \
88
- [{"type": "input_text", "text": prompt}]
89
-
90
- response = OpenAI().responses.create(model="gpt-4.1-nano", input=[{"role": "user", "content": content}])
82
+ response = OpenAI().responses.create(
83
+ model="gpt-4.1-nano",
84
+ input=[{
85
+ "role": "user",
86
+ "content": pdf_attachment.to_openai_content("Analyze the following documents:")}])
91
87
  response.output_text
92
88
  ```
93
89
 
@@ -100,27 +96,10 @@ from attachments import Attachments
100
96
  pptx_file = Attachments(
101
97
  "https://github.com/microsoft/markitdown/raw/refs/heads/main/packages/markitdown/tests/test_files/test.pptx")
102
98
 
103
- prompt = f"""
104
- Analyze the following documents:
105
- {pptx_file}
106
- """
107
-
108
- content = [
109
- {
110
- "type": "image",
111
- "source": {
112
- "type": "base64",
113
- "media_type": image.split(";")[0].split(":")[1], # this is ugly but it works
114
- "data": image.split(",")[1] # this is ugly but it works
115
- }
116
- }
117
- for image in pptx_file.images
118
- ] + [{"type": "text", "text": prompt}]
119
-
120
99
  message = anthropic.Anthropic().messages.create(
121
100
  max_tokens=8192,
122
101
  model="claude-3-5-haiku-20241022",
123
- messages=[{"role": "user", "content": content}])
102
+ messages=[{"role": "user", "content": pptx_file.to_claude_content("Analyze the following documents:")}])
124
103
  print(message.content)
125
104
  ```
126
105
 
@@ -129,6 +108,7 @@ print(message.content)
129
108
  I am not a fan of the object that langchain and anthropic make you create, but hey!, I don't make the rules...
130
109
 
131
110
  ```python
111
+ #pip install -U langchain-anthropic
132
112
  from langchain.chat_models import init_chat_model
133
113
  from attachments import Attachments
134
114
 
@@ -137,21 +117,7 @@ docx_file = Attachments("https://github.com/microsoft/markitdown/raw/refs/heads/
137
117
  llm = init_chat_model("anthropic:claude-3-5-sonnet-latest")
138
118
 
139
119
  # Creation of a list of object as expected by langchain/anthropic
140
- content = [{"type": "text", "text": f"Analyze the following documents: {docx_file}"}]
141
-
142
- for image in docx_file.images
143
- media_type_i = image.split(";")[0].split(":")[1]
144
- media_data_i = image.split(",")[1]
145
- content.append({
146
- "type": "image",
147
- "source": {
148
- "type": "base64",
149
- "media_type": media_type_i,
150
- "data": media_data_i
151
- }
152
- })
153
-
154
-
120
+ content = docx_file.to_claude_content("Analyze the following documents:")
155
121
  message = {
156
122
  "role": "user",
157
123
  "content": content,
@@ -39,15 +39,11 @@ from attachments import Attachments
39
39
 
40
40
  pdf_attachment = Attachments("https://github.com/microsoft/markitdown/raw/refs/heads/main/packages/markitdown/tests/test_files/test.pdf")
41
41
 
42
- prompt = f"""
43
- Analyze the following documents:
44
- {pdf_attachment}
45
- """
46
-
47
- content = [{"type": "input_image","image_url": image} for image in pdf_attachment.images] + \
48
- [{"type": "input_text", "text": prompt}]
49
-
50
- response = OpenAI().responses.create(model="gpt-4.1-nano", input=[{"role": "user", "content": content}])
42
+ response = OpenAI().responses.create(
43
+ model="gpt-4.1-nano",
44
+ input=[{
45
+ "role": "user",
46
+ "content": pdf_attachment.to_openai_content("Analyze the following documents:")}])
51
47
  response.output_text
52
48
  ```
53
49
 
@@ -60,27 +56,10 @@ from attachments import Attachments
60
56
  pptx_file = Attachments(
61
57
  "https://github.com/microsoft/markitdown/raw/refs/heads/main/packages/markitdown/tests/test_files/test.pptx")
62
58
 
63
- prompt = f"""
64
- Analyze the following documents:
65
- {pptx_file}
66
- """
67
-
68
- content = [
69
- {
70
- "type": "image",
71
- "source": {
72
- "type": "base64",
73
- "media_type": image.split(";")[0].split(":")[1], # this is ugly but it works
74
- "data": image.split(",")[1] # this is ugly but it works
75
- }
76
- }
77
- for image in pptx_file.images
78
- ] + [{"type": "text", "text": prompt}]
79
-
80
59
  message = anthropic.Anthropic().messages.create(
81
60
  max_tokens=8192,
82
61
  model="claude-3-5-haiku-20241022",
83
- messages=[{"role": "user", "content": content}])
62
+ messages=[{"role": "user", "content": pptx_file.to_claude_content("Analyze the following documents:")}])
84
63
  print(message.content)
85
64
  ```
86
65
 
@@ -89,6 +68,7 @@ print(message.content)
89
68
  I am not a fan of the object that langchain and anthropic make you create, but hey!, I don't make the rules...
90
69
 
91
70
  ```python
71
+ #pip install -U langchain-anthropic
92
72
  from langchain.chat_models import init_chat_model
93
73
  from attachments import Attachments
94
74
 
@@ -97,21 +77,7 @@ docx_file = Attachments("https://github.com/microsoft/markitdown/raw/refs/heads/
97
77
  llm = init_chat_model("anthropic:claude-3-5-sonnet-latest")
98
78
 
99
79
  # Creation of a list of object as expected by langchain/anthropic
100
- content = [{"type": "text", "text": f"Analyze the following documents: {docx_file}"}]
101
-
102
- for image in docx_file.images
103
- media_type_i = image.split(";")[0].split(":")[1]
104
- media_data_i = image.split(",")[1]
105
- content.append({
106
- "type": "image",
107
- "source": {
108
- "type": "base64",
109
- "media_type": media_type_i,
110
- "data": media_data_i
111
- }
112
- })
113
-
114
-
80
+ content = docx_file.to_claude_content("Analyze the following documents:")
115
81
  message = {
116
82
  "role": "user",
117
83
  "content": content,
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "attachments"
7
- version = "0.2.6"
7
+ version = "0.3.0"
8
8
  authors = [
9
9
  { name="Maxim Rivest", email="mrive052@gmail.com" },
10
10
  ]
@@ -11,7 +11,7 @@ from typing import List, Dict, Tuple, Optional, Any, Union, cast
11
11
  from collections import defaultdict
12
12
 
13
13
  from .detectors import Detector
14
- from .parsers import ParserRegistry, PDFParser, PPTXParser, HTMLParser, ImageParser, AudioParser, DOCXParser, ODTParser
14
+ from .parsers import ParserRegistry, PDFParser, PPTXParser, HTMLParser, ImageParser, AudioParser, DOCXParser, ODTParser, TextParser
15
15
  from .renderers import RendererRegistry, DefaultXMLRenderer, PlainTextRenderer
16
16
  from .exceptions import DetectionError, ParsingError
17
17
  from .image_processing import DEFAULT_IMAGE_OUTPUT_FORMAT, DEFAULT_IMAGE_QUALITY
@@ -73,6 +73,7 @@ class Attachments:
73
73
 
74
74
  self.parser_registry.register('docx', DOCXParser)
75
75
  self.parser_registry.register('odt', ODTParser)
76
+ self.parser_registry.register('txt', TextParser) # Register TextParser
76
77
 
77
78
  # Register ImageParser class for various image types
78
79
  # The registry will create an instance for each, configured with self._config
@@ -188,14 +189,42 @@ class Attachments:
188
189
 
189
190
  file_type = self.detector.detect(path_for_detector_and_parser, content_type=detected_file_type_arg)
190
191
 
191
- if not file_type:
192
- print(f"Warning: Could not detect file type for '{path_for_detector_and_parser}' (from input '{path_str}'). Skipping.")
193
- continue
192
+ parser = None
193
+ original_detected_type = file_type # Keep track of initially detected type
194
194
 
195
- parser = self.parser_registry.get_parser(file_type)
195
+ if file_type:
196
+ try:
197
+ parser = self.parser_registry.get_parser(file_type)
198
+ except KeyError:
199
+ if self.verbose:
200
+ print(f"Warning: No parser registered for detected type '{file_type}' for '{path_for_detector_and_parser}' (from input '{path_str}'). Attempting to parse as plain text.")
201
+ file_type = 'txt' # Fallback to txt
202
+ else:
203
+ if self.verbose:
204
+ print(f"Warning: Could not detect file type for '{path_for_detector_and_parser}' (from input '{path_str}'). Attempting to parse as plain text.")
205
+ file_type = 'txt' # Fallback to txt
206
+
207
+ if not parser: # If parser is still None, it means we are in a fallback scenario or initial detection failed
208
+ try:
209
+ parser = self.parser_registry.get_parser(file_type) # This should get TextParser if file_type is 'txt'
210
+ except KeyError:
211
+ # This should ideally not happen if TextParser is registered, but as a safeguard:
212
+ print(f"Critical Warning: Fallback TextParser not found or not registered for type '{file_type}'. Cannot process '{path_for_detector_and_parser}'. Skipping.")
213
+ # Clean up temp file if it was created for a URL
214
+ if is_url and temp_file_path_for_parsing and os.path.exists(temp_file_path_for_parsing):
215
+ try:
216
+ os.remove(temp_file_path_for_parsing)
217
+ except Exception as e_clean_critical:
218
+ print(f"Warning: Could not clean up temp file {temp_file_path_for_parsing} during critical skip: {e_clean_critical}")
219
+ continue # Skip this file
220
+
221
+ # If file_type was overridden for fallback, ensure the parsed_content reflects that.
222
+ # However, we might want to store the original_detected_type if it existed.
196
223
  parsed_content = parser.parse(path_for_detector_and_parser, indices=indices)
197
224
 
198
- parsed_content['type'] = file_type
225
+ parsed_content['type'] = file_type # Use the final file_type (could be 'txt')
226
+ if original_detected_type and original_detected_type != file_type:
227
+ parsed_content['original_detected_type'] = original_detected_type
199
228
  parsed_content['id'] = f"{file_type}{i+1}"
200
229
  parsed_content['original_path_str'] = path_str
201
230
  parsed_content['file_path'] = original_file_path_or_url
@@ -150,6 +150,27 @@ class PPTXParser(BaseParser):
150
150
  except Exception as e:
151
151
  raise ParsingError(message=f"Failed to parse PPTX '{file_path}': {e}", file_path=file_path, parser_name=self.__class__.__name__) from e
152
152
 
153
+ class TextParser(BaseParser):
154
+ """Parses plain text files."""
155
+ def parse(self, file_path: str, indices: Optional[str] = None) -> Dict[str, Any]:
156
+ """Reads the file content as UTF-8 text.
157
+ `indices` argument is ignored for text files.
158
+ """
159
+ try:
160
+ with open(file_path, 'r', encoding='utf-8') as f:
161
+ text_content = f.read()
162
+ return {
163
+ "text": text_content,
164
+ "original_basename": os.path.basename(file_path),
165
+ "file_path": file_path,
166
+ }
167
+ except FileNotFoundError:
168
+ raise ParsingError(message=f"Text file not found: {file_path}", file_path=file_path, parser_name=self.__class__.__name__) from None
169
+ except UnicodeDecodeError as e:
170
+ raise ParsingError(message=f"Error decoding text file '{file_path}' as UTF-8: {e}. The file might not be plain text or uses a different encoding.", file_path=file_path, parser_name=self.__class__.__name__) from e
171
+ except Exception as e:
172
+ raise ParsingError(message=f"Failed to parse text file '{file_path}': {e}", file_path=file_path, parser_name=self.__class__.__name__) from e
173
+
153
174
  # ImageParser and AudioParser __init__ are fine from previous step.
154
175
  # Their full parse methods will be restored/verified after document parsers.
155
176
  class ImageParser(BaseParser):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: attachments
3
- Version: 0.2.6
3
+ Version: 0.3.0
4
4
  Summary: A Python library to handle various file types for LLMs.
5
5
  Author-email: Maxim Rivest <mrive052@gmail.com>
6
6
  License-Expression: MIT
@@ -79,15 +79,11 @@ from attachments import Attachments
79
79
 
80
80
  pdf_attachment = Attachments("https://github.com/microsoft/markitdown/raw/refs/heads/main/packages/markitdown/tests/test_files/test.pdf")
81
81
 
82
- prompt = f"""
83
- Analyze the following documents:
84
- {pdf_attachment}
85
- """
86
-
87
- content = [{"type": "input_image","image_url": image} for image in pdf_attachment.images] + \
88
- [{"type": "input_text", "text": prompt}]
89
-
90
- response = OpenAI().responses.create(model="gpt-4.1-nano", input=[{"role": "user", "content": content}])
82
+ response = OpenAI().responses.create(
83
+ model="gpt-4.1-nano",
84
+ input=[{
85
+ "role": "user",
86
+ "content": pdf_attachment.to_openai_content("Analyze the following documents:")}])
91
87
  response.output_text
92
88
  ```
93
89
 
@@ -100,27 +96,10 @@ from attachments import Attachments
100
96
  pptx_file = Attachments(
101
97
  "https://github.com/microsoft/markitdown/raw/refs/heads/main/packages/markitdown/tests/test_files/test.pptx")
102
98
 
103
- prompt = f"""
104
- Analyze the following documents:
105
- {pptx_file}
106
- """
107
-
108
- content = [
109
- {
110
- "type": "image",
111
- "source": {
112
- "type": "base64",
113
- "media_type": image.split(";")[0].split(":")[1], # this is ugly but it works
114
- "data": image.split(",")[1] # this is ugly but it works
115
- }
116
- }
117
- for image in pptx_file.images
118
- ] + [{"type": "text", "text": prompt}]
119
-
120
99
  message = anthropic.Anthropic().messages.create(
121
100
  max_tokens=8192,
122
101
  model="claude-3-5-haiku-20241022",
123
- messages=[{"role": "user", "content": content}])
102
+ messages=[{"role": "user", "content": pptx_file.to_claude_content("Analyze the following documents:")}])
124
103
  print(message.content)
125
104
  ```
126
105
 
@@ -129,6 +108,7 @@ print(message.content)
129
108
  I am not a fan of the object that langchain and anthropic make you create, but hey!, I don't make the rules...
130
109
 
131
110
  ```python
111
+ #pip install -U langchain-anthropic
132
112
  from langchain.chat_models import init_chat_model
133
113
  from attachments import Attachments
134
114
 
@@ -137,21 +117,7 @@ docx_file = Attachments("https://github.com/microsoft/markitdown/raw/refs/heads/
137
117
  llm = init_chat_model("anthropic:claude-3-5-sonnet-latest")
138
118
 
139
119
  # Creation of a list of object as expected by langchain/anthropic
140
- content = [{"type": "text", "text": f"Analyze the following documents: {docx_file}"}]
141
-
142
- for image in docx_file.images
143
- media_type_i = image.split(";")[0].split(":")[1]
144
- media_data_i = image.split(",")[1]
145
- content.append({
146
- "type": "image",
147
- "source": {
148
- "type": "base64",
149
- "media_type": media_type_i,
150
- "data": media_data_i
151
- }
152
- })
153
-
154
-
120
+ content = docx_file.to_claude_content("Analyze the following documents:")
155
121
  message = {
156
122
  "role": "user",
157
123
  "content": content,
@@ -282,5 +282,55 @@ class TestCoreFunctionality(unittest.TestCase):
282
282
  # It's unclear what this test is intended to check, so it's left unchanged
283
283
  pass
284
284
 
285
+ def test_fallback_to_text_parser(self):
286
+ """Test that files with unknown extensions or no extension fall back to TextParser."""
287
+ unknown_ext_content = "Unknown extension fallback test."
288
+ no_ext_content = "No extension fallback test."
289
+
290
+ temp_dir = TEST_DATA_DIR # Use the existing test data directory for temp files
291
+ unknown_ext_filepath = os.path.join(temp_dir, "test_unknown.unknownext")
292
+ no_ext_filepath = os.path.join(temp_dir, "test_no_ext_file")
293
+
294
+ try:
295
+ with open(unknown_ext_filepath, "w", encoding="utf-8") as f:
296
+ f.write(unknown_ext_content)
297
+ with open(no_ext_filepath, "w", encoding="utf-8") as f:
298
+ f.write(no_ext_content)
299
+
300
+ atts = Attachments(unknown_ext_filepath, no_ext_filepath, verbose=True) # Verbose for debugging if needed
301
+ self.assertEqual(len(atts.attachments_data), 2, "Should process two files with fallback")
302
+
303
+ # Sort by original path to ensure consistent order for assertions
304
+ sorted_attachments = sorted(atts.attachments_data, key=lambda x: x['original_path_str'])
305
+
306
+ # Test the file with no extension
307
+ # The order depends on how Attachments processes and potentially sorts them, or how listdir returns them.
308
+ # Assuming Attachments processes in order of input for now, or we find by path.
309
+ no_ext_data = next((item for item in sorted_attachments if item['original_path_str'] == no_ext_filepath), None)
310
+ self.assertIsNotNone(no_ext_data, "Data for no-extension file not found")
311
+ if no_ext_data: # Check to satisfy mypy
312
+ self.assertEqual(no_ext_data['type'], 'txt', "File with no extension should be parsed as txt")
313
+ self.assertEqual(no_ext_data['text'], no_ext_content, "Text content mismatch for no-extension file")
314
+ self.assertIsNone(no_ext_data.get('original_detected_type'), "original_detected_type should be None for no-extension file")
315
+
316
+ # Test the file with an unknown extension
317
+ unknown_ext_data = next((item for item in sorted_attachments if item['original_path_str'] == unknown_ext_filepath), None)
318
+ self.assertIsNotNone(unknown_ext_data, "Data for unknown-extension file not found")
319
+ if unknown_ext_data: # Check to satisfy mypy
320
+ self.assertEqual(unknown_ext_data['type'], 'txt', "File with unknown extension should be parsed as txt")
321
+ self.assertEqual(unknown_ext_data['text'], unknown_ext_content, "Text content mismatch for unknown-extension file")
322
+ self.assertIsNone(unknown_ext_data.get('original_detected_type'), "original_detected_type should be None for unknown-extension file")
323
+
324
+ finally:
325
+ # Clean up temporary files
326
+ if os.path.exists(unknown_ext_filepath):
327
+ os.remove(unknown_ext_filepath)
328
+ if os.path.exists(no_ext_filepath):
329
+ os.remove(no_ext_filepath)
330
+
331
+ def test_initialize_attachments_with_list_of_paths(self):
332
+ """Test initializing Attachments with a list of paths."""
333
+ # ... existing code ...
334
+
285
335
  if __name__ == '__main__':
286
336
  unittest.main()
File without changes
File without changes