airbyte-source-box-data-extract 0.1.7__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.
@@ -0,0 +1,129 @@
1
+ Metadata-Version: 2.3
2
+ Name: airbyte-source-box-data-extract
3
+ Version: 0.1.7
4
+ Summary: Source implementation for Box File Text Extraction.
5
+ License: MIT
6
+ Author: Airbyte
7
+ Author-email: contact@airbyte.io
8
+ Requires-Python: >=3.11,<3.12
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Requires-Dist: airbyte-cdk (>=6,<7)
13
+ Requires-Dist: box-sdk-gen (>=1.11.1,<2.0.0)
14
+ Project-URL: Documentation, https://docs.airbyte.com/integrations/sources/box-files-text
15
+ Project-URL: Homepage, https://airbyte.com
16
+ Project-URL: Repository, https://github.com/airbytehq/airbyte
17
+ Description-Content-Type: text/markdown
18
+
19
+ # Box Data Extract Source
20
+
21
+ This is the repository for the Box Generated source connector, written in Python.
22
+ For information about how to use this connector within Airbyte, see [the documentation](https://docs.airbyte.com/integrations/sources/box-data-extract).
23
+
24
+ This connector uses Box AI to extract data directly from documents stored in Box.
25
+
26
+ For example, a company managing lease contracts can automatically capture key details and populate their system of record. Likewise, a financial institution can extract critical data from loan application documents—such as bank statements and W-2s—and integrate it into approval workflows.
27
+
28
+ By prioritizing content intelligence, this connector unlocks new opportunities for automation and AI-driven insights.
29
+
30
+ ## Local development
31
+
32
+ ### Prerequisites
33
+
34
+ * Python (`^3.11`)
35
+ * Poetry (`^1.8`) - installation instructions [here](https://python-poetry.org/docs/#installation)
36
+
37
+
38
+
39
+ ### Installing the connector
40
+
41
+ From this connector directory, run:
42
+ ```bash
43
+ poetry install --with dev
44
+ ```
45
+
46
+
47
+ ### Create credentials
48
+
49
+ **If you are a community contributor**, follow the instructions in the [documentation](https://docs.airbyte.com/integrations/sources/box-data-extract)
50
+ to generate the necessary credentials. Then create a file `secrets/config.json` conforming to the `src/source_box_data_extract/spec.yaml` file.
51
+ Note that any directory named `secrets` is gitignored across the entire Airbyte repo, so there is no danger of accidentally checking in sensitive information.
52
+ See `sample_files/sample_config.json` for a sample config file.
53
+
54
+
55
+ ### Locally running the connector
56
+
57
+ ```
58
+ poetry run source-box-data-extract spec
59
+ poetry run source-box-data-extract check --config secrets/config.json
60
+ poetry run source-box-data-extract discover --config secrets/config.json
61
+ poetry run source-box-data-extract read --config secrets/config.json --catalog sample_files/configured_catalog.json
62
+ ```
63
+
64
+ ### Running tests
65
+
66
+ To run tests locally, from the connector directory run:
67
+
68
+ ```
69
+ poetry run pytest tests
70
+ ```
71
+
72
+ ### Building the docker image
73
+
74
+ 1. Install [`airbyte-ci`](https://github.com/airbytehq/airbyte/blob/master/airbyte-ci/connectors/pipelines/README.md)
75
+ 2. Run the following command to build the docker image:
76
+ ```bash
77
+ airbyte-ci connectors --name=source-box-data-extract build
78
+ ```
79
+
80
+ An image will be available on your host with the tag `airbyte/source-box-data-extract:dev`.
81
+
82
+
83
+ ### Running as a docker container
84
+
85
+ Then run any of the connector commands as follows:
86
+ ```
87
+ docker run --rm airbyte/source-box-data-extract:dev spec
88
+ docker run --rm -v $(pwd)/secrets:/secrets airbyte/source-box-data-extract:dev check --config /secrets/config.json
89
+ docker run --rm -v $(pwd)/secrets:/secrets airbyte/source-box-data-extract:dev discover --config /secrets/config.json
90
+ docker run --rm -v $(pwd)/secrets:/secrets -v $(pwd)/integration_tests:/integration_tests airbyte/source-box-data-extract:dev read --config /secrets/config.json --catalog /integration_tests/configured_catalog.json
91
+ ```
92
+
93
+ ### Running our CI test suite
94
+
95
+ You can run our full test suite locally using [`airbyte-ci`](https://github.com/airbytehq/airbyte/blob/master/airbyte-ci/connectors/pipelines/README.md):
96
+
97
+ ```bash
98
+ airbyte-ci connectors --name=source-box-data-extract test
99
+ ```
100
+
101
+ ### Customizing acceptance Tests
102
+
103
+ Customize `acceptance-test-config.yml` file to configure acceptance tests. See [Connector Acceptance Tests](https://docs.airbyte.com/connector-development/testing-connectors/connector-acceptance-tests-reference) for more information.
104
+ If your connector requires to create or destroy resources for use during acceptance tests create fixtures for it and place them inside integration_tests/acceptance.py.
105
+
106
+ ### Dependency Management
107
+
108
+ All of your dependencies should be managed via Poetry.
109
+ To add a new dependency, run:
110
+
111
+ ```bash
112
+ poetry add <package-name>
113
+ ```
114
+
115
+ Please commit the changes to `pyproject.toml` and `poetry.lock` files.
116
+
117
+ ## Publishing a new version of the connector
118
+
119
+ You've checked out the repo, implemented a million dollar feature, and you're ready to share your changes with the world. Now what?
120
+ 1. Make sure your changes are passing our test suite: `airbyte-ci connectors --name=source-box-data-extract test`
121
+ 2. Bump the connector version (please follow [semantic versioning for connectors](https://docs.airbyte.com/contributing-to-airbyte/resources/pull-requests-handbook/#semantic-versioning-for-connectors)):
122
+ - bump the `dockerImageTag` value in in `metadata.yaml`
123
+ - bump the `version` value in `pyproject.toml`
124
+ 3. Make sure the `metadata.yaml` content is up to date.
125
+ 4. Make sure the connector documentation and its changelog is up to date (`docs/integrations/sources/box-data-extract.md`).
126
+ 5. Create a Pull Request: use [our PR naming conventions](https://docs.airbyte.com/contributing-to-airbyte/resources/pull-requests-handbook/#pull-request-title-convention).
127
+ 6. Pat yourself on the back for being an awesome contributor.
128
+ 7. Someone from Airbyte will take a look at your PR and iterate with you to merge it into master.
129
+ 8. Once your PR is merged, the new version of the connector will be automatically published to Docker Hub and our connector registry.
@@ -0,0 +1,111 @@
1
+ # Box Data Extract Source
2
+
3
+ This is the repository for the Box Generated source connector, written in Python.
4
+ For information about how to use this connector within Airbyte, see [the documentation](https://docs.airbyte.com/integrations/sources/box-data-extract).
5
+
6
+ This connector uses Box AI to extract data directly from documents stored in Box.
7
+
8
+ For example, a company managing lease contracts can automatically capture key details and populate their system of record. Likewise, a financial institution can extract critical data from loan application documents—such as bank statements and W-2s—and integrate it into approval workflows.
9
+
10
+ By prioritizing content intelligence, this connector unlocks new opportunities for automation and AI-driven insights.
11
+
12
+ ## Local development
13
+
14
+ ### Prerequisites
15
+
16
+ * Python (`^3.11`)
17
+ * Poetry (`^1.8`) - installation instructions [here](https://python-poetry.org/docs/#installation)
18
+
19
+
20
+
21
+ ### Installing the connector
22
+
23
+ From this connector directory, run:
24
+ ```bash
25
+ poetry install --with dev
26
+ ```
27
+
28
+
29
+ ### Create credentials
30
+
31
+ **If you are a community contributor**, follow the instructions in the [documentation](https://docs.airbyte.com/integrations/sources/box-data-extract)
32
+ to generate the necessary credentials. Then create a file `secrets/config.json` conforming to the `src/source_box_data_extract/spec.yaml` file.
33
+ Note that any directory named `secrets` is gitignored across the entire Airbyte repo, so there is no danger of accidentally checking in sensitive information.
34
+ See `sample_files/sample_config.json` for a sample config file.
35
+
36
+
37
+ ### Locally running the connector
38
+
39
+ ```
40
+ poetry run source-box-data-extract spec
41
+ poetry run source-box-data-extract check --config secrets/config.json
42
+ poetry run source-box-data-extract discover --config secrets/config.json
43
+ poetry run source-box-data-extract read --config secrets/config.json --catalog sample_files/configured_catalog.json
44
+ ```
45
+
46
+ ### Running tests
47
+
48
+ To run tests locally, from the connector directory run:
49
+
50
+ ```
51
+ poetry run pytest tests
52
+ ```
53
+
54
+ ### Building the docker image
55
+
56
+ 1. Install [`airbyte-ci`](https://github.com/airbytehq/airbyte/blob/master/airbyte-ci/connectors/pipelines/README.md)
57
+ 2. Run the following command to build the docker image:
58
+ ```bash
59
+ airbyte-ci connectors --name=source-box-data-extract build
60
+ ```
61
+
62
+ An image will be available on your host with the tag `airbyte/source-box-data-extract:dev`.
63
+
64
+
65
+ ### Running as a docker container
66
+
67
+ Then run any of the connector commands as follows:
68
+ ```
69
+ docker run --rm airbyte/source-box-data-extract:dev spec
70
+ docker run --rm -v $(pwd)/secrets:/secrets airbyte/source-box-data-extract:dev check --config /secrets/config.json
71
+ docker run --rm -v $(pwd)/secrets:/secrets airbyte/source-box-data-extract:dev discover --config /secrets/config.json
72
+ docker run --rm -v $(pwd)/secrets:/secrets -v $(pwd)/integration_tests:/integration_tests airbyte/source-box-data-extract:dev read --config /secrets/config.json --catalog /integration_tests/configured_catalog.json
73
+ ```
74
+
75
+ ### Running our CI test suite
76
+
77
+ You can run our full test suite locally using [`airbyte-ci`](https://github.com/airbytehq/airbyte/blob/master/airbyte-ci/connectors/pipelines/README.md):
78
+
79
+ ```bash
80
+ airbyte-ci connectors --name=source-box-data-extract test
81
+ ```
82
+
83
+ ### Customizing acceptance Tests
84
+
85
+ Customize `acceptance-test-config.yml` file to configure acceptance tests. See [Connector Acceptance Tests](https://docs.airbyte.com/connector-development/testing-connectors/connector-acceptance-tests-reference) for more information.
86
+ If your connector requires to create or destroy resources for use during acceptance tests create fixtures for it and place them inside integration_tests/acceptance.py.
87
+
88
+ ### Dependency Management
89
+
90
+ All of your dependencies should be managed via Poetry.
91
+ To add a new dependency, run:
92
+
93
+ ```bash
94
+ poetry add <package-name>
95
+ ```
96
+
97
+ Please commit the changes to `pyproject.toml` and `poetry.lock` files.
98
+
99
+ ## Publishing a new version of the connector
100
+
101
+ You've checked out the repo, implemented a million dollar feature, and you're ready to share your changes with the world. Now what?
102
+ 1. Make sure your changes are passing our test suite: `airbyte-ci connectors --name=source-box-data-extract test`
103
+ 2. Bump the connector version (please follow [semantic versioning for connectors](https://docs.airbyte.com/contributing-to-airbyte/resources/pull-requests-handbook/#semantic-versioning-for-connectors)):
104
+ - bump the `dockerImageTag` value in in `metadata.yaml`
105
+ - bump the `version` value in `pyproject.toml`
106
+ 3. Make sure the `metadata.yaml` content is up to date.
107
+ 4. Make sure the connector documentation and its changelog is up to date (`docs/integrations/sources/box-data-extract.md`).
108
+ 5. Create a Pull Request: use [our PR naming conventions](https://docs.airbyte.com/contributing-to-airbyte/resources/pull-requests-handbook/#pull-request-title-convention).
109
+ 6. Pat yourself on the back for being an awesome contributor.
110
+ 7. Someone from Airbyte will take a look at your PR and iterate with you to merge it into master.
111
+ 8. Once your PR is merged, the new version of the connector will be automatically published to Docker Hub and our connector registry.
@@ -0,0 +1,9 @@
1
+ #
2
+ # Copyright (c) 2024 Airbyte, Inc., all rights reserved.
3
+ #
4
+
5
+ from source_box_data_extract.run import run
6
+
7
+
8
+ if __name__ == "__main__":
9
+ run()
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = [
3
+ "poetry-core>=1.0.0",
4
+ ]
5
+ build-backend = "poetry.core.masonry.api"
6
+
7
+ [tool.poetry]
8
+ version = "0.1.7"
9
+ name = "airbyte-source-box-data-extract"
10
+ description = "Source implementation for Box File Text Extraction."
11
+ authors = [
12
+ "Airbyte <contact@airbyte.io>",
13
+ ]
14
+ license = "MIT"
15
+ readme = "README.md"
16
+ documentation = "https://docs.airbyte.com/integrations/sources/box-files-text"
17
+ homepage = "https://airbyte.com"
18
+ repository = "https://github.com/airbytehq/airbyte"
19
+ packages = [
20
+ { include = "source_box_data_extract" },
21
+ { include = "main.py" },
22
+ ]
23
+
24
+ [tool.poetry.dependencies]
25
+ python = "^3.11,<3.12"
26
+ airbyte-cdk = "^6"
27
+ box-sdk-gen = "^1.11.1"
28
+
29
+ [tool.poetry.scripts]
30
+ source-box-data-extract = "source_box_data_extract.run:run"
31
+
32
+ [tool.poetry.group.dev.dependencies]
33
+ requests-mock = "*"
34
+ pytest-mock = "*"
35
+ pytest = "*"
@@ -0,0 +1,8 @@
1
+ #
2
+ # Copyright (c) 2024 Airbyte, Inc., all rights reserved.
3
+ #
4
+
5
+
6
+ from .source import SourceBoxDataExtract
7
+
8
+ __all__ = ["SourceBoxDataExtract"]
@@ -0,0 +1,248 @@
1
+ # Copyright (c) 2024 Airbyte, Inc., all rights reserved.
2
+
3
+ import json
4
+ import logging
5
+ from dataclasses import dataclass
6
+ from typing import Any, Iterable, List, Mapping, MutableMapping, Optional, Tuple, Union
7
+
8
+ import requests
9
+ from box_sdk_gen import (
10
+ AiAsk,
11
+ AiExtractResponse,
12
+ AiItemBase,
13
+ AiItemBaseTypeField,
14
+ AiResponseFull,
15
+ BoxCCGAuth,
16
+ BoxClient,
17
+ BoxSDKError,
18
+ CCGConfig,
19
+ CreateAiAskMode,
20
+ CreateAiExtractStructuredFields,
21
+ CreateAiExtractStructuredFieldsOptionsField,
22
+ File,
23
+ )
24
+ from requests_cache import Dict
25
+
26
+
27
+ logger = logging.getLogger("airbyte")
28
+
29
+
30
+ @dataclass
31
+ class BoxFileExtended:
32
+ file: File
33
+ text_representation: str
34
+
35
+
36
+ def get_box_ccg_client(config: Mapping[str, Any]) -> BoxClient:
37
+ client_id = config["client_id"]
38
+ client_secret = config["client_secret"]
39
+ box_subject_type = config["box_subject_type"]
40
+ box_subject_id = config["box_subject_id"]
41
+
42
+ if box_subject_type == "enterprise":
43
+ enterprise_id = box_subject_id
44
+ user_id = None
45
+ else:
46
+ enterprise_id = None
47
+ user_id = box_subject_id
48
+ ccg_config = CCGConfig(client_id=client_id, client_secret=client_secret, enterprise_id=enterprise_id, user_id=user_id)
49
+ ccg_auth = BoxCCGAuth(ccg_config)
50
+ return add_extra_header_to_box_client(BoxClient(ccg_auth))
51
+
52
+
53
+ def add_extra_header_to_box_client(box_client: BoxClient) -> BoxClient:
54
+ """
55
+ Add extra headers to the Box client.
56
+
57
+ Args:
58
+ box_client (BoxClient): A Box client object.
59
+ header (Dict[str, str]): A dictionary of extra headers to add to the Box client.
60
+
61
+ Returns:
62
+ BoxClient: A Box client object with the extra headers added.
63
+ """
64
+ header = {"x-box-ai-library": "airbyte"}
65
+ return box_client.with_extra_headers(extra_headers=header)
66
+
67
+
68
+ def _do_request(box_client: BoxClient, url: str):
69
+ """
70
+ Performs a GET request to a Box API endpoint using the provided Box client.
71
+
72
+ This is an internal helper function and should not be called directly.
73
+
74
+ Args:
75
+ box_client (BoxClient): An authenticated Box client object.
76
+ url (str): The URL of the Box API endpoint to make the request to.
77
+
78
+ Returns:
79
+ bytes: The content of the response from the Box API.
80
+
81
+ Raises:
82
+ BoxSDKError: If an error occurs while retrieving the access token.
83
+ requests.exceptions.RequestException: If the request fails (e.g., network error,
84
+ 4XX or 5XX status code).
85
+ """
86
+ try:
87
+ access_token = box_client.auth.retrieve_token().access_token
88
+ except BoxSDKError as e:
89
+ raise
90
+
91
+ resp = requests.get(url, headers={"Authorization": f"Bearer {access_token}"})
92
+ resp.raise_for_status()
93
+ return resp.content
94
+
95
+
96
+ def box_file_get_by_id(client: BoxClient, file_id: str) -> File:
97
+ return client.files.get_file_by_id(file_id=file_id)
98
+
99
+
100
+ def box_file_text_extract(client: BoxClient, file_id: str) -> str:
101
+ # Request the file with the "extracted_text" representation hint
102
+ file_text_representation = client.files.get_file_by_id(
103
+ file_id,
104
+ x_rep_hints="[extracted_text]",
105
+ fields=["name", "representations"],
106
+ )
107
+ # Check if any representations exist
108
+ if not file_text_representation.representations.entries:
109
+ logger.debug(f"No representation for file {file_text_representation.id}")
110
+ return ""
111
+
112
+ # Find the "extracted_text" representation
113
+ extracted_text_entry = next(
114
+ (entry for entry in file_text_representation.representations.entries if entry.representation == "extracted_text"),
115
+ None,
116
+ )
117
+ if not extracted_text_entry:
118
+ return ""
119
+
120
+ # Handle cases where the extracted text needs generation
121
+ if extracted_text_entry.status.state == "none":
122
+ _do_request(extracted_text_entry.info.url) # Trigger text generation
123
+
124
+ # Construct the download URL and sanitize filename
125
+ url = extracted_text_entry.content.url_template.replace("{+asset_path}", "")
126
+
127
+ # Download and truncate the raw content
128
+ raw_content = _do_request(client, url)
129
+
130
+ # check to see if rawcontent is bytes
131
+ if isinstance(raw_content, bytes):
132
+ return raw_content.decode("utf-8")
133
+ else:
134
+ return raw_content
135
+
136
+
137
+ def box_file_ai_ask(client: BoxClient, file_id: str, prompt: str) -> str:
138
+ mode = CreateAiAskMode.SINGLE_ITEM_QA
139
+ ai_item = AiItemBase(id=file_id, type=AiItemBaseTypeField.FILE)
140
+ response = client.ai.create_ai_ask(mode=mode, prompt=prompt, items=[ai_item])
141
+ return response.answer
142
+
143
+
144
+ def box_file_ai_extract(client: BoxClient, file_id: str, prompt: str) -> str:
145
+ ai_item = AiItemBase(id=file_id, type=AiItemBaseTypeField.FILE)
146
+ response = client.ai.create_ai_extract(prompt=prompt, items=[ai_item])
147
+ return response.answer
148
+
149
+
150
+ def box_file_ai_extract_structured(client: BoxClient, file_id: str, fields_json_str: str) -> str:
151
+ ai_item = AiItemBase(id=file_id, type=AiItemBaseTypeField.FILE)
152
+ fields_list = json.loads(fields_json_str)
153
+ ai_fields = []
154
+ options = []
155
+ for field in fields_list:
156
+ field_options = field.get("options")
157
+ if field_options is not None:
158
+ for option in field.get("options"):
159
+ options.append(CreateAiExtractStructuredFieldsOptionsField(key=option.get("key")))
160
+
161
+ ai_fields.append(
162
+ CreateAiExtractStructuredFields(
163
+ key=field.get("key"),
164
+ description=field.get("description"),
165
+ display_name=field.get("display_name"),
166
+ prompt=field.get("prompt"),
167
+ type=field.get("type"),
168
+ options=options if options is not None and len(options) > 0 else None,
169
+ )
170
+ )
171
+ response: AiExtractResponse = client.ai.create_ai_extract_structured(items=[ai_item], fields=ai_fields)
172
+ return json.dumps(response.to_dict(), indent=2)
173
+
174
+
175
+ def box_folder_text_representation(
176
+ client: BoxClient, folder_id: str, is_recursive: bool = False, by_pass_text_extraction: bool = False
177
+ ) -> Iterable[BoxFileExtended]:
178
+ # folder items iterator
179
+ for item in client.folders.get_folder_items(folder_id).entries:
180
+ if item.type == "file":
181
+ file = box_file_get_by_id(client=client, file_id=item.id)
182
+ if not by_pass_text_extraction:
183
+ text_representation = box_file_text_extract(client=client, file_id=item.id)
184
+ else:
185
+ text_representation = ""
186
+ yield BoxFileExtended(file=file, text_representation=text_representation)
187
+ elif item.type == "folder" and is_recursive:
188
+ yield from box_folder_text_representation(
189
+ client=client, folder_id=item.id, is_recursive=is_recursive, by_pass_text_extraction=by_pass_text_extraction
190
+ )
191
+
192
+
193
+ def box_folder_ai_ask(
194
+ client: BoxClient, folder_id: str, prompt: str, is_recursive: bool = False, by_pass_text_extraction: bool = False
195
+ ) -> Iterable[BoxFileExtended]:
196
+ # folder items iterator
197
+ for item in client.folders.get_folder_items(folder_id).entries:
198
+ if item.type == "file":
199
+ file = box_file_get_by_id(client=client, file_id=item.id)
200
+ if not by_pass_text_extraction:
201
+ text_representation = box_file_ai_ask(client=client, file_id=item.id, prompt=prompt)
202
+ else:
203
+ text_representation = ""
204
+ yield BoxFileExtended(file=file, text_representation=text_representation)
205
+ elif item.type == "folder" and is_recursive:
206
+ yield from box_folder_ai_ask(
207
+ client=client, folder_id=item.id, prompt=prompt, is_recursive=is_recursive, by_pass_text_extraction=by_pass_text_extraction
208
+ )
209
+
210
+
211
+ def box_folder_ai_extract(
212
+ client: BoxClient, folder_id: str, prompt: str, is_recursive: bool = False, by_pass_text_extraction: bool = False
213
+ ) -> Iterable[BoxFileExtended]:
214
+ # folder items iterator
215
+ for item in client.folders.get_folder_items(folder_id).entries:
216
+ if item.type == "file":
217
+ file = box_file_get_by_id(client=client, file_id=item.id)
218
+ if not by_pass_text_extraction:
219
+ text_representation = box_file_ai_extract(client=client, file_id=item.id, prompt=prompt)
220
+ else:
221
+ text_representation = ""
222
+ yield BoxFileExtended(file=file, text_representation=text_representation)
223
+ elif item.type == "folder" and is_recursive:
224
+ yield from box_folder_ai_extract(
225
+ client=client, folder_id=item.id, prompt=prompt, is_recursive=is_recursive, by_pass_text_extraction=by_pass_text_extraction
226
+ )
227
+
228
+
229
+ def box_folder_ai_extract_structured(
230
+ client: BoxClient, folder_id: str, fields_json_str: str, is_recursive: bool = False, by_pass_text_extraction: bool = False
231
+ ) -> Iterable[BoxFileExtended]:
232
+ # folder items iterator
233
+ for item in client.folders.get_folder_items(folder_id).entries:
234
+ if item.type == "file":
235
+ file = box_file_get_by_id(client=client, file_id=item.id)
236
+ if not by_pass_text_extraction:
237
+ text_representation = box_file_ai_extract_structured(client=client, file_id=item.id, fields_json_str=fields_json_str)
238
+ else:
239
+ text_representation = ""
240
+ yield BoxFileExtended(file=file, text_representation=text_representation)
241
+ elif item.type == "folder" and is_recursive:
242
+ yield from box_folder_ai_extract_structured(
243
+ client=client,
244
+ folder_id=item.id,
245
+ fields_json_str=fields_json_str,
246
+ is_recursive=is_recursive,
247
+ by_pass_text_extraction=by_pass_text_extraction,
248
+ )
@@ -0,0 +1,15 @@
1
+ #
2
+ # Copyright (c) 2024 Airbyte, Inc., all rights reserved.
3
+ #
4
+
5
+
6
+ import sys
7
+
8
+ from airbyte_cdk.entrypoint import launch
9
+
10
+ from .source import SourceBoxDataExtract
11
+
12
+
13
+ def run():
14
+ source = SourceBoxDataExtract()
15
+ launch(source, sys.argv[1:])
@@ -0,0 +1,235 @@
1
+ # Copyright (c) 2024 Airbyte, Inc., all rights reserved.
2
+
3
+ import json
4
+ from typing import Dict
5
+
6
+
7
+ def get_generic_json_schema() -> Dict:
8
+ generic_schema = """
9
+ {
10
+ "$schema": "http://json-schema.org/draft-07/schema#",
11
+ "type": "object",
12
+ "properties": {
13
+ "type": {
14
+ "type": "string"
15
+ },
16
+ "id": {
17
+ "type": "string"
18
+ },
19
+ "file_version": {
20
+ "type": "object",
21
+ "properties": {
22
+ "type": {
23
+ "type": "string"
24
+ },
25
+ "id": {
26
+ "type": "string"
27
+ },
28
+ "sha1": {
29
+ "type": "string"
30
+ }
31
+ },
32
+ "required": ["type", "id", "sha1"]
33
+ },
34
+ "sequence_id": {
35
+ "type": "string"
36
+ },
37
+ "etag": {
38
+ "type": "string"
39
+ },
40
+ "sha1": {
41
+ "type": "string"
42
+ },
43
+ "name": {
44
+ "type": "string"
45
+ },
46
+ "description": {
47
+ "type": "string"
48
+ },
49
+ "size": {
50
+ "type": "integer"
51
+ },
52
+ "path_collection": {
53
+ "type": "object",
54
+ "properties": {
55
+ "total_count": {
56
+ "type": "integer"
57
+ },
58
+ "entries": {
59
+ "type": "array",
60
+ "items": [
61
+ {
62
+ "type": "object",
63
+ "properties": {
64
+ "type": {
65
+ "type": "string"
66
+ },
67
+ "id": {
68
+ "type": "string"
69
+ },
70
+ "sequence_id": {
71
+ "type": "null"
72
+ },
73
+ "etag": {
74
+ "type": "null"
75
+ },
76
+ "name": {
77
+ "type": "string"
78
+ }
79
+ },
80
+ "required": ["type", "id", "sequence_id", "etag", "name"]
81
+ },
82
+ {
83
+ "type": "object",
84
+ "properties": {
85
+ "type": {
86
+ "type": "string"
87
+ },
88
+ "id": {
89
+ "type": "string"
90
+ },
91
+ "sequence_id": {
92
+ "type": "string"
93
+ },
94
+ "etag": {
95
+ "type": "string"
96
+ },
97
+ "name": {
98
+ "type": "string"
99
+ }
100
+ },
101
+ "required": ["type", "id", "sequence_id", "etag", "name"]
102
+ },
103
+ {
104
+ "type": "object",
105
+ "properties": {
106
+ "type": {
107
+ "type": "string"
108
+ },
109
+ "id": {
110
+ "type": "string"
111
+ },
112
+ "sequence_id": {
113
+ "type": "string"
114
+ },
115
+ "etag": {
116
+ "type": "string"
117
+ },
118
+ "name": {
119
+ "type": "string"
120
+ }
121
+ },
122
+ "required": ["type", "id", "sequence_id", "etag", "name"]
123
+ }
124
+ ]
125
+ }
126
+ },
127
+ "required": ["total_count", "entries"]
128
+ },
129
+ "created_at": {
130
+ "type": "string"
131
+ },
132
+ "modified_at": {
133
+ "type": "string"
134
+ },
135
+ "trashed_at": {
136
+ "type": "null"
137
+ },
138
+ "purged_at": {
139
+ "type": "null"
140
+ },
141
+ "content_created_at": {
142
+ "type": "string"
143
+ },
144
+ "content_modified_at": {
145
+ "type": "string"
146
+ },
147
+ "created_by": {
148
+ "type": "object",
149
+ "properties": {
150
+ "type": {
151
+ "type": "string"
152
+ },
153
+ "id": {
154
+ "type": "string"
155
+ },
156
+ "name": {
157
+ "type": "string"
158
+ },
159
+ "login": {
160
+ "type": "string"
161
+ }
162
+ },
163
+ "required": ["type", "id", "name", "login"]
164
+ },
165
+ "modified_by": {
166
+ "type": "object",
167
+ "properties": {
168
+ "type": {
169
+ "type": "string"
170
+ },
171
+ "id": {
172
+ "type": "string"
173
+ },
174
+ "name": {
175
+ "type": "string"
176
+ },
177
+ "login": {
178
+ "type": "string"
179
+ }
180
+ },
181
+ "required": ["type", "id", "name", "login"]
182
+ },
183
+ "owned_by": {
184
+ "type": "object",
185
+ "properties": {
186
+ "type": {
187
+ "type": "string"
188
+ },
189
+ "id": {
190
+ "type": "string"
191
+ },
192
+ "name": {
193
+ "type": "string"
194
+ },
195
+ "login": {
196
+ "type": "string"
197
+ }
198
+ },
199
+ "required": ["type", "id", "name", "login"]
200
+ },
201
+ "shared_link": {
202
+ "type": "null"
203
+ },
204
+ "parent": {
205
+ "type": "object",
206
+ "properties": {
207
+ "type": {
208
+ "type": "string"
209
+ },
210
+ "id": {
211
+ "type": "string"
212
+ },
213
+ "sequence_id": {
214
+ "type": "string"
215
+ },
216
+ "etag": {
217
+ "type": "string"
218
+ },
219
+ "name": {
220
+ "type": "string"
221
+ }
222
+ },
223
+ "required": ["type", "id", "sequence_id", "etag", "name"]
224
+ },
225
+ "item_status": {
226
+ "type": "string"
227
+ },
228
+ "text_representation": {
229
+ "type": "string"
230
+ }
231
+ }
232
+ }
233
+
234
+ """
235
+ return json.loads(generic_schema)
@@ -0,0 +1,264 @@
1
+ #
2
+ # Copyright (c) 2024 Airbyte, Inc., all rights reserved.
3
+ #
4
+
5
+
6
+ import logging
7
+ from typing import Any, Dict, Iterable, List, Mapping, MutableMapping, Optional, Tuple, Union
8
+
9
+ import requests
10
+ from box_sdk_gen import BoxAPIError, BoxClient, File, Folder, Items
11
+
12
+ from airbyte_cdk.models import AirbyteMessage, AirbyteStream, ConfiguredAirbyteStream, SyncMode
13
+ from airbyte_cdk.sources import AbstractSource
14
+ from airbyte_cdk.sources.streams import Stream
15
+ from airbyte_cdk.sources.streams.http import HttpStream
16
+ from airbyte_cdk.sources.streams.http.requests_native_auth import TokenAuthenticator
17
+
18
+ from .box_api import (
19
+ box_folder_ai_ask,
20
+ box_folder_ai_extract,
21
+ box_folder_ai_extract_structured,
22
+ box_folder_text_representation,
23
+ get_box_ccg_client,
24
+ )
25
+ from .schemas import get_generic_json_schema
26
+
27
+
28
+ logger = logging.getLogger("airbyte")
29
+
30
+ # A stream's read method can return one of the following types:
31
+ # Mapping[str, Any]: The content of an AirbyteRecordMessage
32
+ # AirbyteMessage: An AirbyteMessage. Could be of any type
33
+ StreamData = Union[Mapping[str, Any], AirbyteMessage]
34
+ """
35
+ TODO: Most comments in this class are instructive and should be deleted after the source is implemented.
36
+
37
+ This file provides a stubbed example of how to use the Airbyte CDK to develop both a source connector which supports full refresh or and an
38
+ incremental syncs from an HTTP API.
39
+
40
+ The various TODOs are both implementation hints and steps - fulfilling all the TODOs should be sufficient to implement one basic and one incremental
41
+ stream from a source. This pattern is the same one used by Airbyte internally to implement connectors.
42
+
43
+ The approach here is not authoritative, and devs are free to use their own judgement.
44
+
45
+ There are additional required TODOs in the files within the integration_tests folder and the spec.yaml file.
46
+ """
47
+
48
+
49
+ # Source
50
+ class SourceBoxDataExtract(AbstractSource):
51
+ def check_connection(self, logger, config) -> Tuple[bool, any]:
52
+ """
53
+ :param config: the user-input config object conforming to the connector's spec.yaml
54
+ :param logger: logger object
55
+ :return Tuple[bool, any]: (True, None) if the input config can be used to connect to the API successfully, (False, error) otherwise.
56
+ """
57
+ logger.info("Checking Box API connection...")
58
+ try:
59
+ box_client = get_box_ccg_client(config)
60
+ user = box_client.users.get_user_me()
61
+ logger.debug(f"box_subject_type: {config.get('box_subject_type')}, box_subject_id: {config.get('box_subject_id')}")
62
+ logger.info(f"Logged into Box as: {user.name} ({user.id} - {user.login})")
63
+ except BoxAPIError as e:
64
+ logger.error(f"Unable to connect to Box API with the provided credentials - {e}")
65
+ return False, f"Unable to connect to Box API with the provided credentials"
66
+ return True, None
67
+
68
+ def streams(self, config: Mapping[str, Any]) -> List[Stream]:
69
+ """
70
+ :param config: A Mapping of the user input configuration as defined in the connector spec.
71
+ """
72
+ box_client = get_box_ccg_client(config)
73
+ box_folder_text_representation_stream = StreamTextRepresentationFolder(
74
+ box_client, config["box_folder_id"], is_recursive=config.get("is_recursive", False)
75
+ )
76
+
77
+ box_folder_ask_ai_stream = StreamAIAskFolder(
78
+ box_client, config["box_folder_id"], config["ask_ai_prompt"], is_recursive=config.get("is_recursive", False)
79
+ )
80
+
81
+ box_folder_extract_ai_stream = StreamAIExtractFolder(
82
+ box_client, config["box_folder_id"], config["extract_ai_prompt"], is_recursive=config.get("is_recursive", False)
83
+ )
84
+
85
+ box_folder_extract_structured_ai_stream = StreamAIExtractStructuredFolder(
86
+ client=box_client,
87
+ folder_id=config["box_folder_id"],
88
+ fields_json_str=config["extract_structured_ai_fields"],
89
+ is_recursive=config.get("is_recursive", False),
90
+ )
91
+
92
+ return [
93
+ box_folder_text_representation_stream,
94
+ box_folder_ask_ai_stream,
95
+ box_folder_extract_ai_stream,
96
+ box_folder_extract_structured_ai_stream,
97
+ ]
98
+
99
+
100
+ # Streams
101
+ class StreamTextRepresentationFolder(Stream):
102
+ """
103
+ Represents a Box Data Text Representation Stream from a Box Folder.
104
+ params:
105
+ client: BoxClient - Box Client object
106
+ folder_id: str - Box Folder ID
107
+ is_recursive: bool - Whether to read the folder recursively
108
+ """
109
+
110
+ client: BoxClient = None
111
+ folder_id: str = None
112
+ is_recursive: bool = False
113
+
114
+ def __init__(self, client: BoxClient, folder_id: str, is_recursive: bool = False):
115
+ self.client = client
116
+ self.folder_id = folder_id
117
+ self.is_recursive = is_recursive
118
+
119
+ @property
120
+ def primary_key(self) -> Optional[Union[str, List[str], List[List[str]]]]:
121
+ """
122
+ :return: string if single primary key, list of strings if composite primary key, list of list of strings if composite primary key consisting of nested fields.
123
+ If the stream has no primary keys, return None.
124
+ """
125
+ return "id"
126
+
127
+ def get_json_schema(self):
128
+ return get_generic_json_schema()
129
+
130
+ def read_records(
131
+ self,
132
+ sync_mode: SyncMode,
133
+ cursor_field: Optional[List[str]] = None,
134
+ stream_slice: Optional[Mapping[str, Any]] = None,
135
+ stream_state: Optional[Mapping[str, Any]] = None,
136
+ ) -> Iterable[StreamData]:
137
+ logger.info(f"Extracting text representation for files in folder {self.folder_id} {'recursively' if self.is_recursive else ''}")
138
+ items = box_folder_text_representation(self.client, self.folder_id, is_recursive=self.is_recursive)
139
+ for item in items:
140
+ airbyte_item: StreamData = item.file.to_dict()
141
+ airbyte_item["text_representation"] = item.text_representation
142
+ logger.info(f"Reading file {item.file.id} - {item.file.name}")
143
+ yield airbyte_item
144
+
145
+
146
+ class StreamAIAskFolder(Stream):
147
+ client: BoxClient = None
148
+ folder_id: str = None
149
+ is_recursive: bool = False
150
+ prompt: str = None
151
+
152
+ def __init__(self, client: BoxClient, folder_id: str, prompt: str, is_recursive: bool = False):
153
+ self.client = client
154
+ self.folder_id = folder_id
155
+ self.is_recursive = is_recursive
156
+ self.prompt = prompt
157
+
158
+ @property
159
+ def primary_key(self) -> Optional[Union[str, List[str], List[List[str]]]]:
160
+ """
161
+ :return: string if single primary key, list of strings if composite primary key, list of list of strings if composite primary key consisting of nested fields.
162
+ If the stream has no primary keys, return None.
163
+ """
164
+ return "id"
165
+
166
+ def get_json_schema(self):
167
+ return get_generic_json_schema()
168
+
169
+ def read_records(
170
+ self,
171
+ sync_mode: SyncMode,
172
+ cursor_field: Optional[List[str]] = None,
173
+ stream_slice: Optional[Mapping[str, Any]] = None,
174
+ stream_state: Optional[Mapping[str, Any]] = None,
175
+ ) -> Iterable[StreamData]:
176
+ logger.info(f"Asking AI {self.prompt} for all files in folder {self.folder_id} {'recursively' if self.is_recursive else ''}")
177
+ items = box_folder_ai_ask(self.client, self.folder_id, prompt=self.prompt, is_recursive=self.is_recursive)
178
+ for item in items:
179
+ airbyte_item: StreamData = item.file.to_dict()
180
+ airbyte_item["text_representation"] = item.text_representation
181
+ logger.info(f"Reading file {item.file.id} - {item.file.name}")
182
+ yield airbyte_item
183
+
184
+
185
+ class StreamAIExtractFolder(Stream):
186
+ client: BoxClient = None
187
+ folder_id: str = None
188
+ is_recursive: bool = False
189
+ prompt: str = None
190
+
191
+ def __init__(self, client: BoxClient, folder_id: str, prompt: str, is_recursive: bool = False):
192
+ self.client = client
193
+ self.folder_id = folder_id
194
+ self.is_recursive = is_recursive
195
+ self.prompt = prompt
196
+
197
+ @property
198
+ def primary_key(self) -> Optional[Union[str, List[str], List[List[str]]]]:
199
+ """
200
+ :return: string if single primary key, list of strings if composite primary key, list of list of strings if composite primary key consisting of nested fields.
201
+ If the stream has no primary keys, return None.
202
+ """
203
+ return "id"
204
+
205
+ def get_json_schema(self):
206
+ return get_generic_json_schema()
207
+
208
+ def read_records(
209
+ self,
210
+ sync_mode: SyncMode,
211
+ cursor_field: Optional[List[str]] = None,
212
+ stream_slice: Optional[Mapping[str, Any]] = None,
213
+ stream_state: Optional[Mapping[str, Any]] = None,
214
+ ) -> Iterable[StreamData]:
215
+ logger.info(f"Extracting AI {self.prompt} for all files in folder {self.folder_id} {'recursively' if self.is_recursive else ''}")
216
+ items = box_folder_ai_extract(self.client, self.folder_id, prompt=self.prompt, is_recursive=self.is_recursive)
217
+ for item in items:
218
+ airbyte_item: StreamData = item.file.to_dict()
219
+ airbyte_item["text_representation"] = item.text_representation
220
+ logger.info(f"Reading file {item.file.id} - {item.file.name}")
221
+ yield airbyte_item
222
+
223
+
224
+ class StreamAIExtractStructuredFolder(Stream):
225
+ client: BoxClient = None
226
+ folder_id: str = None
227
+ is_recursive: bool = False
228
+ fields_json_str: str = None
229
+
230
+ def __init__(self, client: BoxClient, folder_id: str, fields_json_str: str, is_recursive: bool = False):
231
+ self.client = client
232
+ self.folder_id = folder_id
233
+ self.is_recursive = is_recursive
234
+ self.fields_json_str = fields_json_str
235
+
236
+ @property
237
+ def primary_key(self) -> Optional[Union[str, List[str], List[List[str]]]]:
238
+ """
239
+ :return: string if single primary key, list of strings if composite primary key, list of list of strings if composite primary key consisting of nested fields.
240
+ If the stream has no primary keys, return None.
241
+ """
242
+ return "id"
243
+
244
+ def get_json_schema(self):
245
+ return get_generic_json_schema()
246
+
247
+ def read_records(
248
+ self,
249
+ sync_mode: SyncMode,
250
+ cursor_field: Optional[List[str]] = None,
251
+ stream_slice: Optional[Mapping[str, Any]] = None,
252
+ stream_state: Optional[Mapping[str, Any]] = None,
253
+ ) -> Iterable[StreamData]:
254
+ logger.info(
255
+ f"Extracting Struvctured AI {self.fields_json_str} for all files in folder {self.folder_id} {'recursively' if self.is_recursive else ''}"
256
+ )
257
+ items = box_folder_ai_extract_structured(
258
+ self.client, self.folder_id, fields_json_str=self.fields_json_str, is_recursive=self.is_recursive
259
+ )
260
+ for item in items:
261
+ airbyte_item: StreamData = item.file.to_dict()
262
+ airbyte_item["text_representation"] = item.text_representation
263
+ logger.info(f"Reading file {item.file.id} - {item.file.name}")
264
+ yield airbyte_item
@@ -0,0 +1,80 @@
1
+ documentationUrl: https://docs.airbyte.com/integrations/sources/box-data-extract
2
+ connectionSpecification:
3
+ $schema: http://json-schema.org/draft-07/schema#
4
+ title: Box File Text Extraction Spec
5
+ type: object
6
+ required:
7
+ - client_id
8
+ - client_secret
9
+ - box_subject_type
10
+ - box_subject_id
11
+ - box_folder_id
12
+ - is_recursive
13
+ properties:
14
+ client_id:
15
+ type: string
16
+ title: Client ID
17
+ description: >-
18
+ You Box App client ID.
19
+ Find yours in the <a href="https://app.box.com/developers/console">developer console</a>.
20
+ order: 0
21
+ client_secret:
22
+ type: string
23
+ title: Client Secret
24
+ description: >-
25
+ You Box App client secret.
26
+ Find yours in the <a href="https://app.box.com/developers/console">developer console</a>.
27
+ airbyte_secret: true
28
+ order: 1
29
+ box_subject_type:
30
+ type: string
31
+ enum:
32
+ - user
33
+ - enterprise
34
+ title: Box Subject Type
35
+ description: >-
36
+ Represents the type of user to login as ("user" or "enterprise").
37
+ Enterprise will login with the application service account.
38
+ User will login with the user if app can impersonate users.
39
+ order: 2
40
+ box_subject_id:
41
+ type: string
42
+ title: Box Subject ID
43
+ description: >-
44
+ If subject type is "enterprise", use your enterprise ID
45
+ If subject type is "user", use the user id to login as.
46
+ order: 3
47
+ box_folder_id:
48
+ type: string
49
+ title: Folder ID
50
+ description: >-
51
+ Folder to retreive data from.
52
+ order: 4
53
+ is_recursive:
54
+ type: boolean
55
+ title: Recursive
56
+ description: >-
57
+ Read the folders recursively.
58
+ order: 5
59
+ ask_ai_prompt:
60
+ type: string
61
+ always_show: true
62
+ title: Ask AI Prompt
63
+ description: >-
64
+ Prompt to use in Ask AI Stream.
65
+ order: 6
66
+ extract_ai_prompt:
67
+ type: string
68
+ always_show: true
69
+ title: Extract AI Prompt
70
+ description: >-
71
+ Prompt to use in Extract AI Stream.
72
+ order: 7
73
+ extract_structured_ai_fields:
74
+ type: string
75
+ always_show: true
76
+ multiline: true
77
+ title: Extract Structured AI Prompt
78
+ description: >-
79
+ Prompt to use in Extract Strctured AI Stream.
80
+ order: 8