airbyte-source-convex 0.4.1__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,110 @@
1
+ Metadata-Version: 2.1
2
+ Name: airbyte-source-convex
3
+ Version: 0.4.1
4
+ Summary: Source implementation for Convex.
5
+ Home-page: https://airbyte.com
6
+ License: MIT
7
+ Author: Airbyte
8
+ Author-email: contact@airbyte.io
9
+ Requires-Python: >=3.9,<3.12
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Requires-Dist: airbyte-cdk (==0.80.0)
16
+ Project-URL: Documentation, https://docs.airbyte.com/integrations/sources/convex
17
+ Project-URL: Repository, https://github.com/airbytehq/airbyte
18
+ Description-Content-Type: text/markdown
19
+
20
+ # Convex source connector
21
+
22
+
23
+ This is the repository for the Convex source connector, written in Python.
24
+ For information about how to use this connector within Airbyte, see [the documentation](https://docs.airbyte.com/integrations/sources/convex).
25
+
26
+ ## Local development
27
+
28
+ ### Prerequisites
29
+ * Python (~=3.9)
30
+ * Poetry (~=1.7) - installation instructions [here](https://python-poetry.org/docs/#installation)
31
+
32
+
33
+ ### Installing the connector
34
+ From this connector directory, run:
35
+ ```bash
36
+ poetry install --with dev
37
+ ```
38
+
39
+
40
+ ### Create credentials
41
+ **If you are a community contributor**, follow the instructions in the [documentation](https://docs.airbyte.com/integrations/sources/convex)
42
+ to generate the necessary credentials. Then create a file `secrets/config.json` conforming to the `source_convex/spec.yaml` file.
43
+ Note that any directory named `secrets` is gitignored across the entire Airbyte repo, so there is no danger of accidentally checking in sensitive information.
44
+ See `sample_files/sample_config.json` for a sample config file.
45
+
46
+
47
+ ### Locally running the connector
48
+ ```
49
+ poetry run source-convex spec
50
+ poetry run source-convex check --config secrets/config.json
51
+ poetry run source-convex discover --config secrets/config.json
52
+ poetry run source-convex read --config secrets/config.json --catalog sample_files/configured_catalog.json
53
+ ```
54
+
55
+ ### Running unit tests
56
+ To run unit tests locally, from the connector directory run:
57
+ ```
58
+ poetry run pytest unit_tests
59
+ ```
60
+
61
+ ### Building the docker image
62
+ 1. Install [`airbyte-ci`](https://github.com/airbytehq/airbyte/blob/master/airbyte-ci/connectors/pipelines/README.md)
63
+ 2. Run the following command to build the docker image:
64
+ ```bash
65
+ airbyte-ci connectors --name=source-convex build
66
+ ```
67
+
68
+ An image will be available on your host with the tag `airbyte/source-convex:dev`.
69
+
70
+
71
+ ### Running as a docker container
72
+ Then run any of the connector commands as follows:
73
+ ```
74
+ docker run --rm airbyte/source-convex:dev spec
75
+ docker run --rm -v $(pwd)/secrets:/secrets airbyte/source-convex:dev check --config /secrets/config.json
76
+ docker run --rm -v $(pwd)/secrets:/secrets airbyte/source-convex:dev discover --config /secrets/config.json
77
+ docker run --rm -v $(pwd)/secrets:/secrets -v $(pwd)/integration_tests:/integration_tests airbyte/source-convex:dev read --config /secrets/config.json --catalog /integration_tests/configured_catalog.json
78
+ ```
79
+
80
+ ### Running our CI test suite
81
+ You can run our full test suite locally using [`airbyte-ci`](https://github.com/airbytehq/airbyte/blob/master/airbyte-ci/connectors/pipelines/README.md):
82
+ ```bash
83
+ airbyte-ci connectors --name=source-convex test
84
+ ```
85
+
86
+ ### Customizing acceptance Tests
87
+ 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.
88
+ 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.
89
+
90
+ ### Dependency Management
91
+ All of your dependencies should be managed via Poetry.
92
+ To add a new dependency, run:
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
+ You've checked out the repo, implemented a million dollar feature, and you're ready to share your changes with the world. Now what?
101
+ 1. Make sure your changes are passing our test suite: `airbyte-ci connectors --name=source-convex test`
102
+ 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)):
103
+ - bump the `dockerImageTag` value in in `metadata.yaml`
104
+ - bump the `version` value in `pyproject.toml`
105
+ 3. Make sure the `metadata.yaml` content is up to date.
106
+ 4. Make sure the connector documentation and its changelog is up to date (`docs/integrations/sources/convex.md`).
107
+ 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).
108
+ 6. Pat yourself on the back for being an awesome contributor.
109
+ 7. Someone from Airbyte will take a look at your PR and iterate with you to merge it into master.
110
+ 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,91 @@
1
+ # Convex source connector
2
+
3
+
4
+ This is the repository for the Convex source connector, written in Python.
5
+ For information about how to use this connector within Airbyte, see [the documentation](https://docs.airbyte.com/integrations/sources/convex).
6
+
7
+ ## Local development
8
+
9
+ ### Prerequisites
10
+ * Python (~=3.9)
11
+ * Poetry (~=1.7) - installation instructions [here](https://python-poetry.org/docs/#installation)
12
+
13
+
14
+ ### Installing the connector
15
+ From this connector directory, run:
16
+ ```bash
17
+ poetry install --with dev
18
+ ```
19
+
20
+
21
+ ### Create credentials
22
+ **If you are a community contributor**, follow the instructions in the [documentation](https://docs.airbyte.com/integrations/sources/convex)
23
+ to generate the necessary credentials. Then create a file `secrets/config.json` conforming to the `source_convex/spec.yaml` file.
24
+ Note that any directory named `secrets` is gitignored across the entire Airbyte repo, so there is no danger of accidentally checking in sensitive information.
25
+ See `sample_files/sample_config.json` for a sample config file.
26
+
27
+
28
+ ### Locally running the connector
29
+ ```
30
+ poetry run source-convex spec
31
+ poetry run source-convex check --config secrets/config.json
32
+ poetry run source-convex discover --config secrets/config.json
33
+ poetry run source-convex read --config secrets/config.json --catalog sample_files/configured_catalog.json
34
+ ```
35
+
36
+ ### Running unit tests
37
+ To run unit tests locally, from the connector directory run:
38
+ ```
39
+ poetry run pytest unit_tests
40
+ ```
41
+
42
+ ### Building the docker image
43
+ 1. Install [`airbyte-ci`](https://github.com/airbytehq/airbyte/blob/master/airbyte-ci/connectors/pipelines/README.md)
44
+ 2. Run the following command to build the docker image:
45
+ ```bash
46
+ airbyte-ci connectors --name=source-convex build
47
+ ```
48
+
49
+ An image will be available on your host with the tag `airbyte/source-convex:dev`.
50
+
51
+
52
+ ### Running as a docker container
53
+ Then run any of the connector commands as follows:
54
+ ```
55
+ docker run --rm airbyte/source-convex:dev spec
56
+ docker run --rm -v $(pwd)/secrets:/secrets airbyte/source-convex:dev check --config /secrets/config.json
57
+ docker run --rm -v $(pwd)/secrets:/secrets airbyte/source-convex:dev discover --config /secrets/config.json
58
+ docker run --rm -v $(pwd)/secrets:/secrets -v $(pwd)/integration_tests:/integration_tests airbyte/source-convex:dev read --config /secrets/config.json --catalog /integration_tests/configured_catalog.json
59
+ ```
60
+
61
+ ### Running our CI test suite
62
+ You can run our full test suite locally using [`airbyte-ci`](https://github.com/airbytehq/airbyte/blob/master/airbyte-ci/connectors/pipelines/README.md):
63
+ ```bash
64
+ airbyte-ci connectors --name=source-convex test
65
+ ```
66
+
67
+ ### Customizing acceptance Tests
68
+ 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.
69
+ 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.
70
+
71
+ ### Dependency Management
72
+ All of your dependencies should be managed via Poetry.
73
+ To add a new dependency, run:
74
+ ```bash
75
+ poetry add <package-name>
76
+ ```
77
+
78
+ Please commit the changes to `pyproject.toml` and `poetry.lock` files.
79
+
80
+ ## Publishing a new version of the connector
81
+ You've checked out the repo, implemented a million dollar feature, and you're ready to share your changes with the world. Now what?
82
+ 1. Make sure your changes are passing our test suite: `airbyte-ci connectors --name=source-convex test`
83
+ 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)):
84
+ - bump the `dockerImageTag` value in in `metadata.yaml`
85
+ - bump the `version` value in `pyproject.toml`
86
+ 3. Make sure the `metadata.yaml` content is up to date.
87
+ 4. Make sure the connector documentation and its changelog is up to date (`docs/integrations/sources/convex.md`).
88
+ 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).
89
+ 6. Pat yourself on the back for being an awesome contributor.
90
+ 7. Someone from Airbyte will take a look at your PR and iterate with you to merge it into master.
91
+ 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,34 @@
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.4.1"
9
+ name = "airbyte-source-convex"
10
+ description = "Source implementation for Convex."
11
+ authors = [
12
+ "Airbyte <contact@airbyte.io>",
13
+ ]
14
+ license = "MIT"
15
+ readme = "README.md"
16
+ documentation = "https://docs.airbyte.com/integrations/sources/convex"
17
+ homepage = "https://airbyte.com"
18
+ repository = "https://github.com/airbytehq/airbyte"
19
+ packages = [
20
+ { include = "source_convex" },
21
+ ]
22
+
23
+ [tool.poetry.dependencies]
24
+ python = "^3.9,<3.12"
25
+ airbyte-cdk = "0.80.0"
26
+
27
+ [tool.poetry.scripts]
28
+ source-convex = "source_convex.run:run"
29
+
30
+ [tool.poetry.group.dev.dependencies]
31
+ responses = "^0.13.3"
32
+ requests-mock = "^1.9.3"
33
+ pytest-mock = "^3.6.1"
34
+ pytest = "^6.1"
@@ -0,0 +1,8 @@
1
+ #
2
+ # Copyright (c) 2023 Airbyte, Inc., all rights reserved.
3
+ #
4
+
5
+
6
+ from .source import SourceConvex
7
+
8
+ __all__ = ["SourceConvex"]
@@ -0,0 +1,14 @@
1
+ #
2
+ # Copyright (c) 2023 Airbyte, Inc., all rights reserved.
3
+ #
4
+
5
+
6
+ import sys
7
+
8
+ from airbyte_cdk.entrypoint import launch
9
+ from source_convex import SourceConvex
10
+
11
+
12
+ def run():
13
+ source = SourceConvex()
14
+ launch(source, sys.argv[1:])
@@ -0,0 +1,243 @@
1
+ #
2
+ # Copyright (c) 2023 Airbyte, Inc., all rights reserved.
3
+ #
4
+
5
+
6
+ from datetime import datetime
7
+ from json import JSONDecodeError
8
+ from typing import Any, Dict, Iterable, Iterator, List, Mapping, MutableMapping, Optional, Tuple, TypedDict, cast
9
+
10
+ import requests
11
+ from airbyte_cdk.models import SyncMode
12
+ from airbyte_cdk.sources import AbstractSource
13
+ from airbyte_cdk.sources.streams import IncrementalMixin, Stream
14
+ from airbyte_cdk.sources.streams.http import HttpStream
15
+ from airbyte_cdk.sources.streams.http.requests_native_auth.token import TokenAuthenticator
16
+
17
+ ConvexConfig = TypedDict(
18
+ "ConvexConfig",
19
+ {
20
+ "deployment_url": str,
21
+ "access_key": str,
22
+ },
23
+ )
24
+
25
+ ConvexState = TypedDict(
26
+ "ConvexState",
27
+ {
28
+ "snapshot_cursor": Optional[str],
29
+ "snapshot_has_more": bool,
30
+ "delta_cursor": Optional[int],
31
+ },
32
+ )
33
+
34
+ CONVEX_CLIENT_VERSION = "0.4.0"
35
+
36
+
37
+ # Source
38
+ class SourceConvex(AbstractSource):
39
+ def _json_schemas(self, config: ConvexConfig) -> requests.Response:
40
+ deployment_url = config["deployment_url"]
41
+ access_key = config["access_key"]
42
+ url = f"{deployment_url}/api/json_schemas?deltaSchema=true&format=json"
43
+ headers = {
44
+ "Authorization": f"Convex {access_key}",
45
+ "Convex-Client": f"airbyte-export-{CONVEX_CLIENT_VERSION}",
46
+ }
47
+ return requests.get(url, headers=headers)
48
+
49
+ def check_connection(self, logger: Any, config: Mapping[str, Any]) -> Tuple[bool, Any]:
50
+ """
51
+ Connection check to validate that the user-provided config can be used to connect to the underlying API
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
+ config = cast(ConvexConfig, config)
58
+ resp = self._json_schemas(config)
59
+ if resp.status_code == 200:
60
+ return True, None
61
+ else:
62
+ return False, format_http_error("Connection to Convex via json_schemas endpoint failed", resp)
63
+
64
+ def streams(self, config: Mapping[str, Any]) -> List[Stream]:
65
+ """
66
+ :param config: A Mapping of the user input configuration as defined in the connector spec.
67
+ """
68
+ config = cast(ConvexConfig, config)
69
+ resp = self._json_schemas(config)
70
+ if resp.status_code != 200:
71
+ raise Exception(format_http_error("Failed request to json_schemas", resp))
72
+ json_schemas = resp.json()
73
+ table_names = list(json_schemas.keys())
74
+ return [
75
+ ConvexStream(
76
+ config["deployment_url"],
77
+ config["access_key"],
78
+ "json", # Use `json` export format
79
+ table_name,
80
+ json_schemas[table_name],
81
+ )
82
+ for table_name in table_names
83
+ ]
84
+
85
+
86
+ class ConvexStream(HttpStream, IncrementalMixin):
87
+ def __init__(
88
+ self,
89
+ deployment_url: str,
90
+ access_key: str,
91
+ fmt: str,
92
+ table_name: str,
93
+ json_schema: Dict[str, Any],
94
+ ):
95
+ self.deployment_url = deployment_url
96
+ self.fmt = fmt
97
+ self.table_name = table_name
98
+ if json_schema:
99
+ json_schema["additionalProperties"] = True
100
+ json_schema["properties"]["_ab_cdc_lsn"] = {"type": "number"}
101
+ json_schema["properties"]["_ab_cdc_updated_at"] = {"type": "string"}
102
+ json_schema["properties"]["_ab_cdc_deleted_at"] = {"anyOf": [{"type": "string"}, {"type": "null"}]}
103
+ else:
104
+ json_schema = {}
105
+ self.json_schema = json_schema
106
+ self._snapshot_cursor_value: Optional[str] = None
107
+ self._snapshot_has_more = True
108
+ self._delta_cursor_value: Optional[int] = None
109
+ self._delta_has_more = True
110
+ super().__init__(TokenAuthenticator(access_key, "Convex"))
111
+
112
+ @property
113
+ def name(self) -> str:
114
+ return self.table_name
115
+
116
+ @property
117
+ def url_base(self) -> str:
118
+ return self.deployment_url
119
+
120
+ def get_json_schema(self) -> Mapping[str, Any]: # type: ignore[override]
121
+ return self.json_schema
122
+
123
+ primary_key = "_id"
124
+ cursor_field = "_ts"
125
+
126
+ # Checkpoint stream reads after this many records. This prevents re-reading of data if the stream fails for any reason.
127
+ state_checkpoint_interval = 128
128
+
129
+ @property
130
+ def state(self) -> MutableMapping[str, Any]:
131
+ value: ConvexState = {
132
+ "snapshot_cursor": self._snapshot_cursor_value,
133
+ "snapshot_has_more": self._snapshot_has_more,
134
+ "delta_cursor": self._delta_cursor_value,
135
+ }
136
+ return cast(MutableMapping[str, Any], value)
137
+
138
+ @state.setter
139
+ def state(self, value: MutableMapping[str, Any]) -> None:
140
+ state = cast(ConvexState, value)
141
+ self._snapshot_cursor_value = state["snapshot_cursor"]
142
+ self._snapshot_has_more = state["snapshot_has_more"]
143
+ self._delta_cursor_value = state["delta_cursor"]
144
+
145
+ def next_page_token(self, response: requests.Response) -> Optional[ConvexState]:
146
+ if response.status_code != 200:
147
+ raise Exception(format_http_error("Failed request", response))
148
+ resp_json = response.json()
149
+ if self._snapshot_has_more:
150
+ self._snapshot_cursor_value = resp_json["cursor"]
151
+ self._snapshot_has_more = resp_json["hasMore"]
152
+ self._delta_cursor_value = resp_json["snapshot"]
153
+ else:
154
+ self._delta_cursor_value = resp_json["cursor"]
155
+ self._delta_has_more = resp_json["hasMore"]
156
+ has_more = self._snapshot_has_more or self._delta_has_more
157
+ return cast(ConvexState, self.state) if has_more else None
158
+
159
+ def path(
160
+ self,
161
+ stream_state: Optional[Mapping[str, Any]] = None,
162
+ stream_slice: Optional[Mapping[str, Any]] = None,
163
+ next_page_token: Optional[Mapping[str, Any]] = None,
164
+ ) -> str:
165
+ # https://docs.convex.dev/http-api/#sync
166
+ if self._snapshot_has_more:
167
+ return "/api/list_snapshot"
168
+ else:
169
+ return "/api/document_deltas"
170
+
171
+ def parse_response(
172
+ self,
173
+ response: requests.Response,
174
+ stream_state: Mapping[str, Any],
175
+ stream_slice: Optional[Mapping[str, Any]] = None,
176
+ next_page_token: Optional[Mapping[str, Any]] = None,
177
+ ) -> Iterable[Mapping[str, Any]]:
178
+ if response.status_code != 200:
179
+ raise Exception(format_http_error("Failed request", response))
180
+ resp_json = response.json()
181
+ return list(resp_json["values"])
182
+
183
+ def request_params(
184
+ self,
185
+ stream_state: Optional[Mapping[str, Any]],
186
+ stream_slice: Optional[Mapping[str, Any]] = None,
187
+ next_page_token: Optional[Mapping[str, Any]] = None,
188
+ ) -> MutableMapping[str, Any]:
189
+ params: Dict[str, Any] = {"tableName": self.table_name, "format": self.fmt}
190
+ if self._snapshot_has_more:
191
+ if self._snapshot_cursor_value:
192
+ params["cursor"] = self._snapshot_cursor_value
193
+ if self._delta_cursor_value:
194
+ params["snapshot"] = self._delta_cursor_value
195
+ else:
196
+ if self._delta_cursor_value:
197
+ params["cursor"] = self._delta_cursor_value
198
+ return params
199
+
200
+ def request_headers(
201
+ self,
202
+ stream_state: Optional[Mapping[str, Any]],
203
+ stream_slice: Optional[Mapping[str, Any]] = None,
204
+ next_page_token: Optional[Mapping[str, Any]] = None,
205
+ ) -> Dict[str, str]:
206
+ """
207
+ Custom headers for each HTTP request, not including Authorization.
208
+ """
209
+ return {
210
+ "Convex-Client": f"airbyte-export-{CONVEX_CLIENT_VERSION}",
211
+ }
212
+
213
+ def get_updated_state(self, current_stream_state: ConvexState, latest_record: Mapping[str, Any]) -> ConvexState:
214
+ """
215
+ This (deprecated) method is still used by AbstractSource to update state between calls to `read_records`.
216
+ """
217
+ return cast(ConvexState, self.state)
218
+
219
+ def read_records(self, sync_mode: SyncMode, *args: Any, **kwargs: Any) -> Iterator[Any]:
220
+ self._delta_has_more = sync_mode == SyncMode.incremental
221
+ for read_record in super().read_records(sync_mode, *args, **kwargs):
222
+ record = dict(read_record)
223
+ ts_ns = record["_ts"]
224
+ ts_seconds = ts_ns / 1e9 # convert from nanoseconds.
225
+ # equivalent of java's `new Timestamp(transactionMillis).toInstant().toString()`
226
+ ts_datetime = datetime.utcfromtimestamp(ts_seconds)
227
+ ts = ts_datetime.isoformat()
228
+ # DebeziumEventUtils.CDC_LSN
229
+ record["_ab_cdc_lsn"] = ts_ns
230
+ # DebeziumEventUtils.CDC_DELETED_AT
231
+ record["_ab_cdc_updated_at"] = ts
232
+ record["_deleted"] = "_deleted" in record and record["_deleted"]
233
+ # DebeziumEventUtils.CDC_DELETED_AT
234
+ record["_ab_cdc_deleted_at"] = ts if record["_deleted"] else None
235
+ yield record
236
+
237
+
238
+ def format_http_error(context: str, resp: requests.Response) -> str:
239
+ try:
240
+ err = resp.json()
241
+ return f"{context}: {resp.status_code}: {err['code']}: {err['message']}"
242
+ except (JSONDecodeError, KeyError):
243
+ return f"{context}: {resp.text}"
@@ -0,0 +1,21 @@
1
+ documentationUrl: https://docs.airbyte.com/integrations/sources/convex
2
+ connectionSpecification:
3
+ additionalProperties: true
4
+ $schema: http://json-schema.org/draft-07/schema#
5
+ title: Convex Source Spec
6
+ type: object
7
+ required:
8
+ - deployment_url
9
+ - access_key
10
+ properties:
11
+ deployment_url:
12
+ type: string
13
+ title: Deployment Url
14
+ examples:
15
+ - https://murky-swan-635.convex.cloud
16
+ - https://cluttered-owl-337.convex.cloud
17
+ access_key:
18
+ type: string
19
+ title: Access Key
20
+ description: API access key used to retrieve data from Convex.
21
+ airbyte_secret: true