dex-connector-google-spreadsheet 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. dex_connector_google_spreadsheet-0.1.0/.gitignore +16 -0
  2. dex_connector_google_spreadsheet-0.1.0/LICENSE +21 -0
  3. dex_connector_google_spreadsheet-0.1.0/PKG-INFO +18 -0
  4. dex_connector_google_spreadsheet-0.1.0/README.md +5 -0
  5. dex_connector_google_spreadsheet-0.1.0/examples/upsert_contact/README.md +11 -0
  6. dex_connector_google_spreadsheet-0.1.0/examples/upsert_contact/__init__.py +3 -0
  7. dex_connector_google_spreadsheet-0.1.0/examples/upsert_contact/flow.py +83 -0
  8. dex_connector_google_spreadsheet-0.1.0/examples/upsert_contact/integration_test.py +115 -0
  9. dex_connector_google_spreadsheet-0.1.0/examples/upsert_contact/main.py +38 -0
  10. dex_connector_google_spreadsheet-0.1.0/pyproject.toml +41 -0
  11. dex_connector_google_spreadsheet-0.1.0/src/dex_connectors/google_spreadsheet/__init__.py +45 -0
  12. dex_connector_google_spreadsheet-0.1.0/src/dex_connectors/google_spreadsheet/_generated/__init__.py +2 -0
  13. dex_connector_google_spreadsheet-0.1.0/src/dex_connectors/google_spreadsheet/_generated/config.py +28 -0
  14. dex_connector_google_spreadsheet-0.1.0/src/dex_connectors/google_spreadsheet/_generated/connection.py +193 -0
  15. dex_connector_google_spreadsheet-0.1.0/src/dex_connectors/google_spreadsheet/_generated/definitions.py +80 -0
  16. dex_connector_google_spreadsheet-0.1.0/src/dex_connectors/google_spreadsheet/_generated/mock.py +218 -0
  17. dex_connector_google_spreadsheet-0.1.0/src/dex_connectors/google_spreadsheet/_generated/models.py +106 -0
  18. dex_connector_google_spreadsheet-0.1.0/src/dex_connectors/google_spreadsheet/_generated/refresh.py +9 -0
  19. dex_connector_google_spreadsheet-0.1.0/src/dex_connectors/google_spreadsheet/_generated/steps.py +196 -0
  20. dex_connector_google_spreadsheet-0.1.0/src/dex_connectors/google_spreadsheet/client.py +226 -0
  21. dex_connector_google_spreadsheet-0.1.0/src/dex_connectors/google_spreadsheet/connection.py +8 -0
  22. dex_connector_google_spreadsheet-0.1.0/src/dex_connectors/google_spreadsheet/dex-connector.json +135 -0
  23. dex_connector_google_spreadsheet-0.1.0/src/dex_connectors/google_spreadsheet/json_values.py +23 -0
  24. dex_connector_google_spreadsheet-0.1.0/src/dex_connectors/google_spreadsheet/models.py +10 -0
  25. dex_connector_google_spreadsheet-0.1.0/src/dex_connectors/google_spreadsheet/operations.py +851 -0
  26. dex_connector_google_spreadsheet-0.1.0/src/dex_connectors/google_spreadsheet/py.typed +0 -0
  27. dex_connector_google_spreadsheet-0.1.0/src/dex_connectors/google_spreadsheet/query_values.py +16 -0
  28. dex_connector_google_spreadsheet-0.1.0/src/dex_connectors/google_spreadsheet/spreadsheetmock/__init__.py +6 -0
  29. dex_connector_google_spreadsheet-0.1.0/src/dex_connectors/google_spreadsheet/steps.py +7 -0
  30. dex_connector_google_spreadsheet-0.1.0/tests/refresh_agreement_cases.json +530 -0
  31. dex_connector_google_spreadsheet-0.1.0/tests/test_conformance.py +43 -0
  32. dex_connector_google_spreadsheet-0.1.0/tests/test_json_values.py +15 -0
  33. dex_connector_google_spreadsheet-0.1.0/tests/test_provider_helpers.py +33 -0
  34. dex_connector_google_spreadsheet-0.1.0/tests/test_query_values.py +36 -0
  35. dex_connector_google_spreadsheet-0.1.0/tests/test_refresh_agreement.py +278 -0
  36. dex_connector_google_spreadsheet-0.1.0/uv.lock +934 -0
@@ -0,0 +1,16 @@
1
+ .DS_Store
2
+ __pycache__/
3
+ *.py[cod]
4
+ node_modules/
5
+ dist/
6
+ sdkpython/dist/dex_connectors_sdk-0.1.0-py3-none-any.whl
7
+ /.bin/dexcli
8
+ /.bin/dex-server-zd
9
+ /sdkpython/.dexcli-data/
10
+ site/.catalog.yaml
11
+ coverage/
12
+ *.out
13
+ *.test
14
+ go.work
15
+ go.work.sum
16
+ .idea/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Super Durable
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,18 @@
1
+ Metadata-Version: 2.5
2
+ Name: dex-connector-google-spreadsheet
3
+ Version: 0.1.0
4
+ Summary: Dex-native Google Sheets connector
5
+ Project-URL: Source, https://github.com/superdurable/dex-connectors-library/tree/main/connectors/google/spreadsheet/python
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.11
9
+ Requires-Dist: dex-connectors-sdk==0.7.0
10
+ Requires-Dist: dex-python-sdk>=1.28.0
11
+ Requires-Dist: httpx>=0.27
12
+ Description-Content-Type: text/markdown
13
+
14
+ # Google Sheets Python Connector
15
+
16
+ This package exposes `getValues`, `findRow`, and `upsertRow` as Dex Python Steps. Install `dex-connector-google-spreadsheet==0.1.0` and create a `google-sheets-oauth` project connection. See [the connector README](../README.md) for Google Cloud OAuth setup and the append safety model.
17
+
18
+ The `examples/upsert_contact` application reads or upserts a contact in a selected spreadsheet. Run its integration test with a local Dex development server; live Google authorization and provider behavior have not been verified for this port.
@@ -0,0 +1,5 @@
1
+ # Google Sheets Python Connector
2
+
3
+ This package exposes `getValues`, `findRow`, and `upsertRow` as Dex Python Steps. Install `dex-connector-google-spreadsheet==0.1.0` and create a `google-sheets-oauth` project connection. See [the connector README](../README.md) for Google Cloud OAuth setup and the append safety model.
4
+
5
+ The `examples/upsert_contact` application reads or upserts a contact in a selected spreadsheet. Run its integration test with a local Dex development server; live Google authorization and provider behavior have not been verified for this port.
@@ -0,0 +1,11 @@
1
+ # Upsert contact with Google Sheets
2
+
3
+ This Flow uses the `google-sheets-contacts` project connection, reads the `email`, `name`, and `status` columns, and upserts a contact by email. Configure the Google Sheets OAuth connection in Dex Web as described in the [connector README](../../../README.md), then start a local Dex server and run `uv run python -m examples.upsert_contact.main` from `connectors/google/spreadsheet/python`.
4
+
5
+ Start `GoogleSheetsUpsertContactPython` with a unique Flow ID and this JSON input:
6
+
7
+ ```json
8
+ {"spreadsheetId":"your-spreadsheet-id","sheetName":"Contacts","email":"ada@example.test","name":"Ada","status":"active"}
9
+ ```
10
+
11
+ Only `upserted` is wired. A conflict, provider rejection, invalid response, uncertain write, or local defect fails the Flow. `integration_test.py` verifies retry and routing through a real local Dex Worker and Client with an inert provider stand-in.
@@ -0,0 +1,3 @@
1
+ # Copyright (c) 2026 Super Durable
2
+ # SPDX-License-Identifier: MIT
3
+ """Runnable Google Sheets contact upsert Flow."""
@@ -0,0 +1,83 @@
1
+ # Copyright (c) 2026 Super Durable
2
+ # SPDX-License-Identifier: MIT
3
+ """Upsert one contact through the Google Sheets connector and Dex."""
4
+
5
+ from dataclasses import dataclass, field
6
+
7
+ import dex
8
+ from dex_connectors.sdk import MutationResult
9
+
10
+ from dex_connectors.google_spreadsheet import Connection, UpsertRowInput, UpsertRowOutput, new_upsert_row_step
11
+
12
+ CONNECTION_NAME = "google-sheets-contacts"
13
+ FLOW_TYPE = "GoogleSheetsUpsertContactPython"
14
+
15
+
16
+ @dataclass(frozen=True, slots=True)
17
+ class ContactInput:
18
+ """Spreadsheet, tab, and contact values supplied to the Flow."""
19
+
20
+ spreadsheet_id: str = field(metadata={"dex_json_name": "spreadsheetId"})
21
+ sheet_name: str = field(metadata={"dex_json_name": "sheetName"})
22
+ email: str
23
+ name: str
24
+ status: str
25
+
26
+ def to_operation_input(self) -> UpsertRowInput:
27
+ """Map a contact to the stable email-key row Mutation."""
28
+ return UpsertRowInput(
29
+ self.spreadsheet_id,
30
+ self.sheet_name,
31
+ "email",
32
+ self.email,
33
+ {"email": self.email, "name": self.name, "status": self.status},
34
+ )
35
+
36
+
37
+ contact_result = dex.Attribute("google-sheets-python-contact-result", UpsertRowOutput)
38
+
39
+
40
+ # dex:group group-id:google-sheets group-label:"Google Sheets"
41
+ # dex:explanation text:"Store the accepted contact row and complete the Flow."
42
+ class CompleteContact(dex.Step[MutationResult[UpsertRowOutput]]):
43
+ """Persist the accepted row locator and complete the Flow."""
44
+
45
+ def execute(self, context: dex.Context, input: MutationResult[UpsertRowOutput]) -> dex.StepDecision:
46
+ """Store only the non-secret provider result."""
47
+ contact_result.set(context, input.value)
48
+ return dex.graceful_complete(input.value)
49
+
50
+
51
+ class ContactFlow(dex.Flow[ContactInput]):
52
+ """Upsert a contact using one static Google Sheets connection."""
53
+
54
+ def __init__(self, connection: Connection) -> None:
55
+ """Bind the connector Mutation during Worker registration."""
56
+ if connection is None:
57
+ raise ValueError("Google Sheets connection is required")
58
+ self.complete = CompleteContact()
59
+ self.upsert = new_upsert_row_step(
60
+ step_type="UpsertContactRow",
61
+ step_input_type=ContactInput,
62
+ annotations={
63
+ "groupId": "google-sheets",
64
+ "groupLabel": "Google Sheets",
65
+ "explanation": "Upsert the contact in Google Sheets using email as the stable key.",
66
+ },
67
+ connection=connection,
68
+ connection_name=CONNECTION_NAME,
69
+ map_to_operation_input=ContactInput.to_operation_input,
70
+ upserted=self.complete,
71
+ )
72
+
73
+ def get_flow_type(self) -> str:
74
+ """Keep the Python example Flow identity stable."""
75
+ return FLOW_TYPE
76
+
77
+ def get_steps(self) -> dex.StepList[ContactInput]:
78
+ """Run the fenced provider Mutation and completion Step."""
79
+ return dex.StepList.start_step(self.upsert).other_steps(self.complete)
80
+
81
+ def get_persistence_schema(self) -> dex.PersistenceSchema:
82
+ """Register the durable contact result Attribute."""
83
+ return dex.PersistenceSchema.of(contact_result)
@@ -0,0 +1,115 @@
1
+ # Copyright (c) 2026 Super Durable
2
+ # SPDX-License-Identifier: MIT
3
+ """Run the contact Flow through a real Dex Worker and Client."""
4
+
5
+ import asyncio
6
+ import os
7
+ import tempfile
8
+ from datetime import timedelta
9
+ from pathlib import Path
10
+ from uuid import uuid4
11
+
12
+ import dex
13
+ import httpx
14
+ import pytest
15
+ from dex_connectors.sdk import ConnectionRef, SecretString, StaticCredentialProvider
16
+
17
+ from dex_connectors.google_spreadsheet import Client, Config, Credentials, UpsertRowOutput, new_connection
18
+
19
+ from .flow import CONNECTION_NAME, ContactFlow, ContactInput
20
+
21
+
22
+ @pytest.mark.asyncio
23
+ async def test_contact_upsert_retries_once_and_fails_conflict_in_real_dex() -> None:
24
+ """Verify durable retry, stable correlation key, and optional-branch failure."""
25
+ server_address = os.environ["DEX_FLOW_SERVICE_ADDRESS"]
26
+ writes: list[httpx.Request] = []
27
+ show_duplicate = False
28
+
29
+ def answer(request: httpx.Request) -> httpx.Response:
30
+ if request.method == "GET":
31
+ values = [["email", "name", "status"]]
32
+ if show_duplicate:
33
+ values.extend(
34
+ [
35
+ ["ada@example.test", "Ada", "active"],
36
+ ["ada@example.test", "Other", "active"],
37
+ ]
38
+ )
39
+ return httpx.Response(
40
+ 200,
41
+ json={
42
+ "range": "'Contacts'!A1:C1",
43
+ "majorDimension": "ROWS",
44
+ "values": values,
45
+ },
46
+ )
47
+ writes.append(request)
48
+ if len(writes) == 1:
49
+ return httpx.Response(503)
50
+ return httpx.Response(200, json={"updates": {"updatedRange": "'Contacts'!A2:C2"}})
51
+
52
+ reference = ConnectionRef("google", CONNECTION_NAME)
53
+ credentials = Credentials(
54
+ "client.apps.googleusercontent.com",
55
+ SecretString("secret"),
56
+ SecretString("inert-token"),
57
+ SecretString("refresh"),
58
+ )
59
+ async with httpx.AsyncClient(transport=httpx.MockTransport(answer)) as provider_transport:
60
+ client = Client(
61
+ Config(endpoint="http://127.0.0.1/v4"),
62
+ StaticCredentialProvider({reference: credentials}),
63
+ provider_transport,
64
+ )
65
+ connection = new_connection(client, reference)
66
+ flow = ContactFlow(connection)
67
+ registry = dex.Registry([flow], allow_async_handlers=True)
68
+ with tempfile.TemporaryDirectory(prefix="google-sheets-python-dex-") as temporary_directory:
69
+ cache = dex.open_blob_cache(
70
+ dex.BlobCacheConfig(str(Path(temporary_directory) / "blobs"), 1 << 24)
71
+ )
72
+ try:
73
+ worker = dex.AsyncWorker(registry, cache, dex.WorkerOptions(server_address=server_address))
74
+ worker_task = asyncio.create_task(worker.start())
75
+ try:
76
+ await worker.wait_pull_registered(timeout=15)
77
+ async with dex.AsyncClient(
78
+ registry, cache, dex.ClientOptions(server_address=server_address)
79
+ ) as dex_client:
80
+ flow_id = f"sheets-python-{uuid4().hex}"
81
+ await dex_client.start_flow(
82
+ flow,
83
+ flow_id,
84
+ ContactInput("sheet-id", "Contacts", "ada@example.test", "Ada", "active"),
85
+ )
86
+ result = await asyncio.wait_for(
87
+ dex_client.wait_for_flow(flow_id, timedelta(seconds=25)), 35
88
+ )
89
+ assert result.status == dex.FlowStatus.COMPLETED, result.error_message
90
+ output = result.single_output(UpsertRowOutput)
91
+ assert output.action == "inserted" and output.row_number == 2
92
+ assert len(writes) == 2
93
+ assert (
94
+ writes[0].headers["X-Idempotency-Key"] == writes[1].headers["X-Idempotency-Key"]
95
+ )
96
+ assert writes[0].headers["X-Idempotency-Key"]
97
+
98
+ show_duplicate = True
99
+ conflict_id = f"sheets-python-{uuid4().hex}"
100
+ await dex_client.start_flow(
101
+ flow,
102
+ conflict_id,
103
+ ContactInput("sheet-id", "Contacts", "ada@example.test", "Ada", "active"),
104
+ )
105
+ conflict = await asyncio.wait_for(
106
+ dex_client.wait_for_flow(conflict_id, timedelta(seconds=20)), 30
107
+ )
108
+ assert conflict.status == dex.FlowStatus.FAILED
109
+ assert "conflict" in (conflict.error_message or "")
110
+ assert len(writes) == 2
111
+ finally:
112
+ await worker.stop()
113
+ await worker_task
114
+ finally:
115
+ cache.close()
@@ -0,0 +1,38 @@
1
+ # Copyright (c) 2026 Super Durable
2
+ # SPDX-License-Identifier: MIT
3
+ """Run the Google Sheets contact Worker with project OAuth configuration."""
4
+
5
+ import asyncio
6
+ import os
7
+ import tempfile
8
+ from pathlib import Path
9
+
10
+ import dex
11
+ from dex_connectors.sdk.projectconfig import load_from_environment
12
+
13
+ from dex_connectors.google_spreadsheet import new_project_connection
14
+
15
+ from .flow import CONNECTION_NAME, ContactFlow
16
+
17
+
18
+ async def main() -> None:
19
+ """Load the project connection and serve the contact Flow."""
20
+ project = await load_from_environment()
21
+ connection = new_project_connection(project, CONNECTION_NAME)
22
+ cache_directory = Path(
23
+ os.environ.get("DEX_BLOB_CACHE_DIR", str(Path(tempfile.gettempdir()) / "dex-sheets-python-blobs"))
24
+ )
25
+ cache = dex.open_blob_cache(dex.BlobCacheConfig(str(cache_directory), 1 << 30))
26
+ try:
27
+ registry = dex.Registry([ContactFlow(connection)], allow_async_handlers=True)
28
+ address = os.environ.get("DEX_FLOW_SERVICE_ADDRESS", "localhost:8801")
29
+ async with dex.AsyncWorker(registry, cache, dex.WorkerOptions(server_address=address)) as worker:
30
+ await worker.start()
31
+ finally:
32
+ cache.close()
33
+ await connection.aclose()
34
+ await project.close()
35
+
36
+
37
+ if __name__ == "__main__":
38
+ asyncio.run(main())
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "dex-connector-google-spreadsheet"
7
+ version = "0.1.0"
8
+ description = "Dex-native Google Sheets connector"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ dependencies = ["dex-connectors-sdk==0.7.0", "dex-python-sdk>=1.28.0", "httpx>=0.27"]
14
+
15
+ [project.urls]
16
+ Source = "https://github.com/superdurable/dex-connectors-library/tree/main/connectors/google/spreadsheet/python"
17
+
18
+ [dependency-groups]
19
+ dev = ["pytest>=8", "pytest-asyncio>=0.24", "mypy>=1.13", "ruff>=0.7", "PyYAML>=6", "types-PyYAML>=6"]
20
+
21
+ [tool.hatch.build.targets.wheel]
22
+ packages = ["src/dex_connectors"]
23
+
24
+ [tool.pytest.ini_options]
25
+ testpaths = ["tests"]
26
+ pythonpath = [".", "src"]
27
+ asyncio_mode = "auto"
28
+ asyncio_default_fixture_loop_scope = "function"
29
+
30
+ [tool.mypy]
31
+ python_version = "3.11"
32
+ strict = true
33
+ mypy_path = "src"
34
+ explicit_package_bases = true
35
+
36
+ [tool.ruff]
37
+ target-version = "py311"
38
+ line-length = 110
39
+
40
+ [tool.ruff.lint]
41
+ select = ["E", "F", "I", "UP", "B"]
@@ -0,0 +1,45 @@
1
+ # Copyright (c) 2026 Super Durable
2
+ # SPDX-License-Identifier: MIT
3
+ """Dex-native Google Sheets range and row operations."""
4
+
5
+ from .client import Client as Client
6
+ from .client import Config as Config
7
+ from .client import Credentials as Credentials
8
+ from .connection import Connection as Connection
9
+ from .connection import new_connection as new_connection
10
+ from .connection import new_connection_with_operations as new_connection_with_operations
11
+ from .connection import new_project_connection as new_project_connection
12
+ from .models import FindRowInput as FindRowInput
13
+ from .models import FindRowOutput as FindRowOutput
14
+ from .models import GetValuesInput as GetValuesInput
15
+ from .models import GetValuesOutput as GetValuesOutput
16
+ from .models import UpsertRowInput as UpsertRowInput
17
+ from .models import UpsertRowOutput as UpsertRowOutput
18
+ from .operations import FindRowOperation as FindRowOperation
19
+ from .operations import GetValuesOperation as GetValuesOperation
20
+ from .operations import UpsertRowOperation as UpsertRowOperation
21
+ from .steps import new_find_row_step as new_find_row_step
22
+ from .steps import new_get_values_step as new_get_values_step
23
+ from .steps import new_upsert_row_step as new_upsert_row_step
24
+
25
+ __all__ = [
26
+ "Client",
27
+ "Config",
28
+ "Credentials",
29
+ "Connection",
30
+ "FindRowInput",
31
+ "FindRowOperation",
32
+ "FindRowOutput",
33
+ "GetValuesInput",
34
+ "GetValuesOperation",
35
+ "GetValuesOutput",
36
+ "UpsertRowInput",
37
+ "UpsertRowOperation",
38
+ "UpsertRowOutput",
39
+ "new_connection",
40
+ "new_connection_with_operations",
41
+ "new_find_row_step",
42
+ "new_get_values_step",
43
+ "new_project_connection",
44
+ "new_upsert_row_step",
45
+ ]
@@ -0,0 +1,2 @@
1
+ # Copyright (c) 2026 Super Durable
2
+ # SPDX-License-Identifier: MIT
@@ -0,0 +1,28 @@
1
+ # Copyright (c) 2026 Super Durable
2
+ # SPDX-License-Identifier: MIT
3
+ # ruff: noqa
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from dex_connectors.sdk import SecretString
8
+
9
+
10
+ @dataclass(frozen=True, slots=True)
11
+ class Config:
12
+
13
+ endpoint: str = "https://sheets.googleapis.com/v4"
14
+ max_response_bytes: int = 4194304
15
+ max_rows: int = 10000
16
+
17
+ setattr(Config, '__module__', 'dex_connectors.google_spreadsheet.client')
18
+
19
+
20
+ @dataclass(frozen=True, slots=True)
21
+ class Credentials:
22
+
23
+ oauth_client_id: str
24
+ oauth_client_secret: SecretString
25
+ access_token: SecretString
26
+ refresh_token: SecretString
27
+
28
+ setattr(Credentials, '__module__', 'dex_connectors.google_spreadsheet.client')
@@ -0,0 +1,193 @@
1
+ # Copyright (c) 2026 Super Durable
2
+ # SPDX-License-Identifier: MIT
3
+ # ruff: noqa
4
+
5
+ from typing import Any
6
+
7
+ import httpx
8
+ from dex_connectors.sdk import ConnectionRef, Mutation, Query, Call, SecretString, CredentialRefreshDriver
9
+ from dex_connectors.sdk.projectconfig import ConnectionKey, LoadedProject
10
+ from dex_connectors.sdk.projectconfig.provider import RefreshingCredentialProvider
11
+ from dex_connectors.sdk.oauthtoken import ManifestCredentialRefreshDriver
12
+
13
+ from ..client import Client, Config, Credentials
14
+ from .definitions import CONNECTOR_ID, PROVIDER, FIND_ROW_DEFINITION, GET_VALUES_DEFINITION, UPSERT_ROW_DEFINITION
15
+ from .refresh import AUTHORIZATION, CREDENTIAL_FIELDS
16
+ from ..models import FindRowInput, FindRowOutput, GetValuesInput, GetValuesOutput, UpsertRowInput, UpsertRowOutput
17
+
18
+ class Connection:
19
+
20
+ __slots__ = ('_get_values', '_find_row', '_upsert_row', '_client', 'reference')
21
+
22
+ def __init__(self,
23
+ reference: ConnectionRef,
24
+ get_values: Query[GetValuesInput, GetValuesOutput],
25
+ find_row: Query[FindRowInput, FindRowOutput],
26
+ upsert_row: Mutation[UpsertRowInput, UpsertRowOutput],
27
+ client: Client | None = None,
28
+ ) -> None:
29
+ reference.validate()
30
+ if reference.provider != PROVIDER or get_values is None or find_row is None or upsert_row is None:
31
+ raise ValueError("Google Sheets connection provider and operations are required")
32
+ if get_values.definition().operation != GET_VALUES_DEFINITION.operation:
33
+ raise ValueError("Google Sheets connection operation identity differs")
34
+ if find_row.definition().operation != FIND_ROW_DEFINITION.operation:
35
+ raise ValueError("Google Sheets connection operation identity differs")
36
+ if upsert_row.definition().operation != UPSERT_ROW_DEFINITION.operation:
37
+ raise ValueError("Google Sheets connection operation identity differs")
38
+ self.reference = reference
39
+ self._get_values = get_values
40
+ self._find_row = find_row
41
+ self._upsert_row = upsert_row
42
+ self._client = client
43
+
44
+ def get_values(self) -> Query[GetValuesInput, GetValuesOutput]:
45
+ return self._get_values
46
+
47
+ def find_row(self) -> Query[FindRowInput, FindRowOutput]:
48
+ return self._find_row
49
+
50
+ def upsert_row(self) -> Mutation[UpsertRowInput, UpsertRowOutput]:
51
+ return self._upsert_row
52
+
53
+ async def aclose(self) -> None:
54
+ if self._client is not None:
55
+ await self._client.aclose()
56
+
57
+ def __repr__(self) -> str:
58
+ return "google_spreadsheet.Connection([REDACTED])"
59
+
60
+ def __reduce__(self) -> tuple[object, ...]:
61
+ raise TypeError("Google Sheets connections cannot be serialized")
62
+
63
+ setattr(Connection, '__module__', 'dex_connectors.google_spreadsheet.connection')
64
+
65
+
66
+ def new_connection(client: Client, reference: ConnectionRef) -> Connection:
67
+ if client is None:
68
+ raise ValueError("Google Sheets client is required")
69
+ return Connection(reference, client.get_values(), client.find_row(), client.upsert_row(), client)
70
+
71
+ setattr(new_connection, '__module__', 'dex_connectors.google_spreadsheet.connection')
72
+
73
+
74
+ def new_connection_with_operations(
75
+ get_values: Query[GetValuesInput, GetValuesOutput],
76
+ find_row: Query[FindRowInput, FindRowOutput],
77
+ upsert_row: Mutation[UpsertRowInput, UpsertRowOutput],
78
+ reference: ConnectionRef,
79
+ ) -> Connection:
80
+ return Connection(reference, get_values, find_row, upsert_row)
81
+
82
+ setattr(new_connection_with_operations, '__module__', 'dex_connectors.google_spreadsheet.connection')
83
+
84
+
85
+ def decode_credentials(material: dict[str, Any], *, shared: bool = False) -> Credentials:
86
+ if not isinstance(material, dict):
87
+ raise ValueError("Google Sheets stored credential does not match the connector credential fields")
88
+ if set(material) - set(('oauth_client_id', 'oauth_client_secret', 'access_token', 'refresh_token')):
89
+ raise ValueError("Google Sheets stored credential does not match the connector credential fields")
90
+ is_shared = shared
91
+ oauth_client_id = material.get("oauth_client_id")
92
+ if oauth_client_id is not None and not (isinstance(oauth_client_id, str)):
93
+ raise ValueError("Google Sheets credential oauth_client_id has an invalid type")
94
+ oauth_client_secret = material.get("oauth_client_secret")
95
+ if oauth_client_secret is not None and not (isinstance(oauth_client_secret, str)):
96
+ raise ValueError("Google Sheets credential oauth_client_secret has an invalid type")
97
+ access_token = material.get("access_token")
98
+ if access_token is not None and not (isinstance(access_token, str)):
99
+ raise ValueError("Google Sheets credential access_token has an invalid type")
100
+ refresh_token = material.get("refresh_token")
101
+ if refresh_token is not None and not (isinstance(refresh_token, str)):
102
+ raise ValueError("Google Sheets credential refresh_token has an invalid type")
103
+ if not oauth_client_id:
104
+ raise ValueError("Google Sheets credential oauth_client_id is required")
105
+ if not oauth_client_secret and not is_shared:
106
+ raise ValueError("Google Sheets credential oauth_client_secret is required")
107
+ if not access_token:
108
+ raise ValueError("Google Sheets credential access_token is required")
109
+ if not refresh_token and not is_shared:
110
+ raise ValueError("Google Sheets credential refresh_token is required")
111
+ return Credentials(oauth_client_id, SecretString(oauth_client_secret or ''), SecretString(access_token), SecretString(refresh_token or ''))
112
+
113
+
114
+ def decode_shared_credentials(material: dict[str, Any]) -> Credentials:
115
+ return decode_credentials(material, shared=True)
116
+
117
+
118
+ def encode_credentials(credentials: Credentials) -> dict[str, Any]:
119
+ material: dict[str, Any] = {}
120
+ if credentials.oauth_client_id:
121
+ material["oauth_client_id"] = credentials.oauth_client_id
122
+ if credentials.oauth_client_secret.reveal():
123
+ material["oauth_client_secret"] = credentials.oauth_client_secret.reveal()
124
+ if credentials.access_token.reveal():
125
+ material["access_token"] = credentials.access_token.reveal()
126
+ if credentials.refresh_token.reveal():
127
+ material["refresh_token"] = credentials.refresh_token.reveal()
128
+ decode_credentials(material)
129
+ return material
130
+
131
+
132
+ class _ProjectCredentialSource:
133
+ def __init__(self, project: LoadedProject, key: ConnectionKey, http_client: httpx.AsyncClient, settings: dict[str, Any]) -> None:
134
+ self.provider = RefreshingCredentialProvider(
135
+ project.connections, key, decode_credentials, encode_credentials, decode_shared_credentials
136
+ )
137
+ self.driver = ManifestCredentialRefreshDriver[Credentials]("Google Sheets", AUTHORIZATION, CREDENTIAL_FIELDS, http_client, settings=settings)
138
+
139
+ async def resolve(self, call: Call) -> Credentials:
140
+ return await self.provider.resolve_with_refresh(call, self.driver)
141
+
142
+ async def resolve_with_refresh(self, call: Call, driver: CredentialRefreshDriver[Credentials] | None = None) -> Credentials:
143
+ return await self.provider.resolve_with_refresh(call, driver or self.driver)
144
+
145
+ async def resolve_after_rejection(self, call: Call, driver: CredentialRefreshDriver[Credentials] | None = None) -> Credentials:
146
+ return await self.provider.resolve_after_rejection(call, driver or self.driver)
147
+
148
+
149
+ def new_project_connection(
150
+ project: LoadedProject, connection_name: str, http_client: httpx.AsyncClient | None = None
151
+ ) -> Connection:
152
+ if project is None:
153
+ raise ValueError("Google Sheets connection requires loaded project configuration")
154
+ reference = ConnectionRef(PROVIDER, connection_name)
155
+ reference.validate()
156
+ key = ConnectionKey(CONNECTOR_ID, connection_name)
157
+ matching = [
158
+ entry
159
+ for entry in project.configuration.connections
160
+ if entry.connector_id == CONNECTOR_ID and entry.connection_name == connection_name
161
+ ]
162
+ if len(matching) != 1:
163
+ raise ValueError("Google Sheets project connection is missing or duplicated")
164
+ if matching[0].provider != PROVIDER:
165
+ raise ValueError("Google Sheets project connection provider differs")
166
+ config = _decode_config(matching[0].configuration)
167
+ http_client = http_client or httpx.AsyncClient()
168
+ client = Client(config, _ProjectCredentialSource(project, key, http_client, matching[0].configuration), http_client)
169
+ return new_connection(client, reference)
170
+
171
+ setattr(new_project_connection, '__module__', 'dex_connectors.google_spreadsheet.connection')
172
+
173
+
174
+ def _decode_config(value: dict[str, Any]) -> Config:
175
+ if set(value) - {"endpoint", "maxResponseBytes", "maxRows"}:
176
+ raise ValueError("Google Sheets configuration has unknown fields")
177
+ config_arguments: dict[str, Any] = {}
178
+ if "endpoint" in value:
179
+ endpoint = value["endpoint"]
180
+ if not (isinstance(endpoint, str)):
181
+ raise ValueError("Google Sheets configuration endpoint has an invalid type")
182
+ config_arguments['endpoint'] = endpoint
183
+ if "maxResponseBytes" in value:
184
+ max_response_bytes = value["maxResponseBytes"]
185
+ if not (type(max_response_bytes) is int):
186
+ raise ValueError("Google Sheets configuration maxResponseBytes has an invalid type")
187
+ config_arguments['max_response_bytes'] = max_response_bytes
188
+ if "maxRows" in value:
189
+ max_rows = value["maxRows"]
190
+ if not (type(max_rows) is int):
191
+ raise ValueError("Google Sheets configuration maxRows has an invalid type")
192
+ config_arguments['max_rows'] = max_rows
193
+ return Config(**config_arguments)