contextbase-plugin-granola 0.2.8__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_granola-0.2.8.dist-info/METADATA +12 -0
- contextbase_plugin_granola-0.2.8.dist-info/RECORD +15 -0
- contextbase_plugin_granola-0.2.8.dist-info/WHEEL +4 -0
- plugin_granola/__init__.py +1 -0
- plugin_granola/binding_config.py +5 -0
- plugin_granola/component.py +116 -0
- plugin_granola/defs/__init__.py +1 -0
- plugin_granola/defs/defs.yaml +1 -0
- plugin_granola/models/__init__.py +1 -0
- plugin_granola/models/ctx.py +68 -0
- plugin_granola/models/ingress.py +64 -0
- plugin_granola/models/translators.py +55 -0
- plugin_granola/plugin.json +7 -0
- plugin_granola/sources/__init__.py +1 -0
- plugin_granola/sources/snapshot.py +150 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: contextbase-plugin-granola
|
|
3
|
+
Version: 0.2.8
|
|
4
|
+
Summary: Granola plugin for ContextBase
|
|
5
|
+
Author: Alizain Feerasta
|
|
6
|
+
Author-email: Alizain Feerasta <alizain.feerasta@gmail.com>
|
|
7
|
+
Requires-Dist: contextbase-shared-plugins==0.2.8
|
|
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_granola/__init__.py,sha256=G6fbCWFZodK-1_ovN9pgUWDR5--cWs1k7kBruHjcQW4,30
|
|
2
|
+
plugin_granola/binding_config.py,sha256=pswh7ZwSmU879JJE5Me8tS2t2IPvngp-N8e0dQJ6D_0,122
|
|
3
|
+
plugin_granola/component.py,sha256=Pb804dvvmbB17DOXQjl60MWrLonh26LPqvxfjYtW1CY,4018
|
|
4
|
+
plugin_granola/defs/__init__.py,sha256=biedetESbQUyf_8Y3QtxWfvwpfNmfi6LsKWWxc1Tf5E,56
|
|
5
|
+
plugin_granola/defs/defs.yaml,sha256=E0IxqxDTc3JPVjDPxPHOGDU3EQAOcb9igsE6oZ9-YFc,52
|
|
6
|
+
plugin_granola/models/__init__.py,sha256=FEQN98wf1RzFO67kJGVa5LgdGLYz9kdq49ZoQxGG_xM,38
|
|
7
|
+
plugin_granola/models/ctx.py,sha256=alUkc1W_ePlGQWBQtPxB5zXr615J9UqXEmw8kYXW2Tk,2204
|
|
8
|
+
plugin_granola/models/ingress.py,sha256=cATCouL29OYjP5JhhobBZG5t6gVomaSkD2adqHGhoBM,1990
|
|
9
|
+
plugin_granola/models/translators.py,sha256=Sf3Kc9EQ_eb2bYOd27XECWzy6ZQ4MUCCKyU0VVyH0eU,1908
|
|
10
|
+
plugin_granola/plugin.json,sha256=wBtIQq0C5g_N-V1h-l3eSdw1Vv_5D3H9D3xblOco_No,83
|
|
11
|
+
plugin_granola/sources/__init__.py,sha256=OAS7xesKpk54KcG_cVsYD67B0H4wnK0r2hDfVrJbwFY,27
|
|
12
|
+
plugin_granola/sources/snapshot.py,sha256=D8mWU_Xti7kqKLNEM-zfIo6UsMv8EUSAEvVw1HZb4m4,5111
|
|
13
|
+
contextbase_plugin_granola-0.2.8.dist-info/WHEEL,sha256=i9aSRDivn5iP9LaR1BLQX2GNAuriQWPsFwbbWygTX2k,81
|
|
14
|
+
contextbase_plugin_granola-0.2.8.dist-info/METADATA,sha256=-gdpbG-HigOoi9yRCx7_TDMgTXq9UdZC7ufVNMMGNiY,402
|
|
15
|
+
contextbase_plugin_granola-0.2.8.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Granola plugin package."""
|
|
@@ -0,0 +1,116 @@
|
|
|
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 GranolaBindingConfig
|
|
23
|
+
from .sources.snapshot import granola_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_NOTE_ASSET_KEY = dagster_dlt_asset_key(SNAPSHOT_SOURCE_NAME, "note")
|
|
29
|
+
SNAPSHOT_TRANSCRIPT_SEGMENT_ASSET_KEY = dagster_dlt_asset_key(
|
|
30
|
+
SNAPSHOT_SOURCE_NAME,
|
|
31
|
+
"transcript_segment",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _build_snapshot_specs(
|
|
36
|
+
partitions_def: dg.PartitionsDefinition,
|
|
37
|
+
automation_condition: dg.AutomationCondition,
|
|
38
|
+
) -> list[dg.AssetSpec]:
|
|
39
|
+
return [
|
|
40
|
+
dg.AssetSpec(
|
|
41
|
+
key=SNAPSHOT_NOTE_ASSET_KEY,
|
|
42
|
+
group_name=dagster_asset_group_name(PLUGIN_ID),
|
|
43
|
+
tags=dagster_asset_tags(PLUGIN_ID),
|
|
44
|
+
automation_condition=automation_condition,
|
|
45
|
+
partitions_def=partitions_def,
|
|
46
|
+
),
|
|
47
|
+
dg.AssetSpec(
|
|
48
|
+
key=SNAPSHOT_TRANSCRIPT_SEGMENT_ASSET_KEY,
|
|
49
|
+
deps=[SNAPSHOT_NOTE_ASSET_KEY],
|
|
50
|
+
group_name=dagster_asset_group_name(PLUGIN_ID),
|
|
51
|
+
tags=dagster_asset_tags(PLUGIN_ID),
|
|
52
|
+
automation_condition=automation_condition,
|
|
53
|
+
partitions_def=partitions_def,
|
|
54
|
+
),
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class GranolaSyncComponent(dg.Component):
|
|
59
|
+
def build_defs(self, context: dg.ComponentLoadContext) -> dg.Definitions:
|
|
60
|
+
partitions_def = dg.DynamicPartitionsDefinition(
|
|
61
|
+
name=dagster_partition_def_name(PLUGIN_ID)
|
|
62
|
+
)
|
|
63
|
+
snapshot_specs = _build_snapshot_specs(
|
|
64
|
+
partitions_def,
|
|
65
|
+
non_overlapping_automation_condition(
|
|
66
|
+
dg.AutomationCondition.on_missing()
|
|
67
|
+
| dg.AutomationCondition.on_cron("*/15 * * * *")
|
|
68
|
+
),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
@dg.multi_asset(
|
|
72
|
+
specs=snapshot_specs,
|
|
73
|
+
can_subset=False,
|
|
74
|
+
name="granola_snapshot",
|
|
75
|
+
pool=dagster_pool_name(PLUGIN_ID),
|
|
76
|
+
)
|
|
77
|
+
def granola_snapshot_assets(
|
|
78
|
+
context: AssetExecutionContext,
|
|
79
|
+
dlt_resource: DagsterDltResource,
|
|
80
|
+
control_plane: dg.ResourceParam[ControlPlaneClient],
|
|
81
|
+
):
|
|
82
|
+
binding = resolve_partition_binding(
|
|
83
|
+
context=context,
|
|
84
|
+
control_plane=control_plane,
|
|
85
|
+
plugin_id=PLUGIN_ID,
|
|
86
|
+
)
|
|
87
|
+
binding_id = str(binding.binding_id)
|
|
88
|
+
parse_binding_config(binding, GranolaBindingConfig)
|
|
89
|
+
|
|
90
|
+
source = granola_snapshot_source(
|
|
91
|
+
binding_id,
|
|
92
|
+
api_key=require_api_key(binding).api_key,
|
|
93
|
+
)
|
|
94
|
+
yield from run_dlt_pipeline(
|
|
95
|
+
context=context,
|
|
96
|
+
dlt_resource=dlt_resource,
|
|
97
|
+
source=source,
|
|
98
|
+
plugin_id=PLUGIN_ID,
|
|
99
|
+
binding_id=binding_id,
|
|
100
|
+
job_name=SNAPSHOT_JOB,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
automation_sensor = dg.AutomationConditionSensorDefinition(
|
|
104
|
+
name="granola_automation_sensor",
|
|
105
|
+
target=dg.AssetSelection.assets(granola_snapshot_assets),
|
|
106
|
+
default_status=dg.DefaultSensorStatus.RUNNING,
|
|
107
|
+
minimum_interval_seconds=30,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
return dg.Definitions(
|
|
111
|
+
assets=[granola_snapshot_assets],
|
|
112
|
+
sensors=[automation_sensor],
|
|
113
|
+
resources={
|
|
114
|
+
"dlt_resource": DLT_RESOURCE,
|
|
115
|
+
},
|
|
116
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Dagster component definitions for plugin_granola."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
type: plugin_granola.component.GranolaSyncComponent
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Granola ingress and ctx models."""
|
|
@@ -0,0 +1,68 @@
|
|
|
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, NonNegativeInt
|
|
7
|
+
|
|
8
|
+
NOTE_COLUMNS = {
|
|
9
|
+
"id": {"description": "Granola note ID. Join to transcript_segment.note_id."},
|
|
10
|
+
"owner": {
|
|
11
|
+
"data_type": "json",
|
|
12
|
+
"description": "Granola owner object with email and optional display name.",
|
|
13
|
+
},
|
|
14
|
+
"calendar_event": {
|
|
15
|
+
"data_type": "json",
|
|
16
|
+
"description": "Granola calendar metadata when the note is linked to an event; NULL otherwise.",
|
|
17
|
+
},
|
|
18
|
+
"attendees": {
|
|
19
|
+
"data_type": "json",
|
|
20
|
+
"description": "Granola attendee objects for the note.",
|
|
21
|
+
},
|
|
22
|
+
"folder_membership": {
|
|
23
|
+
"data_type": "json",
|
|
24
|
+
"description": "Granola folders containing the note, including parent_folder_id for nested folders when present.",
|
|
25
|
+
},
|
|
26
|
+
"summary_text": {"description": "Granola-generated summary as plain text."},
|
|
27
|
+
"summary_markdown": {
|
|
28
|
+
"description": "Granola-generated summary as Markdown. NULL when the note has no generated summary."
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
TRANSCRIPT_SEGMENT_COLUMNS = {
|
|
33
|
+
"note_id": {"description": "Granola note ID. Join to note.id."},
|
|
34
|
+
"ordinal": {
|
|
35
|
+
"description": "0-based segment order within a note. Order by this to reconstruct the transcript."
|
|
36
|
+
},
|
|
37
|
+
"speaker": {
|
|
38
|
+
"data_type": "json",
|
|
39
|
+
"description": "Raw Granola speaker JSON. diarization_label can be NULL and is not a stable person identity.",
|
|
40
|
+
},
|
|
41
|
+
"text": {
|
|
42
|
+
"description": "Full text for this transcript segment; use note_id plus ordinal for context."
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class GranolaNoteRow(CtxModel):
|
|
48
|
+
id: IdStr
|
|
49
|
+
object: str
|
|
50
|
+
title: str | None = None
|
|
51
|
+
owner: dict[str, Any]
|
|
52
|
+
created_at: AwareDatetime
|
|
53
|
+
updated_at: AwareDatetime
|
|
54
|
+
web_url: str
|
|
55
|
+
calendar_event: dict[str, Any] | None = None
|
|
56
|
+
attendees: list[dict[str, Any]]
|
|
57
|
+
folder_membership: list[dict[str, Any]]
|
|
58
|
+
summary_text: str
|
|
59
|
+
summary_markdown: str | None = None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class GranolaTranscriptSegmentRow(CtxModel):
|
|
63
|
+
note_id: IdStr
|
|
64
|
+
ordinal: NonNegativeInt
|
|
65
|
+
speaker: dict[str, Any]
|
|
66
|
+
text: str
|
|
67
|
+
start_time: AwareDatetime
|
|
68
|
+
end_time: AwareDatetime
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
from pydantic import AwareDatetime, Field
|
|
6
|
+
from shared_plugins.models import IdStr, IngressModel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class GranolaPersonIngress(IngressModel):
|
|
10
|
+
email: str = Field(strict=True)
|
|
11
|
+
name: str | None = Field(default=None, strict=True)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class GranolaCalendarInviteeIngress(IngressModel):
|
|
15
|
+
email: str = Field(strict=True)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class GranolaCalendarEventIngress(IngressModel):
|
|
19
|
+
event_title: str = Field(strict=True)
|
|
20
|
+
invitees: list[GranolaCalendarInviteeIngress] = Field(default_factory=list)
|
|
21
|
+
organiser: str = Field(strict=True)
|
|
22
|
+
calendar_event_id: str = Field(strict=True)
|
|
23
|
+
scheduled_start_time: AwareDatetime
|
|
24
|
+
scheduled_end_time: AwareDatetime
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class GranolaFolderMembershipIngress(IngressModel):
|
|
28
|
+
id: IdStr
|
|
29
|
+
object: str = Field(strict=True)
|
|
30
|
+
name: str = Field(strict=True)
|
|
31
|
+
parent_folder_id: IdStr | None = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class GranolaSpeakerIngress(IngressModel):
|
|
35
|
+
source: str = Field(strict=True)
|
|
36
|
+
diarization_label: str | None = Field(default=None, strict=True)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class GranolaTranscriptSegmentIngress(IngressModel):
|
|
40
|
+
speaker: GranolaSpeakerIngress
|
|
41
|
+
text: str = Field(strict=True)
|
|
42
|
+
start_time: AwareDatetime
|
|
43
|
+
end_time: AwareDatetime
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class GranolaNoteListIngress(IngressModel):
|
|
47
|
+
id: IdStr
|
|
48
|
+
object: Literal["note"]
|
|
49
|
+
title: str | None = Field(default=None, strict=True)
|
|
50
|
+
owner: GranolaPersonIngress
|
|
51
|
+
created_at: AwareDatetime
|
|
52
|
+
updated_at: AwareDatetime
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class GranolaNoteDetailIngress(GranolaNoteListIngress):
|
|
56
|
+
web_url: str = Field(strict=True)
|
|
57
|
+
calendar_event: GranolaCalendarEventIngress | None = None
|
|
58
|
+
attendees: list[GranolaPersonIngress] = Field(default_factory=list)
|
|
59
|
+
folder_membership: list[GranolaFolderMembershipIngress] = Field(
|
|
60
|
+
default_factory=list
|
|
61
|
+
)
|
|
62
|
+
summary_text: str = Field(strict=True)
|
|
63
|
+
summary_markdown: str | None = Field(default=None, strict=True)
|
|
64
|
+
transcript: list[GranolaTranscriptSegmentIngress] | None = None
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable, Iterator
|
|
4
|
+
|
|
5
|
+
from .ctx import GranolaNoteRow, GranolaTranscriptSegmentRow
|
|
6
|
+
from .ingress import GranolaNoteDetailIngress
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def granola_notes_to_ctx_models(
|
|
10
|
+
*,
|
|
11
|
+
binding_id: str,
|
|
12
|
+
notes: Iterable[GranolaNoteDetailIngress],
|
|
13
|
+
) -> Iterator[GranolaNoteRow]:
|
|
14
|
+
for note in notes:
|
|
15
|
+
yield GranolaNoteRow(
|
|
16
|
+
ctx_binding_id=binding_id,
|
|
17
|
+
ctx_source_updated_at=note.updated_at,
|
|
18
|
+
id=note.id,
|
|
19
|
+
object=note.object,
|
|
20
|
+
title=note.title,
|
|
21
|
+
owner=note.owner.model_dump(mode="json"),
|
|
22
|
+
created_at=note.created_at,
|
|
23
|
+
updated_at=note.updated_at,
|
|
24
|
+
web_url=note.web_url,
|
|
25
|
+
calendar_event=(
|
|
26
|
+
note.calendar_event.model_dump(mode="json")
|
|
27
|
+
if note.calendar_event is not None
|
|
28
|
+
else None
|
|
29
|
+
),
|
|
30
|
+
attendees=[attendee.model_dump(mode="json") for attendee in note.attendees],
|
|
31
|
+
folder_membership=[
|
|
32
|
+
folder.model_dump(mode="json") for folder in note.folder_membership
|
|
33
|
+
],
|
|
34
|
+
summary_text=note.summary_text,
|
|
35
|
+
summary_markdown=note.summary_markdown,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def granola_transcript_segments_to_ctx_models(
|
|
40
|
+
*,
|
|
41
|
+
binding_id: str,
|
|
42
|
+
notes: Iterable[GranolaNoteDetailIngress],
|
|
43
|
+
) -> Iterator[GranolaTranscriptSegmentRow]:
|
|
44
|
+
for note in notes:
|
|
45
|
+
for ordinal, segment in enumerate(note.transcript or []):
|
|
46
|
+
yield GranolaTranscriptSegmentRow(
|
|
47
|
+
ctx_binding_id=binding_id,
|
|
48
|
+
ctx_source_updated_at=note.updated_at,
|
|
49
|
+
note_id=note.id,
|
|
50
|
+
ordinal=ordinal,
|
|
51
|
+
speaker=segment.speaker.model_dump(mode="json"),
|
|
52
|
+
text=segment.text,
|
|
53
|
+
start_time=segment.start_time,
|
|
54
|
+
end_time=segment.end_time,
|
|
55
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Granola dlt sources."""
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable, Iterator, Mapping
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import dlt
|
|
7
|
+
from dlt.sources.rest_api import rest_api_resources
|
|
8
|
+
from shared_plugins.naming import (
|
|
9
|
+
dlt_resource_name,
|
|
10
|
+
dlt_source_name,
|
|
11
|
+
plugin_id_from_module,
|
|
12
|
+
)
|
|
13
|
+
from shared_plugins.resources import ctx_dlt_transformer
|
|
14
|
+
|
|
15
|
+
from ..models.ctx import (
|
|
16
|
+
NOTE_COLUMNS,
|
|
17
|
+
TRANSCRIPT_SEGMENT_COLUMNS,
|
|
18
|
+
GranolaNoteRow,
|
|
19
|
+
GranolaTranscriptSegmentRow,
|
|
20
|
+
)
|
|
21
|
+
from ..models.ingress import GranolaNoteDetailIngress
|
|
22
|
+
from ..models.translators import (
|
|
23
|
+
granola_notes_to_ctx_models,
|
|
24
|
+
granola_transcript_segments_to_ctx_models,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
PLUGIN_ID = plugin_id_from_module(__file__)
|
|
28
|
+
JOB = "snapshot"
|
|
29
|
+
GRANOLA_BASE_URL = "https://public-api.granola.ai/v1"
|
|
30
|
+
GRANOLA_INITIAL_UPDATED_AFTER = "1970-01-01T00:00:00Z"
|
|
31
|
+
GRANOLA_PAGE_SIZE = 30
|
|
32
|
+
GRANOLA_NOTES_LIST_RESOURCE_NAME = "granola_notes_list"
|
|
33
|
+
GRANOLA_NOTE_DETAILS_RESOURCE_NAME = "granola_note_details"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def build_granola_note_details_resource(*, api_key: str) -> Any:
|
|
37
|
+
normalized_api_key = api_key.strip()
|
|
38
|
+
if not normalized_api_key:
|
|
39
|
+
raise RuntimeError("Granola API key cannot be empty.")
|
|
40
|
+
|
|
41
|
+
resources = rest_api_resources(
|
|
42
|
+
{
|
|
43
|
+
"client": {
|
|
44
|
+
"base_url": GRANOLA_BASE_URL,
|
|
45
|
+
"auth": {
|
|
46
|
+
"type": "bearer",
|
|
47
|
+
"token": normalized_api_key,
|
|
48
|
+
},
|
|
49
|
+
"headers": {
|
|
50
|
+
"Accept": "application/json",
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
"resources": [
|
|
54
|
+
{
|
|
55
|
+
"name": GRANOLA_NOTES_LIST_RESOURCE_NAME,
|
|
56
|
+
"selected": False,
|
|
57
|
+
"endpoint": {
|
|
58
|
+
"path": "notes",
|
|
59
|
+
"method": "GET",
|
|
60
|
+
"paginator": {
|
|
61
|
+
"type": "cursor",
|
|
62
|
+
"cursor_path": "cursor",
|
|
63
|
+
"cursor_param": "cursor",
|
|
64
|
+
},
|
|
65
|
+
"data_selector": "notes",
|
|
66
|
+
"params": {
|
|
67
|
+
"page_size": GRANOLA_PAGE_SIZE,
|
|
68
|
+
"updated_after": "{incremental.start_value}",
|
|
69
|
+
},
|
|
70
|
+
"incremental": {
|
|
71
|
+
"cursor_path": "updated_at",
|
|
72
|
+
"initial_value": GRANOLA_INITIAL_UPDATED_AFTER,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"name": GRANOLA_NOTE_DETAILS_RESOURCE_NAME,
|
|
78
|
+
"selected": False,
|
|
79
|
+
"endpoint": {
|
|
80
|
+
"path": f"notes/{{resources.{GRANOLA_NOTES_LIST_RESOURCE_NAME}.id}}",
|
|
81
|
+
"method": "GET",
|
|
82
|
+
"paginator": "single_page",
|
|
83
|
+
"data_selector": "$",
|
|
84
|
+
"params": {
|
|
85
|
+
"include": "transcript",
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
}
|
|
91
|
+
)
|
|
92
|
+
resources_by_name = {resource.name: resource for resource in resources}
|
|
93
|
+
return resources_by_name[GRANOLA_NOTE_DETAILS_RESOURCE_NAME]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _ingress_note_details_from_raw(
|
|
97
|
+
raw_notes: Mapping[str, Any] | Iterable[Mapping[str, Any]],
|
|
98
|
+
) -> Iterator[GranolaNoteDetailIngress]:
|
|
99
|
+
if isinstance(raw_notes, Mapping):
|
|
100
|
+
raw_notes = (raw_notes,)
|
|
101
|
+
for raw_note in raw_notes:
|
|
102
|
+
yield GranolaNoteDetailIngress.model_validate(raw_note)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dlt.source(name=dlt_source_name(PLUGIN_ID, JOB))
|
|
106
|
+
def granola_snapshot_source(
|
|
107
|
+
binding_id: str,
|
|
108
|
+
*,
|
|
109
|
+
api_key: str,
|
|
110
|
+
) -> tuple[Any, ...]:
|
|
111
|
+
note_details_resource = build_granola_note_details_resource(api_key=api_key)
|
|
112
|
+
|
|
113
|
+
@dlt.transformer(
|
|
114
|
+
data_from=note_details_resource,
|
|
115
|
+
name="granola_ingress_note_details",
|
|
116
|
+
selected=False,
|
|
117
|
+
)
|
|
118
|
+
def ingress_note_details_resource(
|
|
119
|
+
raw_note: Mapping[str, Any] | Iterable[Mapping[str, Any]],
|
|
120
|
+
) -> Iterator[GranolaNoteDetailIngress]:
|
|
121
|
+
yield from _ingress_note_details_from_raw(raw_note)
|
|
122
|
+
|
|
123
|
+
@ctx_dlt_transformer(
|
|
124
|
+
data_from=ingress_note_details_resource,
|
|
125
|
+
name=dlt_resource_name("note"),
|
|
126
|
+
write_disposition="merge",
|
|
127
|
+
primary_key=("_ctx_binding_id", "id"),
|
|
128
|
+
columns=NOTE_COLUMNS,
|
|
129
|
+
)
|
|
130
|
+
def note_resource(
|
|
131
|
+
note: GranolaNoteDetailIngress,
|
|
132
|
+
) -> Iterator[GranolaNoteRow]:
|
|
133
|
+
yield from granola_notes_to_ctx_models(binding_id=binding_id, notes=[note])
|
|
134
|
+
|
|
135
|
+
@ctx_dlt_transformer(
|
|
136
|
+
data_from=ingress_note_details_resource,
|
|
137
|
+
name=dlt_resource_name("transcript_segment"),
|
|
138
|
+
write_disposition="merge",
|
|
139
|
+
primary_key=("_ctx_binding_id", "note_id", "ordinal"),
|
|
140
|
+
columns=TRANSCRIPT_SEGMENT_COLUMNS,
|
|
141
|
+
)
|
|
142
|
+
def transcript_segment_resource(
|
|
143
|
+
note: GranolaNoteDetailIngress,
|
|
144
|
+
) -> Iterator[GranolaTranscriptSegmentRow]:
|
|
145
|
+
yield from granola_transcript_segments_to_ctx_models(
|
|
146
|
+
binding_id=binding_id,
|
|
147
|
+
notes=[note],
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
return (note_resource, transcript_segment_resource)
|