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.
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.11.15
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
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,7 @@
1
+ from __future__ import annotations
2
+
3
+ from sqlalchemy.orm import DeclarativeBase
4
+
5
+
6
+ class Base(DeclarativeBase):
7
+ pass
@@ -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
+ )
@@ -0,0 +1,7 @@
1
+ {
2
+ "auth": {
3
+ "type": "none"
4
+ },
5
+ "mode": "dagster",
6
+ "plugin_id": "icloud_tabs_local"
7
+ }
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,)