dex-connector-figma 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.
- dex_connector_figma-0.1.0/.gitignore +6 -0
- dex_connector_figma-0.1.0/PKG-INFO +50 -0
- dex_connector_figma-0.1.0/README.md +39 -0
- dex_connector_figma-0.1.0/examples/figma_lookup/__init__.py +3 -0
- dex_connector_figma-0.1.0/examples/figma_lookup/flow.py +126 -0
- dex_connector_figma-0.1.0/examples/figma_lookup/worker.py +51 -0
- dex_connector_figma-0.1.0/pyproject.toml +37 -0
- dex_connector_figma-0.1.0/src/dex_connectors/figma/__init__.py +51 -0
- dex_connector_figma-0.1.0/src/dex_connectors/figma/_generated/__init__.py +2 -0
- dex_connector_figma-0.1.0/src/dex_connectors/figma/_generated/config.py +25 -0
- dex_connector_figma-0.1.0/src/dex_connectors/figma/_generated/connection.py +137 -0
- dex_connector_figma-0.1.0/src/dex_connectors/figma/_generated/definitions.py +58 -0
- dex_connector_figma-0.1.0/src/dex_connectors/figma/_generated/mock.py +131 -0
- dex_connector_figma-0.1.0/src/dex_connectors/figma/_generated/models.py +74 -0
- dex_connector_figma-0.1.0/src/dex_connectors/figma/_generated/steps.py +134 -0
- dex_connector_figma-0.1.0/src/dex_connectors/figma/client.py +368 -0
- dex_connector_figma-0.1.0/src/dex_connectors/figma/connection.py +46 -0
- dex_connector_figma-0.1.0/src/dex_connectors/figma/dex-connector.json +87 -0
- dex_connector_figma-0.1.0/src/dex_connectors/figma/figmamock/__init__.py +53 -0
- dex_connector_figma-0.1.0/src/dex_connectors/figma/models.py +24 -0
- dex_connector_figma-0.1.0/src/dex_connectors/figma/py.typed +0 -0
- dex_connector_figma-0.1.0/src/dex_connectors/figma/steps.py +35 -0
- dex_connector_figma-0.1.0/tests/test_conformance.py +32 -0
- dex_connector_figma-0.1.0/tests/test_manifest_contract.py +15 -0
- dex_connector_figma-0.1.0/tests/test_operations.py +174 -0
- dex_connector_figma-0.1.0/uv.lock +932 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: dex-connector-figma
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Figma connector for Dex Flows
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: dex-connectors-sdk==0.1.0
|
|
8
|
+
Requires-Dist: dex-python-sdk>=1.28.0
|
|
9
|
+
Requires-Dist: httpx>=0.27
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# Figma connector for Python
|
|
13
|
+
|
|
14
|
+
Install with `pip install dex-connector-figma==0.1.0`. The package requires
|
|
15
|
+
`dex-connectors-sdk==0.1.0` and `dex-python-sdk>=1.28.0`, and implements the
|
|
16
|
+
manifest Queries `getFile` and `getFileComments` with the same branches,
|
|
17
|
+
defaults, and failure classification as the Go module.
|
|
18
|
+
|
|
19
|
+
## Operations
|
|
20
|
+
|
|
21
|
+
| Operation | Factory | Happy branch | Optional branches |
|
|
22
|
+
| --- | --- | --- | --- |
|
|
23
|
+
| `getFile` | `new_get_file_step` | `found` | `notFound`, `providerRejected`, `invalidResponse`, `defect` |
|
|
24
|
+
| `getFileComments` | `new_get_file_comments_step` | `listed` | `notFound`, `providerRejected`, `invalidResponse`, `defect` |
|
|
25
|
+
|
|
26
|
+
Both take a file key of 1 to 128 letters, digits, `_`, or `-`. `getFile`
|
|
27
|
+
returns the file name, last-modified text, and the document node tree as
|
|
28
|
+
JSON. Integral number values retain their exact integer value; a number that
|
|
29
|
+
Dex JSON encoding would round selects `invalidResponse` and names its document
|
|
30
|
+
field. A response without a name or document also selects `invalidResponse`.
|
|
31
|
+
404 selects `notFound`; 408, 409, 429, 5xx, and transport failures retry; 401,
|
|
32
|
+
403, and other non-200 responses select `providerRejected`.
|
|
33
|
+
|
|
34
|
+
## Authorization
|
|
35
|
+
|
|
36
|
+
Create a Dex Web connection named `figma` as described in the
|
|
37
|
+
[connector README](../README.md#connection). The personal access token travels
|
|
38
|
+
only in `X-Figma-Token` and is resolved for every attempt.
|
|
39
|
+
|
|
40
|
+
## Example
|
|
41
|
+
|
|
42
|
+
[`examples/figma_lookup`](examples/figma_lookup/flow.py) reads a file and then
|
|
43
|
+
its comments. Run the Worker with
|
|
44
|
+
`uv run python -m examples.figma_lookup.worker` after `dexcli dev` has loaded a
|
|
45
|
+
project containing the `figma` connection.
|
|
46
|
+
|
|
47
|
+
## Tests
|
|
48
|
+
|
|
49
|
+
`figmamock.Mock` scripts both Queries and defaults to the manifest answers.
|
|
50
|
+
Run `uv run pytest tests` for provider parity and manifest contract checks.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Figma connector for Python
|
|
2
|
+
|
|
3
|
+
Install with `pip install dex-connector-figma==0.1.0`. The package requires
|
|
4
|
+
`dex-connectors-sdk==0.1.0` and `dex-python-sdk>=1.28.0`, and implements the
|
|
5
|
+
manifest Queries `getFile` and `getFileComments` with the same branches,
|
|
6
|
+
defaults, and failure classification as the Go module.
|
|
7
|
+
|
|
8
|
+
## Operations
|
|
9
|
+
|
|
10
|
+
| Operation | Factory | Happy branch | Optional branches |
|
|
11
|
+
| --- | --- | --- | --- |
|
|
12
|
+
| `getFile` | `new_get_file_step` | `found` | `notFound`, `providerRejected`, `invalidResponse`, `defect` |
|
|
13
|
+
| `getFileComments` | `new_get_file_comments_step` | `listed` | `notFound`, `providerRejected`, `invalidResponse`, `defect` |
|
|
14
|
+
|
|
15
|
+
Both take a file key of 1 to 128 letters, digits, `_`, or `-`. `getFile`
|
|
16
|
+
returns the file name, last-modified text, and the document node tree as
|
|
17
|
+
JSON. Integral number values retain their exact integer value; a number that
|
|
18
|
+
Dex JSON encoding would round selects `invalidResponse` and names its document
|
|
19
|
+
field. A response without a name or document also selects `invalidResponse`.
|
|
20
|
+
404 selects `notFound`; 408, 409, 429, 5xx, and transport failures retry; 401,
|
|
21
|
+
403, and other non-200 responses select `providerRejected`.
|
|
22
|
+
|
|
23
|
+
## Authorization
|
|
24
|
+
|
|
25
|
+
Create a Dex Web connection named `figma` as described in the
|
|
26
|
+
[connector README](../README.md#connection). The personal access token travels
|
|
27
|
+
only in `X-Figma-Token` and is resolved for every attempt.
|
|
28
|
+
|
|
29
|
+
## Example
|
|
30
|
+
|
|
31
|
+
[`examples/figma_lookup`](examples/figma_lookup/flow.py) reads a file and then
|
|
32
|
+
its comments. Run the Worker with
|
|
33
|
+
`uv run python -m examples.figma_lookup.worker` after `dexcli dev` has loaded a
|
|
34
|
+
project containing the `figma` connection.
|
|
35
|
+
|
|
36
|
+
## Tests
|
|
37
|
+
|
|
38
|
+
`figmamock.Mock` scripts both Queries and defaults to the manifest answers.
|
|
39
|
+
Run `uv run pytest tests` for provider parity and manifest contract checks.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Copyright (c) 2026 Super Durable
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
"""Runnable Figma lookup Flow: read a file, then its comments."""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
import dex
|
|
8
|
+
from dex_connectors.sdk import QueryResult
|
|
9
|
+
|
|
10
|
+
from dex_connectors.figma import (
|
|
11
|
+
Connection,
|
|
12
|
+
File,
|
|
13
|
+
FileComment,
|
|
14
|
+
FileComments,
|
|
15
|
+
GetFileCommentsInput,
|
|
16
|
+
GetFileInput,
|
|
17
|
+
new_get_file_comments_step,
|
|
18
|
+
new_get_file_step,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
FLOW_TYPE = "FigmaLookup"
|
|
22
|
+
CONNECTION_NAME = "figma"
|
|
23
|
+
_FIRST_STEP_TYPE = "ReadFigmaFirst"
|
|
24
|
+
_SECOND_STEP_TYPE = "ReadFigmaSecond"
|
|
25
|
+
_ANNOTATION_GROUP = {"groupId": "figma-lookup", "groupLabel": "Figma lookup"}
|
|
26
|
+
_FILE_KEY = dex.Attribute("figma-file-key", str)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True, slots=True)
|
|
30
|
+
class FigmaLookupRequest:
|
|
31
|
+
"""Figma file key to read."""
|
|
32
|
+
|
|
33
|
+
file_key: str = field(metadata={"dex_json_name": "fileKey"})
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True, slots=True)
|
|
37
|
+
class FigmaLookupOutcome:
|
|
38
|
+
"""Comments on the file."""
|
|
39
|
+
|
|
40
|
+
comments: tuple[FileComment, ...] = ()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def map_to_get_file_input(request: FigmaLookupRequest) -> GetFileInput:
|
|
44
|
+
"""Read the requested file."""
|
|
45
|
+
return GetFileInput(request.file_key)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def map_to_get_file_comments_input(input: GetFileCommentsInput) -> GetFileCommentsInput:
|
|
49
|
+
"""Read the comments of the file chosen after the file read."""
|
|
50
|
+
return input
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# dex:group group-id:figma-lookup group-label:"Figma lookup"
|
|
54
|
+
# dex:explanation text:"Record the file key and start the lookup."
|
|
55
|
+
class _StartRequest(dex.Step[FigmaLookupRequest]):
|
|
56
|
+
def get_step_type(self) -> str:
|
|
57
|
+
return "StartFigmaLookup"
|
|
58
|
+
|
|
59
|
+
def execute(self, context: dex.Context, input: FigmaLookupRequest) -> dex.StepDecision:
|
|
60
|
+
if not input.file_key:
|
|
61
|
+
return dex.force_fail("fileKey is required")
|
|
62
|
+
_FILE_KEY.set(context, input.file_key)
|
|
63
|
+
return dex.go_to(dex.StepRef(_FIRST_STEP_TYPE), input)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# dex:group group-id:figma-lookup group-label:"Figma lookup"
|
|
67
|
+
# dex:explanation text:"Continue from the file to its comments."
|
|
68
|
+
class _AfterFirst(dex.Step[QueryResult[File]]):
|
|
69
|
+
def get_step_type(self) -> str:
|
|
70
|
+
return "AfterFigmaFirst"
|
|
71
|
+
|
|
72
|
+
def execute(self, context: dex.Context, input: QueryResult[File]) -> dex.StepDecision:
|
|
73
|
+
return dex.go_to(dex.StepRef(_SECOND_STEP_TYPE), GetFileCommentsInput(_FILE_KEY.get(context)))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# dex:group group-id:figma-lookup group-label:"Figma lookup"
|
|
77
|
+
# dex:explanation text:"Complete with the comments Figma returned."
|
|
78
|
+
class _CompleteLookup(dex.Step[QueryResult[FileComments]]):
|
|
79
|
+
def get_step_type(self) -> str:
|
|
80
|
+
return "CompleteFigmaLookup"
|
|
81
|
+
|
|
82
|
+
def execute(self, context: dex.Context, input: QueryResult[FileComments]) -> dex.StepDecision:
|
|
83
|
+
return dex.graceful_complete(FigmaLookupOutcome(input.value.comments))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class FigmaLookupFlow(dex.Flow[FigmaLookupRequest]):
|
|
87
|
+
"""Read a Figma file and its comments through the static figma connection."""
|
|
88
|
+
|
|
89
|
+
def __init__(self, connection: Connection) -> None:
|
|
90
|
+
if connection.reference.name != CONNECTION_NAME:
|
|
91
|
+
raise ValueError("Figma lookup requires the static figma connection")
|
|
92
|
+
self._start = _StartRequest()
|
|
93
|
+
self._after_first = _AfterFirst()
|
|
94
|
+
self._complete = _CompleteLookup()
|
|
95
|
+
self._file = new_get_file_step(
|
|
96
|
+
step_type=_FIRST_STEP_TYPE,
|
|
97
|
+
step_input_type=FigmaLookupRequest,
|
|
98
|
+
annotations={**_ANNOTATION_GROUP, "explanation": "Read Figma records."},
|
|
99
|
+
connection=connection,
|
|
100
|
+
connection_name=CONNECTION_NAME,
|
|
101
|
+
map_to_operation_input=map_to_get_file_input,
|
|
102
|
+
found=self._after_first,
|
|
103
|
+
)
|
|
104
|
+
self._comments = new_get_file_comments_step(
|
|
105
|
+
step_type=_SECOND_STEP_TYPE,
|
|
106
|
+
step_input_type=GetFileCommentsInput,
|
|
107
|
+
annotations={**_ANNOTATION_GROUP, "explanation": "Read the related Figma resource."},
|
|
108
|
+
connection=connection,
|
|
109
|
+
connection_name=CONNECTION_NAME,
|
|
110
|
+
map_to_operation_input=map_to_get_file_comments_input,
|
|
111
|
+
listed=self._complete,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def get_flow_type(self) -> str:
|
|
115
|
+
"""Return the stable Flow identity used in Dex Web."""
|
|
116
|
+
return FLOW_TYPE
|
|
117
|
+
|
|
118
|
+
def get_steps(self) -> dex.StepList[FigmaLookupRequest]:
|
|
119
|
+
"""Register the start, both Figma reads, and their result Steps."""
|
|
120
|
+
return dex.StepList.start_step(self._start).other_steps(
|
|
121
|
+
self._file, self._after_first, self._comments, self._complete
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def get_persistence_schema(self) -> dex.PersistenceSchema:
|
|
125
|
+
"""Persist the requested file key between the two reads."""
|
|
126
|
+
return dex.PersistenceSchema.of(_FILE_KEY)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Copyright (c) 2026 Super Durable
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
"""Worker entrypoint for the Figma lookup Flow."""
|
|
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.figma import new_project_connection
|
|
14
|
+
|
|
15
|
+
from .flow import CONNECTION_NAME, FigmaLookupFlow
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def run_worker() -> None:
|
|
19
|
+
"""Load the named Dex Web connection and serve Figma lookups."""
|
|
20
|
+
project = await load_from_environment()
|
|
21
|
+
connection = None
|
|
22
|
+
cache = None
|
|
23
|
+
try:
|
|
24
|
+
connection = new_project_connection(project, CONNECTION_NAME)
|
|
25
|
+
registry = dex.Registry([FigmaLookupFlow(connection)], allow_async_handlers=True)
|
|
26
|
+
cache_directory = os.environ.get(
|
|
27
|
+
"DEX_BLOB_CACHE_DIR", str(Path(tempfile.gettempdir()) / "dex-figma-lookup-blobs")
|
|
28
|
+
)
|
|
29
|
+
cache = dex.open_blob_cache(dex.BlobCacheConfig(cache_directory, 1 << 30))
|
|
30
|
+
worker = dex.AsyncWorker(
|
|
31
|
+
registry,
|
|
32
|
+
cache,
|
|
33
|
+
dex.WorkerOptions(server_address=os.environ.get("DEX_FLOW_SERVICE_ADDRESS", "localhost:8801")),
|
|
34
|
+
)
|
|
35
|
+
async with worker:
|
|
36
|
+
await worker.start()
|
|
37
|
+
finally:
|
|
38
|
+
if connection is not None:
|
|
39
|
+
await connection.aclose()
|
|
40
|
+
if cache is not None:
|
|
41
|
+
cache.close()
|
|
42
|
+
await project.close()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def main() -> None:
|
|
46
|
+
"""Run until the Worker stops or the process receives an interrupt."""
|
|
47
|
+
asyncio.run(run_worker())
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
main()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "dex-connector-figma"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Figma connector for Dex Flows"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
dependencies = ["dex-connectors-sdk==0.1.0", "dex-python-sdk>=1.28.0", "httpx>=0.27"]
|
|
13
|
+
|
|
14
|
+
[dependency-groups]
|
|
15
|
+
dev = ["pytest>=8", "pytest-asyncio>=0.24", "mypy>=1.13", "ruff>=0.7", "pyyaml>=6", "types-PyYAML>=6"]
|
|
16
|
+
|
|
17
|
+
[tool.hatch.build.targets.wheel]
|
|
18
|
+
packages = ["src/dex_connectors"]
|
|
19
|
+
|
|
20
|
+
[tool.pytest.ini_options]
|
|
21
|
+
testpaths = ["tests"]
|
|
22
|
+
pythonpath = [".", "src"]
|
|
23
|
+
asyncio_mode = "auto"
|
|
24
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
25
|
+
|
|
26
|
+
[tool.mypy]
|
|
27
|
+
python_version = "3.11"
|
|
28
|
+
strict = true
|
|
29
|
+
mypy_path = "src"
|
|
30
|
+
explicit_package_bases = true
|
|
31
|
+
|
|
32
|
+
[tool.ruff]
|
|
33
|
+
target-version = "py311"
|
|
34
|
+
line-length = 110
|
|
35
|
+
|
|
36
|
+
[tool.ruff.lint]
|
|
37
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Copyright (c) 2026 Super Durable
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
"""Figma file and comment Queries for Dex connector Steps."""
|
|
4
|
+
|
|
5
|
+
from .client import (
|
|
6
|
+
CONNECTOR_ID,
|
|
7
|
+
DEFECT,
|
|
8
|
+
FOUND,
|
|
9
|
+
GET_FILE_COMMENTS_DEFINITION,
|
|
10
|
+
GET_FILE_DEFINITION,
|
|
11
|
+
INVALID_RESPONSE,
|
|
12
|
+
LISTED,
|
|
13
|
+
NOT_FOUND,
|
|
14
|
+
PROVIDER_REJECTED,
|
|
15
|
+
Client,
|
|
16
|
+
Config,
|
|
17
|
+
Credentials,
|
|
18
|
+
GetFileCommentsOperation,
|
|
19
|
+
GetFileOperation,
|
|
20
|
+
)
|
|
21
|
+
from .connection import Connection, new_connection, new_connection_with_operations, new_project_connection
|
|
22
|
+
from .models import File, FileComment, FileComments, GetFileCommentsInput, GetFileInput
|
|
23
|
+
from .steps import new_get_file_comments_step, new_get_file_step
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"Client",
|
|
27
|
+
"Config",
|
|
28
|
+
"Connection",
|
|
29
|
+
"Credentials",
|
|
30
|
+
"File",
|
|
31
|
+
"FileComment",
|
|
32
|
+
"FileComments",
|
|
33
|
+
"GetFileCommentsInput",
|
|
34
|
+
"GetFileCommentsOperation",
|
|
35
|
+
"GetFileInput",
|
|
36
|
+
"GetFileOperation",
|
|
37
|
+
"new_connection",
|
|
38
|
+
"new_connection_with_operations",
|
|
39
|
+
"new_get_file_comments_step",
|
|
40
|
+
"new_get_file_step",
|
|
41
|
+
"new_project_connection",
|
|
42
|
+
"CONNECTOR_ID",
|
|
43
|
+
"DEFECT",
|
|
44
|
+
"FOUND",
|
|
45
|
+
"GET_FILE_COMMENTS_DEFINITION",
|
|
46
|
+
"GET_FILE_DEFINITION",
|
|
47
|
+
"INVALID_RESPONSE",
|
|
48
|
+
"LISTED",
|
|
49
|
+
"NOT_FOUND",
|
|
50
|
+
"PROVIDER_REJECTED",
|
|
51
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
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
|
+
base_url: str = "https://api.figma.com/v1"
|
|
14
|
+
timeout_seconds: float = 30
|
|
15
|
+
max_response_bytes: int = 8388608
|
|
16
|
+
|
|
17
|
+
setattr(Config, '__module__', 'dex_connectors.figma.client')
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class Credentials:
|
|
22
|
+
|
|
23
|
+
api_key: SecretString
|
|
24
|
+
|
|
25
|
+
setattr(Credentials, '__module__', 'dex_connectors.figma.client')
|
|
@@ -0,0 +1,137 @@
|
|
|
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, Query, Call, SecretString
|
|
9
|
+
from dex_connectors.sdk.projectconfig import ConnectionKey, LoadedProject
|
|
10
|
+
|
|
11
|
+
from ..client import Client, Config, Credentials
|
|
12
|
+
from .definitions import CONNECTOR_ID, GET_FILE_COMMENTS_DEFINITION, GET_FILE_DEFINITION
|
|
13
|
+
from ..models import File, FileComments, GetFileCommentsInput, GetFileInput
|
|
14
|
+
|
|
15
|
+
class Connection:
|
|
16
|
+
|
|
17
|
+
__slots__ = ('_get_file', '_get_file_comments', '_client', 'reference')
|
|
18
|
+
|
|
19
|
+
def __init__(self,
|
|
20
|
+
reference: ConnectionRef,
|
|
21
|
+
get_file: Query[GetFileInput, File],
|
|
22
|
+
get_file_comments: Query[GetFileCommentsInput, FileComments],
|
|
23
|
+
client: Client | None = None,
|
|
24
|
+
) -> None:
|
|
25
|
+
reference.validate()
|
|
26
|
+
if reference.provider != CONNECTOR_ID or get_file is None or get_file_comments is None:
|
|
27
|
+
raise ValueError("Figma connection provider and operations are required")
|
|
28
|
+
if get_file.definition().operation != GET_FILE_DEFINITION.operation:
|
|
29
|
+
raise ValueError("Figma connection operation identity differs")
|
|
30
|
+
if get_file_comments.definition().operation != GET_FILE_COMMENTS_DEFINITION.operation:
|
|
31
|
+
raise ValueError("Figma connection operation identity differs")
|
|
32
|
+
self.reference = reference
|
|
33
|
+
self._get_file = get_file
|
|
34
|
+
self._get_file_comments = get_file_comments
|
|
35
|
+
self._client = client
|
|
36
|
+
|
|
37
|
+
def get_file(self) -> Query[GetFileInput, File]:
|
|
38
|
+
return self._get_file
|
|
39
|
+
|
|
40
|
+
def get_file_comments(self) -> Query[GetFileCommentsInput, FileComments]:
|
|
41
|
+
return self._get_file_comments
|
|
42
|
+
|
|
43
|
+
async def aclose(self) -> None:
|
|
44
|
+
if self._client is not None:
|
|
45
|
+
await self._client.aclose()
|
|
46
|
+
|
|
47
|
+
def __repr__(self) -> str:
|
|
48
|
+
return "figma.Connection([REDACTED])"
|
|
49
|
+
|
|
50
|
+
def __reduce__(self) -> tuple[object, ...]:
|
|
51
|
+
raise TypeError("Figma connections cannot be serialized")
|
|
52
|
+
|
|
53
|
+
setattr(Connection, '__module__', 'dex_connectors.figma.connection')
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def new_connection(client: Client, reference: ConnectionRef) -> Connection:
|
|
57
|
+
if client is None:
|
|
58
|
+
raise ValueError("Figma client is required")
|
|
59
|
+
return Connection(reference, client.get_file(), client.get_file_comments(), client)
|
|
60
|
+
|
|
61
|
+
setattr(new_connection, '__module__', 'dex_connectors.figma.connection')
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def new_connection_with_operations(
|
|
65
|
+
get_file: Query[GetFileInput, File],
|
|
66
|
+
get_file_comments: Query[GetFileCommentsInput, FileComments],
|
|
67
|
+
reference: ConnectionRef,
|
|
68
|
+
) -> Connection:
|
|
69
|
+
return Connection(reference, get_file, get_file_comments)
|
|
70
|
+
|
|
71
|
+
setattr(new_connection_with_operations, '__module__', 'dex_connectors.figma.connection')
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class _ProjectCredentialSource:
|
|
75
|
+
|
|
76
|
+
def __init__(self, project: LoadedProject, key: ConnectionKey) -> None:
|
|
77
|
+
self.project = project
|
|
78
|
+
self.key = key
|
|
79
|
+
|
|
80
|
+
async def resolve(self, call: Call) -> Credentials:
|
|
81
|
+
material = await self.project.resolve_credential(self.key, str(call.id))
|
|
82
|
+
if not isinstance(material, dict):
|
|
83
|
+
raise ValueError("Figma stored credential does not match the connector credential fields")
|
|
84
|
+
if set(material) - set(('api_key',)):
|
|
85
|
+
raise ValueError("Figma stored credential does not match the connector credential fields")
|
|
86
|
+
api_key = material.get("api_key")
|
|
87
|
+
if api_key is not None and not (isinstance(api_key, str)):
|
|
88
|
+
raise ValueError("Figma credential api_key has an invalid type")
|
|
89
|
+
if not api_key:
|
|
90
|
+
raise ValueError("Figma credential api_key is required")
|
|
91
|
+
return Credentials(SecretString(api_key))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def new_project_connection(
|
|
95
|
+
project: LoadedProject, connection_name: str, http_client: httpx.AsyncClient | None = None
|
|
96
|
+
) -> Connection:
|
|
97
|
+
if project is None:
|
|
98
|
+
raise ValueError("Figma connection requires loaded project configuration")
|
|
99
|
+
reference = ConnectionRef(CONNECTOR_ID, connection_name)
|
|
100
|
+
reference.validate()
|
|
101
|
+
key = ConnectionKey(CONNECTOR_ID, connection_name)
|
|
102
|
+
matching = [
|
|
103
|
+
entry
|
|
104
|
+
for entry in project.configuration.connections
|
|
105
|
+
if entry.connector_id == CONNECTOR_ID and entry.connection_name == connection_name
|
|
106
|
+
]
|
|
107
|
+
if len(matching) != 1:
|
|
108
|
+
raise ValueError("Figma project connection is missing or duplicated")
|
|
109
|
+
if matching[0].provider != CONNECTOR_ID:
|
|
110
|
+
raise ValueError("Figma project connection provider differs")
|
|
111
|
+
config = _decode_config(matching[0].configuration)
|
|
112
|
+
client = Client(config, _ProjectCredentialSource(project, key), http_client)
|
|
113
|
+
return new_connection(client, reference)
|
|
114
|
+
|
|
115
|
+
setattr(new_project_connection, '__module__', 'dex_connectors.figma.connection')
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _decode_config(value: dict[str, Any]) -> Config:
|
|
119
|
+
if set(value) - {"baseUrl", "timeout", "maxResponseBytes"}:
|
|
120
|
+
raise ValueError("Figma configuration has unknown fields")
|
|
121
|
+
config_arguments: dict[str, Any] = {}
|
|
122
|
+
if "baseUrl" in value:
|
|
123
|
+
base_url = value["baseUrl"]
|
|
124
|
+
if not (isinstance(base_url, str)):
|
|
125
|
+
raise ValueError("Figma configuration baseUrl has an invalid type")
|
|
126
|
+
config_arguments['base_url'] = base_url
|
|
127
|
+
if "timeout" in value:
|
|
128
|
+
timeout = value["timeout"]
|
|
129
|
+
if not (type(timeout) is int):
|
|
130
|
+
raise ValueError("Figma configuration timeout has an invalid type")
|
|
131
|
+
config_arguments['timeout_seconds'] = timeout / 1_000_000_000
|
|
132
|
+
if "maxResponseBytes" in value:
|
|
133
|
+
max_response_bytes = value["maxResponseBytes"]
|
|
134
|
+
if not (type(max_response_bytes) is int):
|
|
135
|
+
raise ValueError("Figma configuration maxResponseBytes has an invalid type")
|
|
136
|
+
config_arguments['max_response_bytes'] = max_response_bytes
|
|
137
|
+
return Config(**config_arguments)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Copyright (c) 2026 Super Durable
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
# ruff: noqa
|
|
4
|
+
|
|
5
|
+
from datetime import timedelta
|
|
6
|
+
|
|
7
|
+
from dex_connectors.sdk import (
|
|
8
|
+
BranchDefinition,
|
|
9
|
+
MutationDefinition,
|
|
10
|
+
OperationRef,
|
|
11
|
+
QueryDefinition,
|
|
12
|
+
RetryPolicy,
|
|
13
|
+
StepDefaults,
|
|
14
|
+
default_connector_step_execute_retry_policy,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
CONNECTOR_ID = "figma"
|
|
18
|
+
PROVIDER = "figma"
|
|
19
|
+
GET_FILE_OPERATION_ID = "getFile"
|
|
20
|
+
GET_FILE_COMMENTS_OPERATION_ID = "getFileComments"
|
|
21
|
+
DEFECT = "defect"
|
|
22
|
+
FOUND = "found"
|
|
23
|
+
INVALID_RESPONSE = "invalidResponse"
|
|
24
|
+
LISTED = "listed"
|
|
25
|
+
NOT_FOUND = "notFound"
|
|
26
|
+
PROVIDER_REJECTED = "providerRejected"
|
|
27
|
+
|
|
28
|
+
GET_FILE_DEFINITION = QueryDefinition(
|
|
29
|
+
OperationRef(CONNECTOR_ID, GET_FILE_OPERATION_ID),
|
|
30
|
+
(
|
|
31
|
+
BranchDefinition(FOUND, "Figma returned the requested data.", False),
|
|
32
|
+
BranchDefinition(NOT_FOUND, "Figma does not have the requested resource.", True),
|
|
33
|
+
BranchDefinition(PROVIDER_REJECTED, "Figma conclusively rejected the read.", True),
|
|
34
|
+
BranchDefinition(INVALID_RESPONSE, "Figma returned malformed or oversized JSON.", True),
|
|
35
|
+
BranchDefinition(DEFECT, "Local input or connection credentials are invalid.", True),
|
|
36
|
+
),
|
|
37
|
+
StepDefaults(
|
|
38
|
+
timedelta(seconds=45),
|
|
39
|
+
2,
|
|
40
|
+
execute_retry=default_connector_step_execute_retry_policy(),
|
|
41
|
+
),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
GET_FILE_COMMENTS_DEFINITION = QueryDefinition(
|
|
45
|
+
OperationRef(CONNECTOR_ID, GET_FILE_COMMENTS_OPERATION_ID),
|
|
46
|
+
(
|
|
47
|
+
BranchDefinition(LISTED, "Figma returned the requested data.", False),
|
|
48
|
+
BranchDefinition(NOT_FOUND, "Figma does not have the requested resource.", True),
|
|
49
|
+
BranchDefinition(PROVIDER_REJECTED, "Figma conclusively rejected the read.", True),
|
|
50
|
+
BranchDefinition(INVALID_RESPONSE, "Figma returned malformed or oversized JSON.", True),
|
|
51
|
+
BranchDefinition(DEFECT, "Local input or connection credentials are invalid.", True),
|
|
52
|
+
),
|
|
53
|
+
StepDefaults(
|
|
54
|
+
timedelta(seconds=45),
|
|
55
|
+
2,
|
|
56
|
+
execute_retry=default_connector_step_execute_retry_policy(),
|
|
57
|
+
),
|
|
58
|
+
)
|