contextbase-plugin-workflowy 0.2.3__py3-none-any.whl
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.
- contextbase_plugin_workflowy-0.2.3.dist-info/METADATA +12 -0
- contextbase_plugin_workflowy-0.2.3.dist-info/RECORD +15 -0
- contextbase_plugin_workflowy-0.2.3.dist-info/WHEEL +4 -0
- plugin_workflowy/__init__.py +0 -0
- plugin_workflowy/binding_config.py +7 -0
- plugin_workflowy/component.py +104 -0
- plugin_workflowy/defs/__init__.py +0 -0
- plugin_workflowy/defs/defs.yaml +1 -0
- plugin_workflowy/models/__init__.py +0 -0
- plugin_workflowy/models/ctx.py +21 -0
- plugin_workflowy/models/ingress.py +51 -0
- plugin_workflowy/models/translators.py +112 -0
- plugin_workflowy/plugin.json +7 -0
- plugin_workflowy/sources/__init__.py +0 -0
- plugin_workflowy/sources/snapshot.py +138 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: contextbase-plugin-workflowy
|
|
3
|
+
Version: 0.2.3
|
|
4
|
+
Summary: Workflowy plugin for ContextBase
|
|
5
|
+
Author: Alizain Feerasta
|
|
6
|
+
Author-email: Alizain Feerasta <alizain.feerasta@gmail.com>
|
|
7
|
+
Requires-Dist: contextbase-shared-plugins==0.2.3
|
|
8
|
+
Requires-Dist: dagster==1.12.14
|
|
9
|
+
Requires-Dist: dagster-dlt==0.28.14
|
|
10
|
+
Requires-Dist: dlt>=1.26.0
|
|
11
|
+
Requires-Dist: pydantic>=2.12.0
|
|
12
|
+
Requires-Python: >=3.14, <3.15
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
plugin_workflowy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
plugin_workflowy/binding_config.py,sha256=asvUwjEKy1SEizvIenPckWIhRuTMwqIhgxjwif46ozs,160
|
|
3
|
+
plugin_workflowy/component.py,sha256=oB9mnQydO9CrZSK6QKzbQiAy7THzf20Ejq65RivgAbQ,3584
|
|
4
|
+
plugin_workflowy/defs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
plugin_workflowy/defs/defs.yaml,sha256=RFaHPrv3Xo84AZsLb18TrDEOLfpKMsX7axl-oRrYaT4,56
|
|
6
|
+
plugin_workflowy/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
plugin_workflowy/models/ctx.py,sha256=BcRgt9VDa6JojYHO0Bnn0Ql-XOxgdXez9RwE_P3QyJM,543
|
|
8
|
+
plugin_workflowy/models/ingress.py,sha256=pb_hXjJaslOFE3WrNx-iA6At5PEN7XT6Ufyzemaa2R0,1477
|
|
9
|
+
plugin_workflowy/models/translators.py,sha256=QKlStO_VP9lqJ73bq70maJl5VHtOU8Ok6tB6Dylmp0s,3228
|
|
10
|
+
plugin_workflowy/plugin.json,sha256=QLcgMi80sLJy2UbjY7w96GQ3hXV1roRICFYgoyWIWA0,85
|
|
11
|
+
plugin_workflowy/sources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
plugin_workflowy/sources/snapshot.py,sha256=VHLh-84QLtWgQlyjrmjygs168a8UqJjtCSWHseg1kjY,4337
|
|
13
|
+
contextbase_plugin_workflowy-0.2.3.dist-info/WHEEL,sha256=i9aSRDivn5iP9LaR1BLQX2GNAuriQWPsFwbbWygTX2k,81
|
|
14
|
+
contextbase_plugin_workflowy-0.2.3.dist-info/METADATA,sha256=X1ITkMjM3yjgF1-TTTOY4fr3W1RjW1Z2JS5VSAce3oQ,406
|
|
15
|
+
contextbase_plugin_workflowy-0.2.3.dist-info/RECORD,,
|
|
File without changes
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import dagster as dg
|
|
2
|
+
from dagster import AssetExecutionContext
|
|
3
|
+
from dagster_dlt import DagsterDltResource
|
|
4
|
+
from shared_plugins.automation import non_overlapping_automation_condition
|
|
5
|
+
from shared_plugins.bindings import (
|
|
6
|
+
parse_binding_config,
|
|
7
|
+
require_api_key,
|
|
8
|
+
)
|
|
9
|
+
from shared_plugins.control_plane import ControlPlaneClient
|
|
10
|
+
from shared_plugins.dlt import resolve_partition_binding, run_dlt_pipeline
|
|
11
|
+
from shared_plugins.naming import (
|
|
12
|
+
dagster_asset_group_name,
|
|
13
|
+
dagster_asset_tags,
|
|
14
|
+
dagster_dlt_asset_key,
|
|
15
|
+
dagster_partition_def_name,
|
|
16
|
+
dagster_pool_name,
|
|
17
|
+
dlt_source_name,
|
|
18
|
+
plugin_id_from_module,
|
|
19
|
+
)
|
|
20
|
+
from shared_plugins.resources import DLT_RESOURCE
|
|
21
|
+
|
|
22
|
+
from .binding_config import WorkflowyBindingConfig
|
|
23
|
+
from .sources.snapshot import workflowy_snapshot_source
|
|
24
|
+
|
|
25
|
+
PLUGIN_ID = plugin_id_from_module(__file__)
|
|
26
|
+
SNAPSHOT_JOB = "snapshot"
|
|
27
|
+
SNAPSHOT_SOURCE_NAME = dlt_source_name(PLUGIN_ID, SNAPSHOT_JOB)
|
|
28
|
+
SNAPSHOT_NODES_ASSET_KEY = dagster_dlt_asset_key(SNAPSHOT_SOURCE_NAME, "nodes")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _build_snapshot_specs(
|
|
32
|
+
partitions_def: dg.PartitionsDefinition,
|
|
33
|
+
automation_condition: dg.AutomationCondition,
|
|
34
|
+
) -> list[dg.AssetSpec]:
|
|
35
|
+
return [
|
|
36
|
+
dg.AssetSpec(
|
|
37
|
+
key=SNAPSHOT_NODES_ASSET_KEY,
|
|
38
|
+
group_name=dagster_asset_group_name(PLUGIN_ID),
|
|
39
|
+
tags=dagster_asset_tags(PLUGIN_ID),
|
|
40
|
+
automation_condition=automation_condition,
|
|
41
|
+
partitions_def=partitions_def,
|
|
42
|
+
)
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class WorkflowySyncComponent(dg.Component):
|
|
47
|
+
def build_defs(self, context: dg.ComponentLoadContext) -> dg.Definitions:
|
|
48
|
+
partitions_def = dg.DynamicPartitionsDefinition(
|
|
49
|
+
name=dagster_partition_def_name(PLUGIN_ID)
|
|
50
|
+
)
|
|
51
|
+
snapshot_specs = _build_snapshot_specs(
|
|
52
|
+
partitions_def,
|
|
53
|
+
non_overlapping_automation_condition(
|
|
54
|
+
dg.AutomationCondition.on_missing()
|
|
55
|
+
| dg.AutomationCondition.on_cron("*/15 * * * *")
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
@dg.multi_asset(
|
|
60
|
+
specs=snapshot_specs,
|
|
61
|
+
can_subset=True,
|
|
62
|
+
name="workflowy_snapshot",
|
|
63
|
+
pool=dagster_pool_name(PLUGIN_ID),
|
|
64
|
+
)
|
|
65
|
+
def workflowy_snapshot_assets(
|
|
66
|
+
context: AssetExecutionContext,
|
|
67
|
+
dlt_resource: DagsterDltResource,
|
|
68
|
+
control_plane: dg.ResourceParam[ControlPlaneClient],
|
|
69
|
+
):
|
|
70
|
+
binding = resolve_partition_binding(
|
|
71
|
+
context=context,
|
|
72
|
+
control_plane=control_plane,
|
|
73
|
+
plugin_id=PLUGIN_ID,
|
|
74
|
+
)
|
|
75
|
+
binding_id = str(binding.binding_id)
|
|
76
|
+
parse_binding_config(binding, WorkflowyBindingConfig)
|
|
77
|
+
|
|
78
|
+
source = workflowy_snapshot_source(
|
|
79
|
+
binding_id,
|
|
80
|
+
api_key=require_api_key(binding).api_key,
|
|
81
|
+
)
|
|
82
|
+
yield from run_dlt_pipeline(
|
|
83
|
+
context=context,
|
|
84
|
+
dlt_resource=dlt_resource,
|
|
85
|
+
source=source,
|
|
86
|
+
plugin_id=PLUGIN_ID,
|
|
87
|
+
binding_id=binding_id,
|
|
88
|
+
job_name=SNAPSHOT_JOB,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
automation_sensor = dg.AutomationConditionSensorDefinition(
|
|
92
|
+
name="workflowy_automation_sensor",
|
|
93
|
+
target=dg.AssetSelection.assets(workflowy_snapshot_assets),
|
|
94
|
+
default_status=dg.DefaultSensorStatus.RUNNING,
|
|
95
|
+
minimum_interval_seconds=30,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
return dg.Definitions(
|
|
99
|
+
assets=[workflowy_snapshot_assets],
|
|
100
|
+
sensors=[automation_sensor],
|
|
101
|
+
resources={
|
|
102
|
+
"dlt_resource": DLT_RESOURCE,
|
|
103
|
+
},
|
|
104
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
type: plugin_workflowy.component.WorkflowySyncComponent
|
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import AwareDatetime
|
|
6
|
+
from shared_plugins.models import CtxModel, IdStr
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class WorkflowyNodeRow(CtxModel):
|
|
10
|
+
id: IdStr
|
|
11
|
+
name: str = ""
|
|
12
|
+
note: str | None = None
|
|
13
|
+
parent_id: str | None = None
|
|
14
|
+
depth: int = 0
|
|
15
|
+
breadcrumb: str = ""
|
|
16
|
+
created_at: AwareDatetime | None = None
|
|
17
|
+
modified_at: AwareDatetime | None = None
|
|
18
|
+
completed_at: AwareDatetime | None = None
|
|
19
|
+
completed: bool = False
|
|
20
|
+
priority: int = 0
|
|
21
|
+
data: dict[str, Any] | None = None
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pydantic import (
|
|
4
|
+
AwareDatetime,
|
|
5
|
+
AliasChoices,
|
|
6
|
+
Field,
|
|
7
|
+
model_validator,
|
|
8
|
+
)
|
|
9
|
+
from shared_plugins.models import IdStr, IngressModel
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class WorkflowyNodeIngress(IngressModel):
|
|
13
|
+
id: IdStr
|
|
14
|
+
|
|
15
|
+
name: str = Field(default="", strict=True)
|
|
16
|
+
note: str | None = None
|
|
17
|
+
parent_id: str | None = Field(
|
|
18
|
+
default=None,
|
|
19
|
+
validation_alias=AliasChoices(
|
|
20
|
+
"parent_id", "parentId", "parentID", "parent", "parent-id"
|
|
21
|
+
),
|
|
22
|
+
)
|
|
23
|
+
priority: int = Field(
|
|
24
|
+
default=0,
|
|
25
|
+
strict=True,
|
|
26
|
+
validation_alias=AliasChoices("priority", "priority_id", "priorityId"),
|
|
27
|
+
)
|
|
28
|
+
created_at: AwareDatetime | None = Field(
|
|
29
|
+
default=None,
|
|
30
|
+
validation_alias=AliasChoices("createdAt", "created_at"),
|
|
31
|
+
)
|
|
32
|
+
modified_at: AwareDatetime | None = Field(
|
|
33
|
+
default=None,
|
|
34
|
+
validation_alias=AliasChoices("modifiedAt", "modified_at"),
|
|
35
|
+
)
|
|
36
|
+
completed_at: AwareDatetime | None = Field(
|
|
37
|
+
default=None,
|
|
38
|
+
validation_alias=AliasChoices("completedAt", "completed_at"),
|
|
39
|
+
)
|
|
40
|
+
completed: bool = Field(
|
|
41
|
+
default=False,
|
|
42
|
+
strict=True,
|
|
43
|
+
validation_alias=AliasChoices("completed", "is_completed", "isCompleted"),
|
|
44
|
+
)
|
|
45
|
+
data: dict[str, object] | None = None
|
|
46
|
+
|
|
47
|
+
@model_validator(mode="after")
|
|
48
|
+
def _derive_completed(self) -> WorkflowyNodeIngress:
|
|
49
|
+
if self.completed_at is not None:
|
|
50
|
+
self.completed = True
|
|
51
|
+
return self
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from collections.abc import Iterable, Iterator
|
|
5
|
+
|
|
6
|
+
from .ctx import WorkflowyNodeRow
|
|
7
|
+
from .ingress import WorkflowyNodeIngress
|
|
8
|
+
|
|
9
|
+
# Keep these as module constants so behavior is explicit and easy to adjust.
|
|
10
|
+
INCLUDE_COMPLETED = True
|
|
11
|
+
BREADCRUMB_SEGMENT_MAX_LEN = 100
|
|
12
|
+
BREADCRUMB_SEPARATOR = " > "
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _completed_subtree_ids(
|
|
16
|
+
nodes: list[WorkflowyNodeIngress],
|
|
17
|
+
) -> set[str]:
|
|
18
|
+
"""Return IDs of all completed nodes and every descendant beneath them."""
|
|
19
|
+
completed_ids = {n.id for n in nodes if n.completed}
|
|
20
|
+
if not completed_ids:
|
|
21
|
+
return set()
|
|
22
|
+
|
|
23
|
+
children_of: dict[str | None, list[str]] = defaultdict(list)
|
|
24
|
+
for n in nodes:
|
|
25
|
+
children_of[n.parent_id].append(n.id)
|
|
26
|
+
|
|
27
|
+
excluded: set[str] = set()
|
|
28
|
+
stack = list(completed_ids)
|
|
29
|
+
while stack:
|
|
30
|
+
node_id = stack.pop()
|
|
31
|
+
if node_id in excluded:
|
|
32
|
+
continue
|
|
33
|
+
excluded.add(node_id)
|
|
34
|
+
stack.extend(children_of.get(node_id, ()))
|
|
35
|
+
|
|
36
|
+
return excluded
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _truncate(text: str, max_len: int) -> str:
|
|
40
|
+
if len(text) <= max_len:
|
|
41
|
+
return text
|
|
42
|
+
return text[: max_len - 1] + "\u2026"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _compute_tree_attrs(
|
|
46
|
+
nodes_by_id: dict[str, WorkflowyNodeIngress],
|
|
47
|
+
) -> dict[str, tuple[int, str]]:
|
|
48
|
+
"""Compute (depth, breadcrumb) for every node. Caches results to avoid
|
|
49
|
+
repeated ancestor walks."""
|
|
50
|
+
cache: dict[str, tuple[int, str]] = {}
|
|
51
|
+
|
|
52
|
+
def resolve(node_id: str) -> tuple[int, str]:
|
|
53
|
+
if node_id in cache:
|
|
54
|
+
return cache[node_id]
|
|
55
|
+
|
|
56
|
+
node = nodes_by_id[node_id]
|
|
57
|
+
segment = _truncate(node.name, BREADCRUMB_SEGMENT_MAX_LEN)
|
|
58
|
+
|
|
59
|
+
if node.parent_id is None or node.parent_id not in nodes_by_id:
|
|
60
|
+
result = (0, segment)
|
|
61
|
+
else:
|
|
62
|
+
parent_depth, parent_breadcrumb = resolve(node.parent_id)
|
|
63
|
+
result = (
|
|
64
|
+
parent_depth + 1,
|
|
65
|
+
parent_breadcrumb + BREADCRUMB_SEPARATOR + segment,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
cache[node_id] = result
|
|
69
|
+
return result
|
|
70
|
+
|
|
71
|
+
for node_id in nodes_by_id:
|
|
72
|
+
resolve(node_id)
|
|
73
|
+
|
|
74
|
+
return cache
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def workflowy_nodes_to_ctx_models(
|
|
78
|
+
*,
|
|
79
|
+
binding_id: str,
|
|
80
|
+
nodes: Iterable[WorkflowyNodeIngress],
|
|
81
|
+
include_completed: bool = INCLUDE_COMPLETED,
|
|
82
|
+
) -> Iterator[WorkflowyNodeRow]:
|
|
83
|
+
all_nodes = list(nodes)
|
|
84
|
+
|
|
85
|
+
if include_completed:
|
|
86
|
+
excluded: set[str] = set()
|
|
87
|
+
else:
|
|
88
|
+
excluded = _completed_subtree_ids(all_nodes)
|
|
89
|
+
|
|
90
|
+
live_nodes = [n for n in all_nodes if n.id not in excluded]
|
|
91
|
+
nodes_by_id = {n.id: n for n in live_nodes}
|
|
92
|
+
tree_attrs = _compute_tree_attrs(nodes_by_id)
|
|
93
|
+
|
|
94
|
+
for node in live_nodes:
|
|
95
|
+
depth, breadcrumb = tree_attrs[node.id]
|
|
96
|
+
|
|
97
|
+
yield WorkflowyNodeRow(
|
|
98
|
+
ctx_binding_id=binding_id,
|
|
99
|
+
ctx_source_updated_at=node.modified_at,
|
|
100
|
+
id=node.id,
|
|
101
|
+
name=node.name,
|
|
102
|
+
note=node.note,
|
|
103
|
+
parent_id=node.parent_id,
|
|
104
|
+
depth=depth,
|
|
105
|
+
breadcrumb=breadcrumb,
|
|
106
|
+
created_at=node.created_at,
|
|
107
|
+
modified_at=node.modified_at,
|
|
108
|
+
completed_at=node.completed_at,
|
|
109
|
+
completed=node.completed,
|
|
110
|
+
priority=node.priority,
|
|
111
|
+
data=node.data,
|
|
112
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
from collections.abc import Iterator
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import dlt
|
|
10
|
+
from dlt.sources.rest_api import rest_api_resources
|
|
11
|
+
from requests import Response
|
|
12
|
+
from shared_plugins.naming import (
|
|
13
|
+
dlt_resource_name,
|
|
14
|
+
dlt_source_name,
|
|
15
|
+
plugin_id_from_module,
|
|
16
|
+
)
|
|
17
|
+
from shared_plugins.resources import ctx_dlt_resource
|
|
18
|
+
|
|
19
|
+
from ..models.ctx import WorkflowyNodeRow
|
|
20
|
+
from ..models.ingress import WorkflowyNodeIngress
|
|
21
|
+
from ..models.translators import workflowy_nodes_to_ctx_models
|
|
22
|
+
|
|
23
|
+
PLUGIN_ID = plugin_id_from_module(__file__)
|
|
24
|
+
JOB = "snapshot"
|
|
25
|
+
WORKFLOWY_BASE_URL = "https://beta.workflowy.com/api/v1"
|
|
26
|
+
WORKFLOWY_EXPORT_PATH = "nodes-export"
|
|
27
|
+
DEFAULT_API_KEY_HEADER = "X-Workflowy-Api-Key"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _extract_nodes_from_export_payload(payload: object) -> list[Mapping[str, Any]]:
|
|
31
|
+
if isinstance(payload, list):
|
|
32
|
+
raw_nodes = payload
|
|
33
|
+
elif isinstance(payload, dict):
|
|
34
|
+
if isinstance(payload.get("items"), list):
|
|
35
|
+
raw_nodes = payload["items"]
|
|
36
|
+
elif isinstance(payload.get("nodes"), list):
|
|
37
|
+
raw_nodes = payload["nodes"]
|
|
38
|
+
elif isinstance(payload.get("data"), list):
|
|
39
|
+
raw_nodes = payload["data"]
|
|
40
|
+
else:
|
|
41
|
+
raise RuntimeError(
|
|
42
|
+
"Workflowy export response did not include a nodes list under items/nodes/data."
|
|
43
|
+
)
|
|
44
|
+
else:
|
|
45
|
+
raise RuntimeError(
|
|
46
|
+
"Workflowy export response was neither a JSON array nor object."
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
extracted: list[Mapping[str, Any]] = []
|
|
50
|
+
for entry in raw_nodes:
|
|
51
|
+
if not isinstance(entry, Mapping):
|
|
52
|
+
raise RuntimeError(
|
|
53
|
+
"Workflowy export nodes list contained a non-object entry."
|
|
54
|
+
)
|
|
55
|
+
extracted.append(entry)
|
|
56
|
+
return extracted
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _canonicalize_workflowy_export_response(
|
|
60
|
+
response: Response,
|
|
61
|
+
*args: object,
|
|
62
|
+
**kwargs: object,
|
|
63
|
+
) -> None:
|
|
64
|
+
if response.status_code >= 400:
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
payload = response.json()
|
|
69
|
+
except ValueError as exc:
|
|
70
|
+
raise RuntimeError("Workflowy export returned invalid JSON.") from exc
|
|
71
|
+
|
|
72
|
+
nodes = _extract_nodes_from_export_payload(payload)
|
|
73
|
+
response._content = json.dumps({"nodes": nodes}).encode("utf-8")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def build_workflowy_export_nodes_resource(*, api_key: str) -> Any:
|
|
77
|
+
normalized_api_key = api_key.strip()
|
|
78
|
+
if not normalized_api_key:
|
|
79
|
+
raise RuntimeError("Workflowy API key cannot be empty.")
|
|
80
|
+
|
|
81
|
+
return rest_api_resources(
|
|
82
|
+
{
|
|
83
|
+
"client": {
|
|
84
|
+
"base_url": WORKFLOWY_BASE_URL,
|
|
85
|
+
"auth": {
|
|
86
|
+
"type": "bearer",
|
|
87
|
+
"token": normalized_api_key,
|
|
88
|
+
},
|
|
89
|
+
"headers": {
|
|
90
|
+
"Accept": "application/json",
|
|
91
|
+
DEFAULT_API_KEY_HEADER: normalized_api_key,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
"resources": [
|
|
95
|
+
{
|
|
96
|
+
"name": "workflowy_export_nodes",
|
|
97
|
+
"selected": False,
|
|
98
|
+
"endpoint": {
|
|
99
|
+
"path": WORKFLOWY_EXPORT_PATH,
|
|
100
|
+
"method": "GET",
|
|
101
|
+
"paginator": "single_page",
|
|
102
|
+
"data_selector": "nodes",
|
|
103
|
+
"response_actions": [_canonicalize_workflowy_export_response],
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
],
|
|
107
|
+
}
|
|
108
|
+
)[0]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _ingress_nodes_from_raw(
|
|
112
|
+
raw_nodes: Iterable[Mapping[str, Any]],
|
|
113
|
+
) -> Iterator[WorkflowyNodeIngress]:
|
|
114
|
+
for raw_node in raw_nodes:
|
|
115
|
+
yield WorkflowyNodeIngress.model_validate(raw_node)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dlt.source(name=dlt_source_name(PLUGIN_ID, JOB))
|
|
119
|
+
def workflowy_snapshot_source(
|
|
120
|
+
binding_id: str,
|
|
121
|
+
*,
|
|
122
|
+
api_key: str,
|
|
123
|
+
) -> tuple[Any, ...]:
|
|
124
|
+
raw_nodes_resource = build_workflowy_export_nodes_resource(api_key=api_key)
|
|
125
|
+
|
|
126
|
+
@ctx_dlt_resource(
|
|
127
|
+
name=dlt_resource_name("nodes"),
|
|
128
|
+
write_disposition="merge",
|
|
129
|
+
primary_key=("_ctx_binding_id", "id"),
|
|
130
|
+
)
|
|
131
|
+
def workflowy_nodes_resource() -> Iterator[WorkflowyNodeRow]:
|
|
132
|
+
ingress_nodes = _ingress_nodes_from_raw(raw_nodes_resource)
|
|
133
|
+
yield from workflowy_nodes_to_ctx_models(
|
|
134
|
+
binding_id=binding_id,
|
|
135
|
+
nodes=ingress_nodes,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
return (workflowy_nodes_resource,)
|