From c60fd1f85d610e0a9bbf18eca290b97d5abf6d9e Mon Sep 17 00:00:00 2001 From: Sebastian Laverde Alfonso Date: Wed, 4 Jan 2023 15:32:02 +0100 Subject: [PATCH] chore: merge latests updates from main (#126) * feat: extract metadata from `.docx`, `.xlsx`, and `.jpg` (#113) * add python-docx dependency * added function for extracting metadata from word documents * add openpyxl * added get_jpg_metadata; fixed typing * bump changelog * added pillow to dependencies * build(deps): Bump transformers from 4.23.1 to 4.25.1 in /requirements (#114) Bumps [transformers](https://github.com/huggingface/transformers) from 4.23.1 to 4.25.1. - [Release notes](https://github.com/huggingface/transformers/releases) - [Commits](https://github.com/huggingface/transformers/compare/v4.23.1...v4.25.1) --- updated-dependencies: - dependency-name: transformers dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps-dev): Bump pip-tools from 6.10.0 to 6.12.1 in /requirements (#115) Bumps [pip-tools](https://github.com/jazzband/pip-tools) from 6.10.0 to 6.12.1. - [Release notes](https://github.com/jazzband/pip-tools/releases) - [Changelog](https://github.com/jazzband/pip-tools/blob/main/CHANGELOG.md) - [Commits](https://github.com/jazzband/pip-tools/compare/6.10.0...6.12.1) --- updated-dependencies: - dependency-name: pip-tools dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Matt Robinson * build(deps): Bump torch from 1.13.0 to 1.13.1 in /requirements (#117) Bumps [torch](https://github.com/pytorch/pytorch) from 1.13.0 to 1.13.1. - [Release notes](https://github.com/pytorch/pytorch/releases) - [Changelog](https://github.com/pytorch/pytorch/blob/master/RELEASE.md) - [Commits](https://github.com/pytorch/pytorch/compare/v1.13.0...v1.13.1) --- updated-dependencies: - dependency-name: torch dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Matt Robinson * build(deps): Bump lxml from 4.9.1 to 4.9.2 in /requirements (#118) Bumps [lxml](https://github.com/lxml/lxml) from 4.9.1 to 4.9.2. - [Release notes](https://github.com/lxml/lxml/releases) - [Changelog](https://github.com/lxml/lxml/blob/master/CHANGES.txt) - [Commits](https://github.com/lxml/lxml/compare/lxml-4.9.1...lxml-4.9.2) --- updated-dependencies: - dependency-name: lxml dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): Bump mypy from 0.990 to 0.991 in /requirements (#123) Bumps [mypy](https://github.com/python/mypy) from 0.990 to 0.991. - [Release notes](https://github.com/python/mypy/releases) - [Commits](https://github.com/python/mypy/compare/v0.990...v0.991) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): Bump packaging from 21.3 to 22.0 in /requirements (#119) Bumps [packaging](https://github.com/pypa/packaging) from 21.3 to 22.0. - [Release notes](https://github.com/pypa/packaging/releases) - [Changelog](https://github.com/pypa/packaging/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pypa/packaging/compare/21.3...22.0) --- updated-dependencies: - dependency-name: packaging dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Matt Robinson * build(deps): Bump pytz from 2022.6 to 2022.7 in /requirements (#122) Bumps [pytz](https://github.com/stub42/pytz) from 2022.6 to 2022.7. - [Release notes](https://github.com/stub42/pytz/releases) - [Commits](https://github.com/stub42/pytz/compare/release_2022.6...release_2022.7) --- updated-dependencies: - dependency-name: pytz dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): Bump pillow from 9.3.0 to 9.4.0 in /requirements (#120) Bumps [pillow](https://github.com/python-pillow/Pillow) from 9.3.0 to 9.4.0. - [Release notes](https://github.com/python-pillow/Pillow/releases) - [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) - [Commits](https://github.com/python-pillow/Pillow/compare/9.3.0...9.4.0) --- updated-dependencies: - dependency-name: pillow dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * feat: Add `extract_attachment_info` (#112) * Adds function to extract attachments and their metadata from eml files * feat: helper functions to identify and extract phone numbers (#124) * added pattern for finding phone numbers * added cleaning brick for extracting phone numbers * add docs * changelog and bump version * switch to us phone numbers * bump dev version Signed-off-by: dependabot[bot] Co-authored-by: Matt Robinson Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Mallori Harrell <6825104+mallorih@users.noreply.github.com> --- CHANGELOG.md | 6 +- docs/requirements.txt | 6 +- docs/source/bricks.rst | 47 ++++++ docs/source/examples.rst | 36 +++++ example-docs/example.jpg | Bin 0 -> 32764 bytes example-docs/fake-email-attachment.eml | 50 ++++++ example-docs/fake-excel.xlsx | Bin 0 -> 4765 bytes example-docs/fake.docx | Bin 0 -> 36602 bytes requirements/base.txt | 20 ++- requirements/build.txt | 6 +- requirements/dev.txt | 30 +--- requirements/huggingface.txt | 24 ++- requirements/test.txt | 29 ++-- setup.py | 5 +- test_unstructured/cleaners/test_extract.py | 13 ++ test_unstructured/file_utils/test_metadata.py | 107 ++++++++++++ test_unstructured/partition/test_email.py | 16 +- test_unstructured/partition/test_text_type.py | 18 +++ unstructured/__version__.py | 2 +- unstructured/cleaners/extract.py | 19 +++ unstructured/file_utils/__init__.py | 0 unstructured/file_utils/metadata.py | 152 ++++++++++++++++++ unstructured/nlp/patterns.py | 8 + unstructured/partition/email.py | 33 +++- unstructured/partition/text_type.py | 12 +- 25 files changed, 570 insertions(+), 69 deletions(-) create mode 100644 example-docs/example.jpg create mode 100644 example-docs/fake-email-attachment.eml create mode 100644 example-docs/fake-excel.xlsx create mode 100644 example-docs/fake.docx create mode 100644 test_unstructured/file_utils/test_metadata.py create mode 100644 unstructured/file_utils/__init__.py create mode 100644 unstructured/file_utils/metadata.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cf4cc9dfa..d110cee72f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,13 @@ -## 0.3.5-dev2 +## 0.3.5-dev5 * Add new pattern to recognize plain text dash bullets * Add test for bullet patterns * Fix for `partition_html` that allows for processing `div` tags that have both text and child elements +* Add ability to extract document metadata from `.docx`, `.xlsx`, and `.jpg` files. +* Helper functions for identifying and extracting phone numbers +* Add new function `extract_attachment_info` that extracts and decode the attachment +of an email. ## 0.3.4 diff --git a/docs/requirements.txt b/docs/requirements.txt index e1cd1e41a7..8f01b64b7a 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with python 3.8 +# This file is autogenerated by pip-compile with python 3.10 # To update, run: # # pip-compile requirements/build.in @@ -22,8 +22,6 @@ idna==3.4 # via requests imagesize==1.4.1 # via sphinx -importlib-metadata==5.0.0 - # via sphinx jinja2==3.1.2 # via sphinx markupsafe==2.1.1 @@ -60,5 +58,3 @@ sphinxcontrib-serializinghtml==1.1.5 # via sphinx urllib3==1.26.12 # via requests -zipp==3.10.0 - # via importlib-metadata diff --git a/docs/source/bricks.rst b/docs/source/bricks.rst index ddf64d76f5..bfee378091 100644 --- a/docs/source/bricks.rst +++ b/docs/source/bricks.rst @@ -77,6 +77,23 @@ Examples: text = f.read() elements = partition_email(text=text) +``extract_attachment_info`` +---------------------- + +The ``extract_attachment_info`` function takes an ``email.message.Message`` object +as input and returns the a list of dictionaries containing the attachment information, +such as ``filename``, ``size``, ``payload``, etc. The attachment is saved to the ``output_dir`` +if specified. + +.. code:: python + + import email + from unstructured.partition.email import extract_attachment_info + + with open("example-docs/fake-email-attachment.eml", "r") as f: + msg = email.message_from_file(f) + attachment_info = extract_attachment_info(msg, output_dir="example-docs") + ``is_bulleted_text`` ---------------------- @@ -162,6 +179,21 @@ Examples: is_possible_title(example_3, sentence_min_length=5) +``contains_us_phone_number`` +---------------------------- + +Checks to see if a section of text contains a US phone number. + +Examples: + +.. code:: python + + from unstructured.partition.text_type import contains_us_phone_number + + # Returns True because the text includes a phone number + contains_us_phone_number("Phone number: 215-867-5309") + + ``contains_verb`` ----------------- @@ -471,6 +503,21 @@ Examples: extract_text_after(text, r"SPEAKER \d{1}:") +``extract_us_phone_number`` +--------------------------- + +Extracts a phone number from a section of text. + +Examples: + +.. code:: python + + from unstructured.cleaners.extract import extract_us_phone_number + + # Returns "215-867-5309" + extract_us_phone_number("Phone number: 215-867-5309") + + ``translate_text`` ------------------ diff --git a/docs/source/examples.rst b/docs/source/examples.rst index 4a400afeba..416b20d5f5 100644 --- a/docs/source/examples.rst +++ b/docs/source/examples.rst @@ -234,3 +234,39 @@ for reading in a document with an XSLT stylesheet is as follows: If you read from a stylesheet ``HTMLDocument`` will use the ``etree.XMLParser`` by default instead of the ``etree.HTMLParser`` because ``HTMLDocument`` assumes you want to convert your raw XML to HTML. + + +################################## +Extracting Metadata from Documents +################################## + +The ``unstructured`` library includes utilities for extracting metadata from +documents. Currently, there is support for extracting metadata from ``.docx``, +``.xlsx``, and ``.jpg`` documents. When you call these functions, the return type +is a ``Metadata`` data class that you can convert to a dictionary by calling the +``to_dict()`` method. If you extract metadata from a ``.jpg`` document, the output +will include EXIF metadata in the ``exif_data`` attribute, if it is available. +Here is an example of how to use the metadata extraction functionality: + + +.. code:: python + + from unstructured.file_utils.metadata import get_jpg_metadata + + filename = "example-docs/example.jpg" + metadata = get_jpg_metadata(filename=filename) + + +You can also pass in a file-like object with: + +.. code:: python + + from unstructured.file_utils.metadata import get_jpg_metadata + + filename = "example-docs/example.jpg" + with open(filename, "rb") as f: + metadata = get_jpg_metadata(file=f) + + +To extract metadata from ``.docx`` or ``.xlsx``, use ``get_docx_metadata`` and +``get_xlsx_metadata``. The interfaces are the same as ``get_jpg_metadata``. diff --git a/example-docs/example.jpg b/example-docs/example.jpg new file mode 100644 index 0000000000000000000000000000000000000000..933719d1cf71cfe3e1794e258eb56a2c15c6fa40 GIT binary patch literal 32764 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 +Subject: Fake email with attachment +From: Mallori Harrell +To: Mallori Harrell +Content-Type: multipart/mixed; boundary="0000000000005d654405f082adb7" + +--0000000000005d654405f082adb7 +Content-Type: multipart/alternative; boundary="0000000000005d654205f082adb5" + +--0000000000005d654205f082adb5 +Content-Type: text/plain; charset="UTF-8" + +Hello! + +Here's the attachments! + +It includes: + + - Lots of whitespace + - Little to no content + - and is a quick read + +Best, + +Mallori + +--0000000000005d654205f082adb5 +Content-Type: text/html; charset="UTF-8" +Content-Transfer-Encoding: quoted-printable + +
Hello!=C2=A0

Here's the attachments= +!

It includes:
  • Lots of whitespace
  • Little=C2= +=A0to no content
  • and is a quick read
Best,

Mallori

+ +--0000000000005d654205f082adb5-- +--0000000000005d654405f082adb7 +Content-Type: text/plain; charset="US-ASCII"; name="fake-attachment.txt" +Content-Disposition: attachment; filename="fake-attachment.txt" +Content-Transfer-Encoding: base64 +X-Attachment-Id: f_lc0tto5j0 +Content-ID: + +SGV5IHRoaXMgaXMgYSBmYWtlIGF0dGFjaG1lbnQh +--0000000000005d654405f082adb7-- \ No newline at end of file diff --git a/example-docs/fake-excel.xlsx b/example-docs/fake-excel.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..4aefebe298ef65ba27afd561f281b571694cc101 GIT binary patch literal 4765 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 s@~r_HDkvQ=2022.12.07", diff --git a/test_unstructured/cleaners/test_extract.py b/test_unstructured/cleaners/test_extract.py index 1a00ec542e..2f215c95a4 100644 --- a/test_unstructured/cleaners/test_extract.py +++ b/test_unstructured/cleaners/test_extract.py @@ -21,3 +21,16 @@ def test_extract_text_before(): def test_extract_text_after(): text = "Teacher: BLAH BLAH BLAH; Student: BLAH BLAH BLAH!" assert extract.extract_text_after(text, "BLAH;", 0) == "Student: BLAH BLAH BLAH!" + + +@pytest.mark.parametrize( + "text, expected", + [ + ("215-867-5309", "215-867-5309"), + ("Phone Number: +1 215.867.5309", "+1 215.867.5309"), + ("Phone Number: Just Kidding", ""), + ], +) +def test_extract_us_phone_number(text, expected): + phone_number = extract.extract_us_phone_number(text) + assert phone_number == expected diff --git a/test_unstructured/file_utils/test_metadata.py b/test_unstructured/file_utils/test_metadata.py new file mode 100644 index 0000000000..ccd34c5f9b --- /dev/null +++ b/test_unstructured/file_utils/test_metadata.py @@ -0,0 +1,107 @@ +import datetime +import os +import pathlib +import pytest + +import docx +import openpyxl + +import unstructured.file_utils.metadata as meta + +DIRECTORY = pathlib.Path(__file__).parent.resolve() +EXAMPLE_JPG_FILENAME = os.path.join(DIRECTORY, "..", "..", "example-docs", "example.jpg") + + +def test_get_docx_metadata_from_filename(tmpdir): + filename = os.path.join(tmpdir, "test-doc.docx") + + document = docx.Document() + document.add_paragraph("Lorem ipsum dolor sit amet.") + document.core_properties.author = "Mr. Miagi" + document.save(filename) + + metadata = meta.get_docx_metadata(filename=filename) + assert metadata.author == "Mr. Miagi" + assert metadata.to_dict()["author"] == "Mr. Miagi" + + +def test_get_docx_metadata_from_file(tmpdir): + filename = os.path.join(tmpdir, "test-doc.docx") + + document = docx.Document() + document.add_paragraph("Lorem ipsum dolor sit amet.") + document.core_properties.author = "Mr. Miagi" + document.save(filename) + + with open(filename, "rb") as f: + metadata = meta.get_docx_metadata(file=f) + assert metadata.author == "Mr. Miagi" + + +def test_get_docx_metadata_raises_without_file_or_filename(): + with pytest.raises(FileNotFoundError): + meta.get_docx_metadata() + + +def test_get_xlsx_metadata_from_filename(tmpdir): + filename = os.path.join(tmpdir, "test-excel.xlsx") + + workbook = openpyxl.Workbook() + workbook.properties.creator = "Mr. Miagi" + workbook.save(filename) + + metadata = meta.get_xlsx_metadata(filename=filename) + metadata.author = "Mr. Miagi" + + +def test_get_xlsx_metadata_from_file(tmpdir): + filename = os.path.join(tmpdir, "test-excel.xlsx") + + workbook = openpyxl.Workbook() + workbook.properties.creator = "Mr. Miagi" + workbook.save(filename) + + with open(filename, "rb") as f: + metadata = meta.get_xlsx_metadata(file=f) + metadata.author = "Mr. Miagi" + + +def test_get_xlsx_metadata_raises_without_file_or_filename(): + with pytest.raises(FileNotFoundError): + meta.get_xlsx_metadata() + + +def test_get_jpg_metadata_from_filename(): + metadata = meta.get_jpg_metadata(filename=EXAMPLE_JPG_FILENAME) + assert metadata.modified == datetime.datetime(2003, 12, 14, 12, 1, 44) + assert metadata.exif_data["Make"] == "Canon" + + +def test_get_jpg_metadata_from_file(): + with open(EXAMPLE_JPG_FILENAME, "rb") as f: + metadata = meta.get_jpg_metadata(file=f) + assert metadata.modified == datetime.datetime(2003, 12, 14, 12, 1, 44) + assert metadata.exif_data["Make"] == "Canon" + + +def test_get_jpg_metadata_raises_without_file_or_filename(): + with pytest.raises(FileNotFoundError): + meta.get_jpg_metadata() + + +def test_get_exif_datetime(): + exif_data = {"DateTime": "2022:12:23 15:49:00", "DateTimeOriginal": "2020:12:14 12:00:00"} + date = meta._get_exif_datetime(exif_data, "DateTime") + assert date == datetime.datetime(2022, 12, 23, 15, 49, 0) + + +def test_get_exif_datetime_ignores_bad_formats(): + exif_data = {"DateTime": "2022-12-23TZ15:49:00", "DateTimeOriginal": "2020:12:14 12:00:00"} + date = meta._get_exif_datetime(exif_data, "DateTime") + assert date is None + + +def test_get_exif_datetime_ignores_missing_key(): + exif_data = {"Datetime": "2022-12-23TZ15:49:00", "DateTimeOriginal": "2020:12:14 12:00:00"} + date = meta._get_exif_datetime(exif_data, "DateTimeDigitized") + assert date is None diff --git a/test_unstructured/partition/test_email.py b/test_unstructured/partition/test_email.py index 939c39a195..4aefc18ffb 100644 --- a/test_unstructured/partition/test_email.py +++ b/test_unstructured/partition/test_email.py @@ -1,9 +1,10 @@ +import email import os import pathlib import pytest from unstructured.documents.elements import NarrativeText, Title, ListItem -from unstructured.partition.email import partition_email +from unstructured.partition.email import partition_email, extract_attachment_info DIRECTORY = pathlib.Path(__file__).parent.resolve() @@ -16,6 +17,10 @@ ListItem(text="Violets are blue"), ] +ATTACH_EXPECTED_OUTPUT = [ + {"filename": "fake-attachment.txt", "payload": b"Hey this is a fake attachment!"} +] + def test_partition_email_from_filename(): filename = os.path.join(DIRECTORY, "..", "..", "example-docs", "fake-email.eml") @@ -41,6 +46,15 @@ def test_partition_email_from_text(): assert elements == EXPECTED_OUTPUT +def test_extract_attachment_info(): + filename = os.path.join(DIRECTORY, "..", "..", "example-docs", "fake-email-attachment.eml") + with open(filename, "r") as f: + msg = email.message_from_file(f) + attachment_info = extract_attachment_info(msg) + assert len(attachment_info) > 0 + assert attachment_info == ATTACH_EXPECTED_OUTPUT + + def test_partition_email_raises_with_none_specified(): with pytest.raises(ValueError): partition_email() diff --git a/test_unstructured/partition/test_text_type.py b/test_unstructured/partition/test_text_type.py index c53064b456..1a1516f430 100644 --- a/test_unstructured/partition/test_text_type.py +++ b/test_unstructured/partition/test_text_type.py @@ -67,6 +67,24 @@ def test_is_possible_title(text, expected, monkeypatch): assert has_verb is expected +@pytest.mark.parametrize( + "text, expected", + [ + ("8675309", True), + ("+1 867-5309", True), + ("2158675309", True), + ("+12158675309", True), + ("867.5309", True), + ("1-800-867-5309", True), + ("1(800)-867-5309", True), + ("Tel: 1(800)-867-5309", True), + ], +) +def test_contains_us_phone_number(text, expected): + has_phone_number = text_type.contains_us_phone_number(text) + assert has_phone_number is expected + + @pytest.mark.parametrize( "text, expected", [ diff --git a/unstructured/__version__.py b/unstructured/__version__.py index e9ec5f2cfc..3232b6506b 100644 --- a/unstructured/__version__.py +++ b/unstructured/__version__.py @@ -1 +1 @@ -__version__ = "0.3.5-dev2" # pragma: no cover +__version__ = "0.3.5-dev5" # pragma: no cover diff --git a/unstructured/cleaners/extract.py b/unstructured/cleaners/extract.py index db396886f9..03e1746fe9 100644 --- a/unstructured/cleaners/extract.py +++ b/unstructured/cleaners/extract.py @@ -1,5 +1,7 @@ import re +from unstructured.nlp.patterns import US_PHONE_NUMBERS_RE + def _get_indexed_match(text: str, pattern: str, index: int = 0) -> re.Match: if not isinstance(index, int) or index < 0: @@ -44,3 +46,20 @@ def extract_text_after(text: str, pattern: str, index: int = 0, strip: bool = Tr _, end = regex_match.span() before_text = text[end:] return before_text.lstrip() if strip else before_text + + +def extract_us_phone_number(text: str): + """Extracts a US phone number from a section of text that includes a phone number. If there + is no phone number present, the result will be an empty string. + + Example + ------- + extract_phone_number("Phone Number: 215-867-5309") -> "215-867-5309" + """ + regex_match = US_PHONE_NUMBERS_RE.search(text) + if regex_match is None: + return "" + + start, end = regex_match.span() + phone_number = text[start:end] + return phone_number.strip() diff --git a/unstructured/file_utils/__init__.py b/unstructured/file_utils/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/unstructured/file_utils/metadata.py b/unstructured/file_utils/metadata.py new file mode 100644 index 0000000000..b46a70b636 --- /dev/null +++ b/unstructured/file_utils/metadata.py @@ -0,0 +1,152 @@ +from dataclasses import dataclass, field +import datetime +import io +from typing import Any, Dict, IO, Final, Optional + +import docx +import openpyxl +from PIL import Image +from PIL.ExifTags import TAGS + +# NOTE(robison) - ref: https://www.media.mit.edu/pia/Research/deepview/exif.html +EXIF_DATETIME_FMT: Final[str] = "%Y:%m:%d %H:%M:%S" + + +@dataclass +class Metadata: + author: str = "" + category: str = "" + comments: str = "" + content_status: str = "" + created: Optional[datetime.datetime] = None + identifier: str = "" + keywords: str = "" + language: str = "" + last_modified_by: str = "" + last_printed: Optional[datetime.datetime] = None + modified: Optional[datetime.datetime] = None + revision: Optional[int] = 0 + subject: str = "" + title: str = "" + version: str = "" + description: str = "" + namespace: str = "" + + # NOTE(robinson) - Metadata for use with image files + exif_data: Dict[str, Any] = field(default_factory=dict) + + def to_dict(self): + return self.__dict__ + + +def get_docx_metadata( + filename: str = "", + file: Optional[IO] = None, +) -> Metadata: + """Extracts document metadata from a Microsoft .docx document.""" + if filename: + doc = docx.Document(filename) + elif file: + doc = docx.Document(file) + else: + raise FileNotFoundError("No filename nor file were specified") + + metadata = Metadata( + author=getattr(doc.core_properties, "author", ""), + category=getattr(doc.core_properties, "category", ""), + comments=getattr(doc.core_properties, "comments", ""), + content_status=getattr(doc.core_properties, "content_status", ""), + created=getattr(doc.core_properties, "created", None), + identifier=getattr(doc.core_properties, "identifier", ""), + keywords=getattr(doc.core_properties, "keywords", ""), + language=getattr(doc.core_properties, "language", ""), + last_modified_by=getattr(doc.core_properties, "last_modified_by", ""), + last_printed=getattr(doc.core_properties, "last_printed", None), + modified=getattr(doc.core_properties, "modified", None), + revision=getattr(doc.core_properties, "revision", None), + subject=getattr(doc.core_properties, "subject", ""), + title=getattr(doc.core_properties, "title", ""), + version=getattr(doc.core_properties, "version", ""), + ) + + return metadata + + +def get_xlsx_metadata( + filename: str = "", + file: Optional[IO] = None, +) -> Metadata: + """Extracts document metadata from a Microsoft .xlsx document.""" + if filename: + workbook = openpyxl.load_workbook(filename) + elif file: + workbook = openpyxl.load_workbook(file) + else: + raise FileNotFoundError("No filename nor file were specified") + + metadata = Metadata( + author=getattr(workbook.properties, "creator", ""), + category=getattr(workbook.properties, "category", ""), + content_status=getattr(workbook.properties, "contentStatus", ""), + created=getattr(workbook.properties, "created", None), + description=getattr(workbook.properties, "description", ""), + identifier=getattr(workbook.properties, "identifier", ""), + keywords=getattr(workbook.properties, "keywords", ""), + language=getattr(workbook.properties, "language", ""), + last_modified_by=getattr(workbook.properties, "lastModifiedBy", ""), + last_printed=getattr(workbook.properties, "lastPrinted", None), + modified=getattr(workbook.properties, "modified", None), + namespace=getattr(workbook.properties, "namespace", ""), + revision=getattr(workbook.properties, "revision", None), + subject=getattr(workbook.properties, "subject", ""), + title=getattr(workbook.properties, "title", ""), + version=getattr(workbook.properties, "version", ""), + ) + + return metadata + + +def get_jpg_metadata( + filename: str = "", + file: Optional[IO] = None, +) -> Metadata: + """Extracts metadata from a JPG image, including EXIF metadata.""" + if filename: + image = Image.open(filename) + elif file: + image = Image.open(io.BytesIO(file.read())) + else: + raise FileNotFoundError("No filename nor file were specified") + + exif_data = image.getexif() + exif_dict: Dict[str, Any] = dict() + for tag_id in exif_data: + tag = TAGS.get(tag_id, tag_id) + data = exif_data.get(tag_id) + exif_dict[tag] = data + + metadata = Metadata( + author=exif_dict.get("Artist", ""), + comments=exif_dict.get("UserComment", ""), + created=_get_exif_datetime(exif_dict, "DateTimeOriginal"), + # NOTE(robinson) - Per EXIF docs, DateTime is the last modified data + # ref: https://www.media.mit.edu/pia/Research/deepview/exif.html + modified=_get_exif_datetime(exif_dict, "DateTime"), + exif_data=exif_dict, + ) + + return metadata + + +def _get_exif_datetime(exif_dict: Dict[str, Any], key: str) -> Optional[datetime.datetime]: + """Converts a datetime string from the EXIF data to a Python datetime object.""" + date = exif_dict.get(key) + if not date: + return None + + try: + return datetime.datetime.strptime(date, EXIF_DATETIME_FMT) + # NOTE(robinson) - An exception could occur if the datetime is not formatted + # using the standard EXIF datetime format + except ValueError: + return None diff --git a/unstructured/nlp/patterns.py b/unstructured/nlp/patterns.py index ac29ce3974..5620f26ca3 100644 --- a/unstructured/nlp/patterns.py +++ b/unstructured/nlp/patterns.py @@ -8,6 +8,14 @@ import re +# NOTE(robinson) - Modified from answers found on this stackoverflow post +# ref: https://stackoverflow.com/questions/16699007/ +# regular-expression-to-match-standard-10-digit-phone-number +US_PHONE_NUMBERS_PATTERN = ( + r"(?:\+?(\d{1,3}))?[-. (]*(\d{3})?[-. )]*(\d{3})[-. ]*(\d{4})(?: *x(\d+))?\s*$" +) +US_PHONE_NUMBERS_RE = re.compile(US_PHONE_NUMBERS_PATTERN) + UNICODE_BULLETS: Final[List[str]] = [ "\u0095", "\u2022", diff --git a/unstructured/partition/email.py b/unstructured/partition/email.py index 117f0b34e5..21abccaecb 100644 --- a/unstructured/partition/email.py +++ b/unstructured/partition/email.py @@ -1,5 +1,6 @@ import email import sys +from email.message import Message from typing import Dict, IO, List, Optional if sys.version_info < (3, 8): @@ -7,7 +8,7 @@ else: from typing import Final -from unstructured.cleaners.core import replace_mime_encodings +from unstructured.cleaners.core import replace_mime_encodings, clean_extra_whitespace from unstructured.documents.elements import Element, Text from unstructured.partition.html import partition_html @@ -15,6 +16,36 @@ VALID_CONTENT_SOURCES: Final[List[str]] = ["text/html"] +def extract_attachment_info( + message: Message, output_dir: Optional[str] = None +) -> List[Dict[str, str]]: + list_attachments = [] + attachment_info = {} + for part in message.walk(): + if "content-disposition" in part: + cdisp = part["content-disposition"].split(";") + cdisp = [clean_extra_whitespace(item) for item in cdisp] + + for item in cdisp: + if item.lower() == "attachment": + continue + key, value = item.split("=") + key = clean_extra_whitespace(key.replace('"', "")) + value = clean_extra_whitespace(value.replace('"', "")) + attachment_info[clean_extra_whitespace(key)] = clean_extra_whitespace(value) + attachment_info["payload"] = part.get_payload(decode=True) + list_attachments.append(attachment_info) + + for attachment in list_attachments: + if output_dir: + filename = output_dir + "/" + attachment["filename"] + with open(filename, "wb") as f: + # mypy wants to just us `w` when opening the file but this + # causes an error since the payloads are bytes not str + f.write(attachment["payload"]) # type: ignore + return list_attachments + + def partition_email( filename: Optional[str] = None, file: Optional[IO] = None, diff --git a/unstructured/partition/text_type.py b/unstructured/partition/text_type.py index 64fada10a8..4b8f6e45c6 100644 --- a/unstructured/partition/text_type.py +++ b/unstructured/partition/text_type.py @@ -9,7 +9,7 @@ from typing import Final from unstructured.cleaners.core import remove_punctuation -from unstructured.nlp.patterns import UNICODE_BULLETS_RE +from unstructured.nlp.patterns import US_PHONE_NUMBERS_RE, UNICODE_BULLETS_RE from unstructured.nlp.tokenize import pos_tag, sent_tokenize, word_tokenize from unstructured.logger import logger @@ -63,6 +63,16 @@ def is_bulleted_text(text: str) -> bool: return UNICODE_BULLETS_RE.match(text.strip()) is not None +def contains_us_phone_number(text: str) -> bool: + """Checks to see if a section of text contains a US phone number. + + Example + ------- + contains_us_phone_number("867-5309") -> True + """ + return US_PHONE_NUMBERS_RE.search(text.strip()) is not None + + def contains_verb(text: str) -> bool: """Use a POS tagger to check if a segment contains verbs. If the section does not have verbs, that indicates that it is not narrative text."""