contextbase-plugin-icloud-tabs-local 0.2.4__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_icloud_tabs_local-0.2.4.dist-info/METADATA +13 -0
- contextbase_plugin_icloud_tabs_local-0.2.4.dist-info/RECORD +16 -0
- contextbase_plugin_icloud_tabs_local-0.2.4.dist-info/WHEEL +4 -0
- plugin_icloud_tabs_local/__init__.py +0 -0
- plugin_icloud_tabs_local/binding_config.py +22 -0
- plugin_icloud_tabs_local/component.py +101 -0
- plugin_icloud_tabs_local/defs/__init__.py +0 -0
- plugin_icloud_tabs_local/defs/defs.yaml +1 -0
- plugin_icloud_tabs_local/models/__init__.py +0 -0
- plugin_icloud_tabs_local/models/base.py +7 -0
- plugin_icloud_tabs_local/models/ctx.py +19 -0
- plugin_icloud_tabs_local/models/ingress.py +47 -0
- plugin_icloud_tabs_local/models/translators.py +30 -0
- plugin_icloud_tabs_local/plugin.json +7 -0
- plugin_icloud_tabs_local/sources/__init__.py +0 -0
- plugin_icloud_tabs_local/sources/snapshot.py +72 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: contextbase-plugin-icloud-tabs-local
|
|
3
|
+
Version: 0.2.4
|
|
4
|
+
Summary: iCloud Tabs local plugin for ContextBase
|
|
5
|
+
Author: Alizain Feerasta
|
|
6
|
+
Author-email: Alizain Feerasta <alizain.feerasta@gmail.com>
|
|
7
|
+
Requires-Dist: contextbase-shared-plugins==0.2.4
|
|
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-Dist: sqlalchemy>=2.0.0
|
|
13
|
+
Requires-Python: >=3.14, <3.15
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
plugin_icloud_tabs_local/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
plugin_icloud_tabs_local/binding_config.py,sha256=AW0zzmbWcubUCYXgVQwOGoj0LU7goNobx8yo1HpuwMM,522
|
|
3
|
+
plugin_icloud_tabs_local/component.py,sha256=dNC7Oi6mgf1QtrarRYpCEbaS8oWhoE3tG1cf1jiIYqk,3569
|
|
4
|
+
plugin_icloud_tabs_local/defs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
plugin_icloud_tabs_local/defs/defs.yaml,sha256=-a43xxav0LBuw9fQilrY-uCIMLnjwb9k7uqcDY1iyWM,70
|
|
6
|
+
plugin_icloud_tabs_local/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
plugin_icloud_tabs_local/models/base.py,sha256=Fp7n4EfxQOxzqm34-D-rYyu60P88eZUIewCBNP_Iuc4,119
|
|
8
|
+
plugin_icloud_tabs_local/models/ctx.py,sha256=WnxMpLFuVYkNPMopDGHDjZ1N9trRTBAekrjeDz4J5dU,572
|
|
9
|
+
plugin_icloud_tabs_local/models/ingress.py,sha256=elYcjzDSqQTVU3Uc3TVaod77fZk7wEGP_XeL7R63lLY,1616
|
|
10
|
+
plugin_icloud_tabs_local/models/translators.py,sha256=YlNVly3eV6b0gYGdrhDv131h3poF33UtLFZ0tsMkW_g,1008
|
|
11
|
+
plugin_icloud_tabs_local/plugin.json,sha256=b_PphNQBDpYvECjtZgKP89iyBTzA2UNL_qVcQDJZ6SI,90
|
|
12
|
+
plugin_icloud_tabs_local/sources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
plugin_icloud_tabs_local/sources/snapshot.py,sha256=1SOq_ncpQbAz2a-w1jfJoW6R7M5Zhy6RCOIqHVWMaU4,2184
|
|
14
|
+
contextbase_plugin_icloud_tabs_local-0.2.4.dist-info/WHEEL,sha256=i9aSRDivn5iP9LaR1BLQX2GNAuriQWPsFwbbWygTX2k,81
|
|
15
|
+
contextbase_plugin_icloud_tabs_local-0.2.4.dist-info/METADATA,sha256=5zFnY640Df5QE26JRHgugIidoAjPyL8COldhbQ1URrQ,455
|
|
16
|
+
contextbase_plugin_icloud_tabs_local-0.2.4.dist-info/RECORD,,
|
|
File without changes
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
|
|
7
|
+
from shared_plugins.bindings import BaseBindingConfigModel, ResolvedPath
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ICloudTabsLocalBindingConfig(BaseBindingConfigModel):
|
|
11
|
+
cloud_tabs_db: ResolvedPath = Field(
|
|
12
|
+
default_factory=lambda: (
|
|
13
|
+
Path.home()
|
|
14
|
+
/ "Library"
|
|
15
|
+
/ "Containers"
|
|
16
|
+
/ "com.apple.Safari"
|
|
17
|
+
/ "Data"
|
|
18
|
+
/ "Library"
|
|
19
|
+
/ "Safari"
|
|
20
|
+
/ "CloudTabs.db"
|
|
21
|
+
),
|
|
22
|
+
)
|
|
@@ -0,0 +1,101 @@
|
|
|
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 parse_binding_config
|
|
6
|
+
from shared_plugins.control_plane import ControlPlaneClient
|
|
7
|
+
from shared_plugins.dlt import resolve_partition_binding, run_dlt_pipeline
|
|
8
|
+
from shared_plugins.naming import (
|
|
9
|
+
dagster_asset_group_name,
|
|
10
|
+
dagster_asset_tags,
|
|
11
|
+
dagster_dlt_asset_key,
|
|
12
|
+
dagster_partition_def_name,
|
|
13
|
+
dagster_pool_name,
|
|
14
|
+
dlt_source_name,
|
|
15
|
+
plugin_id_from_module,
|
|
16
|
+
)
|
|
17
|
+
from shared_plugins.resources import DLT_RESOURCE
|
|
18
|
+
|
|
19
|
+
from .binding_config import ICloudTabsLocalBindingConfig
|
|
20
|
+
from .sources.snapshot import icloud_tabs_local_snapshot_source
|
|
21
|
+
|
|
22
|
+
PLUGIN_ID = plugin_id_from_module(__file__)
|
|
23
|
+
SNAPSHOT_JOB = "snapshot"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _build_snapshot_specs(
|
|
27
|
+
partitions_def: dg.PartitionsDefinition,
|
|
28
|
+
automation_condition: dg.AutomationCondition,
|
|
29
|
+
) -> list[dg.AssetSpec]:
|
|
30
|
+
snapshot_source_name = dlt_source_name(PLUGIN_ID, SNAPSHOT_JOB)
|
|
31
|
+
shared = dict(
|
|
32
|
+
group_name=dagster_asset_group_name(PLUGIN_ID),
|
|
33
|
+
tags=dagster_asset_tags(PLUGIN_ID),
|
|
34
|
+
automation_condition=automation_condition,
|
|
35
|
+
partitions_def=partitions_def,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
return [
|
|
39
|
+
dg.AssetSpec(
|
|
40
|
+
key=dagster_dlt_asset_key(snapshot_source_name, "tab"),
|
|
41
|
+
**shared,
|
|
42
|
+
),
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ICloudTabsLocalSyncComponent(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=partitions_def,
|
|
53
|
+
automation_condition=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="icloud_tabs_local_snapshot",
|
|
63
|
+
pool=dagster_pool_name(PLUGIN_ID),
|
|
64
|
+
)
|
|
65
|
+
def icloud_tabs_local_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
|
+
cfg = parse_binding_config(binding, ICloudTabsLocalBindingConfig)
|
|
77
|
+
|
|
78
|
+
source = icloud_tabs_local_snapshot_source(binding_id, cfg)
|
|
79
|
+
yield from run_dlt_pipeline(
|
|
80
|
+
context=context,
|
|
81
|
+
dlt_resource=dlt_resource,
|
|
82
|
+
source=source,
|
|
83
|
+
plugin_id=PLUGIN_ID,
|
|
84
|
+
binding_id=binding_id,
|
|
85
|
+
job_name=SNAPSHOT_JOB,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
automation_sensor = dg.AutomationConditionSensorDefinition(
|
|
89
|
+
name="icloud_tabs_local_automation_sensor",
|
|
90
|
+
target=dg.AssetSelection.assets(icloud_tabs_local_snapshot_assets),
|
|
91
|
+
default_status=dg.DefaultSensorStatus.RUNNING,
|
|
92
|
+
minimum_interval_seconds=30,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return dg.Definitions(
|
|
96
|
+
assets=[icloud_tabs_local_snapshot_assets],
|
|
97
|
+
sensors=[automation_sensor],
|
|
98
|
+
resources={
|
|
99
|
+
"dlt_resource": DLT_RESOURCE,
|
|
100
|
+
},
|
|
101
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
type: plugin_icloud_tabs_local.component.ICloudTabsLocalSyncComponent
|
|
File without changes
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pydantic import AwareDatetime
|
|
4
|
+
from shared_plugins.models import CtxModel
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TabRow(CtxModel):
|
|
8
|
+
tab_uuid: str
|
|
9
|
+
device_uuid: str
|
|
10
|
+
device_name: str | None = None
|
|
11
|
+
device_type_identifier: str | None = None
|
|
12
|
+
device_last_modified: AwareDatetime | None = None
|
|
13
|
+
has_duplicate_device_name: int | None = None
|
|
14
|
+
is_ephemeral_device: int | None = None
|
|
15
|
+
title: str | None = None
|
|
16
|
+
url: str
|
|
17
|
+
is_pinned: int | None = None
|
|
18
|
+
is_showing_reader: int | None = None
|
|
19
|
+
last_viewed_time: AwareDatetime | None = None
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import String
|
|
6
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
7
|
+
from shared_plugins.sqlalchemy_types import AppleCoreDataTimestamp
|
|
8
|
+
|
|
9
|
+
from .base import Base
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CloudTabDevice(Base):
|
|
13
|
+
"""Row from cloud_tab_devices in CloudTabs.db."""
|
|
14
|
+
|
|
15
|
+
__tablename__ = "cloud_tab_devices"
|
|
16
|
+
|
|
17
|
+
device_uuid: Mapped[str] = mapped_column("device_uuid", String, primary_key=True)
|
|
18
|
+
device_name: Mapped[str | None] = mapped_column("device_name", String)
|
|
19
|
+
device_type_identifier: Mapped[str | None] = mapped_column(
|
|
20
|
+
"device_type_identifier",
|
|
21
|
+
String,
|
|
22
|
+
)
|
|
23
|
+
last_modified: Mapped[datetime] = mapped_column(
|
|
24
|
+
"last_modified",
|
|
25
|
+
AppleCoreDataTimestamp(),
|
|
26
|
+
)
|
|
27
|
+
has_duplicate_device_name: Mapped[int | None] = mapped_column(
|
|
28
|
+
"has_duplicate_device_name"
|
|
29
|
+
)
|
|
30
|
+
is_ephemeral_device: Mapped[int | None] = mapped_column("is_ephemeral_device")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CloudTab(Base):
|
|
34
|
+
"""Row from cloud_tabs in CloudTabs.db."""
|
|
35
|
+
|
|
36
|
+
__tablename__ = "cloud_tabs"
|
|
37
|
+
|
|
38
|
+
tab_uuid: Mapped[str] = mapped_column("tab_uuid", String, primary_key=True)
|
|
39
|
+
device_uuid: Mapped[str] = mapped_column("device_uuid", String)
|
|
40
|
+
title: Mapped[str | None] = mapped_column("title", String)
|
|
41
|
+
url: Mapped[str] = mapped_column("url", String)
|
|
42
|
+
is_pinned: Mapped[int | None] = mapped_column("is_pinned")
|
|
43
|
+
is_showing_reader: Mapped[int | None] = mapped_column("is_showing_reader")
|
|
44
|
+
last_viewed_time: Mapped[datetime | None] = mapped_column(
|
|
45
|
+
"last_viewed_time",
|
|
46
|
+
AppleCoreDataTimestamp(null_sentinels=(-1,)),
|
|
47
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable, Iterator
|
|
4
|
+
|
|
5
|
+
from .ctx import TabRow
|
|
6
|
+
from .ingress import CloudTab, CloudTabDevice
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def tabs_to_ctx_models(
|
|
10
|
+
*,
|
|
11
|
+
binding_id: str,
|
|
12
|
+
rows: Iterable[tuple[CloudTab, CloudTabDevice]],
|
|
13
|
+
) -> Iterator[TabRow]:
|
|
14
|
+
for tab, device in rows:
|
|
15
|
+
yield TabRow(
|
|
16
|
+
ctx_binding_id=binding_id,
|
|
17
|
+
ctx_source_updated_at=device.last_modified,
|
|
18
|
+
tab_uuid=tab.tab_uuid,
|
|
19
|
+
device_uuid=tab.device_uuid,
|
|
20
|
+
device_name=device.device_name,
|
|
21
|
+
device_type_identifier=device.device_type_identifier,
|
|
22
|
+
device_last_modified=device.last_modified,
|
|
23
|
+
has_duplicate_device_name=device.has_duplicate_device_name,
|
|
24
|
+
is_ephemeral_device=device.is_ephemeral_device,
|
|
25
|
+
title=tab.title,
|
|
26
|
+
url=tab.url,
|
|
27
|
+
is_pinned=tab.is_pinned,
|
|
28
|
+
is_showing_reader=tab.is_showing_reader,
|
|
29
|
+
last_viewed_time=tab.last_viewed_time,
|
|
30
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from collections.abc import Iterator
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import dlt
|
|
9
|
+
from shared_plugins.naming import (
|
|
10
|
+
dlt_resource_name,
|
|
11
|
+
dlt_source_name,
|
|
12
|
+
plugin_id_from_module,
|
|
13
|
+
)
|
|
14
|
+
from shared_plugins.resources import ctx_dlt_resource
|
|
15
|
+
from shared_plugins.sqlite import sqlite_snapshot
|
|
16
|
+
from sqlalchemy import create_engine, select
|
|
17
|
+
from sqlalchemy.orm import Session
|
|
18
|
+
|
|
19
|
+
from ..binding_config import ICloudTabsLocalBindingConfig
|
|
20
|
+
from ..models.ctx import TabRow
|
|
21
|
+
from ..models.ingress import CloudTab, CloudTabDevice
|
|
22
|
+
from ..models.translators import tabs_to_ctx_models
|
|
23
|
+
|
|
24
|
+
PLUGIN_ID = plugin_id_from_module(__file__)
|
|
25
|
+
JOB = "snapshot"
|
|
26
|
+
LOGGER = logging.getLogger(__name__)
|
|
27
|
+
MERGE_WRITE_DISPOSITION = {"disposition": "merge", "strategy": "delete-insert"}
|
|
28
|
+
MERGE_KEY = ("_ctx_binding_id",)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _load_tabs(
|
|
32
|
+
db_path: Path,
|
|
33
|
+
) -> list[tuple[CloudTab, CloudTabDevice]]:
|
|
34
|
+
if not db_path.is_file():
|
|
35
|
+
raise RuntimeError(f"Missing iCloud tabs database: '{db_path}'.")
|
|
36
|
+
with sqlite_snapshot(db_path) as snapshot_path:
|
|
37
|
+
engine = create_engine(f"sqlite:///{snapshot_path}")
|
|
38
|
+
try:
|
|
39
|
+
stmt = select(CloudTab, CloudTabDevice).join(
|
|
40
|
+
CloudTabDevice, CloudTab.device_uuid == CloudTabDevice.device_uuid
|
|
41
|
+
)
|
|
42
|
+
with Session(engine) as session:
|
|
43
|
+
rows = session.execute(stmt).all()
|
|
44
|
+
finally:
|
|
45
|
+
engine.dispose()
|
|
46
|
+
return rows
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dlt.source(name=dlt_source_name(PLUGIN_ID, JOB))
|
|
50
|
+
def icloud_tabs_local_snapshot_source(
|
|
51
|
+
binding_id: str,
|
|
52
|
+
cfg: ICloudTabsLocalBindingConfig,
|
|
53
|
+
) -> tuple[Any, ...]:
|
|
54
|
+
|
|
55
|
+
@ctx_dlt_resource(
|
|
56
|
+
name=dlt_resource_name("tab"),
|
|
57
|
+
write_disposition=MERGE_WRITE_DISPOSITION,
|
|
58
|
+
merge_key=MERGE_KEY,
|
|
59
|
+
primary_key=("_ctx_binding_id", "tab_uuid"),
|
|
60
|
+
)
|
|
61
|
+
def tab_resource() -> Iterator[TabRow]:
|
|
62
|
+
rows = _load_tabs(cfg.cloud_tabs_db)
|
|
63
|
+
LOGGER.info(
|
|
64
|
+
"icloud_tabs_local.snapshot_loaded tabs=%d",
|
|
65
|
+
len(rows),
|
|
66
|
+
)
|
|
67
|
+
yield from tabs_to_ctx_models(
|
|
68
|
+
binding_id=binding_id,
|
|
69
|
+
rows=rows,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
return (tab_resource,)
|