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.
- airbyte_source_box_data_extract-0.1.7/PKG-INFO +129 -0
- airbyte_source_box_data_extract-0.1.7/README.md +111 -0
- airbyte_source_box_data_extract-0.1.7/main.py +9 -0
- airbyte_source_box_data_extract-0.1.7/pyproject.toml +35 -0
- airbyte_source_box_data_extract-0.1.7/source_box_data_extract/__init__.py +8 -0
- airbyte_source_box_data_extract-0.1.7/source_box_data_extract/box_api.py +248 -0
- airbyte_source_box_data_extract-0.1.7/source_box_data_extract/run.py +15 -0
- airbyte_source_box_data_extract-0.1.7/source_box_data_extract/schemas.py +235 -0
- airbyte_source_box_data_extract-0.1.7/source_box_data_extract/source.py +264 -0
- airbyte_source_box_data_extract-0.1.7/source_box_data_extract/spec.yaml +80 -0
|
@@ -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,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,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
|