keep-up-with 0.1.0__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.
Files changed (83) hide show
  1. keep_up_with/__init__.py +1 -0
  2. keep_up_with/cli/__init__.py +0 -0
  3. keep_up_with/cli/agent/__init__.py +0 -0
  4. keep_up_with/cli/agent/events.py +67 -0
  5. keep_up_with/cli/agent/inbox.py +63 -0
  6. keep_up_with/cli/agent/main.py +21 -0
  7. keep_up_with/cli/agent/message.py +151 -0
  8. keep_up_with/cli/agent/output.py +34 -0
  9. keep_up_with/cli/agent/space.py +142 -0
  10. keep_up_with/cli/agent/subscriptions.py +87 -0
  11. keep_up_with/cli/agent/thread.py +186 -0
  12. keep_up_with/cli/agent/tools.py +113 -0
  13. keep_up_with/cli/user/__init__.py +0 -0
  14. keep_up_with/cli/user/codex_daemon.py +39 -0
  15. keep_up_with/cli/user/main.py +90 -0
  16. keep_up_with/cli/user/reset.py +58 -0
  17. keep_up_with/cli/user/setup.py +796 -0
  18. keep_up_with/cli/user/start.py +154 -0
  19. keep_up_with/cli/user/status.py +87 -0
  20. keep_up_with/cli/user/ui.py +607 -0
  21. keep_up_with/core/__init__.py +25 -0
  22. keep_up_with/core/config.py +123 -0
  23. keep_up_with/core/events.py +301 -0
  24. keep_up_with/integrations/__init__.py +0 -0
  25. keep_up_with/integrations/base.py +379 -0
  26. keep_up_with/integrations/data/__init__.py +0 -0
  27. keep_up_with/integrations/data/arxiv/__init__.py +14 -0
  28. keep_up_with/integrations/data/arxiv/client.py +497 -0
  29. keep_up_with/integrations/data/arxiv/tools.py +19 -0
  30. keep_up_with/integrations/data/browser/__init__.py +15 -0
  31. keep_up_with/integrations/data/browser/tools.py +41 -0
  32. keep_up_with/integrations/data/common.py +46 -0
  33. keep_up_with/integrations/data/image/__init__.py +14 -0
  34. keep_up_with/integrations/data/image/tools.py +266 -0
  35. keep_up_with/integrations/data/raindrop/__init__.py +21 -0
  36. keep_up_with/integrations/data/raindrop/client.py +66 -0
  37. keep_up_with/integrations/data/raindrop/subscription.py +22 -0
  38. keep_up_with/integrations/data/raindrop/tools.py +16 -0
  39. keep_up_with/integrations/data/reddit/__init__.py +29 -0
  40. keep_up_with/integrations/data/reddit/client.py +750 -0
  41. keep_up_with/integrations/data/reddit/subscription.py +101 -0
  42. keep_up_with/integrations/data/reddit/tools.py +61 -0
  43. keep_up_with/integrations/data/repo/__init__.py +14 -0
  44. keep_up_with/integrations/data/repo/tools.py +106 -0
  45. keep_up_with/integrations/data/video/__init__.py +20 -0
  46. keep_up_with/integrations/data/video/client.py +436 -0
  47. keep_up_with/integrations/data/video/tools.py +76 -0
  48. keep_up_with/integrations/data/web/__init__.py +26 -0
  49. keep_up_with/integrations/data/web/subscription.py +154 -0
  50. keep_up_with/integrations/data/web/tools.py +534 -0
  51. keep_up_with/integrations/data/x/__init__.py +23 -0
  52. keep_up_with/integrations/data/x/client.py +858 -0
  53. keep_up_with/integrations/data/x/subscription.py +32 -0
  54. keep_up_with/integrations/data/x/tools.py +45 -0
  55. keep_up_with/integrations/data/youtube/__init__.py +25 -0
  56. keep_up_with/integrations/data/youtube/client.py +209 -0
  57. keep_up_with/integrations/data/youtube/subscription.py +25 -0
  58. keep_up_with/integrations/data/youtube/tools.py +38 -0
  59. keep_up_with/integrations/messaging/__init__.py +0 -0
  60. keep_up_with/integrations/messaging/discord/__init__.py +20 -0
  61. keep_up_with/integrations/messaging/discord/client.py +979 -0
  62. keep_up_with/integrations/messaging/discord/payloads.py +32 -0
  63. keep_up_with/integrations/messaging/discord/setup.py +222 -0
  64. keep_up_with/integrations/messaging/discord/subscription.py +137 -0
  65. keep_up_with/integrations/messaging/file/__init__.py +13 -0
  66. keep_up_with/integrations/messaging/file/client.py +786 -0
  67. keep_up_with/integrations/registry.py +124 -0
  68. keep_up_with/resources/__init__.py +1 -0
  69. keep_up_with/resources/presets/ai.toml +165 -0
  70. keep_up_with/resources/workspace_template/AGENTS.md +113 -0
  71. keep_up_with/resources/workspace_template/skills/anti-slop/SKILL.md +130 -0
  72. keep_up_with/resources/workspace_template/skills/keep-up-with/SKILL.md +118 -0
  73. keep_up_with/resources/workspace_template/skills/keep-up-with/references/formatting.md +34 -0
  74. keep_up_with/resources/workspace_template/skills/keep-up-with/references/template.md +25 -0
  75. keep_up_with/resources/workspace_template/skills/keep-up-with/references/visuals.md +114 -0
  76. keep_up_with/runtime/__init__.py +0 -0
  77. keep_up_with/runtime/codex.py +92 -0
  78. keep_up_with/runtime/gateway.py +815 -0
  79. keep_up_with-0.1.0.dist-info/METADATA +146 -0
  80. keep_up_with-0.1.0.dist-info/RECORD +83 -0
  81. keep_up_with-0.1.0.dist-info/WHEEL +4 -0
  82. keep_up_with-0.1.0.dist-info/entry_points.txt +3 -0
  83. keep_up_with-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
File without changes
File without changes
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Annotated
4
+
5
+ import typer
6
+
7
+ from keep_up_with.cli.agent.output import echo_json, echo_jsonl, fail
8
+ from keep_up_with.core.config import get_config
9
+ from keep_up_with.core.events import EventStore
10
+
11
+ MAX_EVENTS = 100
12
+
13
+ app = typer.Typer(
14
+ add_completion=False,
15
+ invoke_without_command=True,
16
+ help="View recorded events",
17
+ no_args_is_help=True,
18
+ )
19
+
20
+
21
+ @app.command("list", help="Print recorded events as JSONL")
22
+ def list_command(
23
+ limit: Annotated[
24
+ int | None,
25
+ typer.Option(
26
+ "--limit",
27
+ "-n",
28
+ help=f"Maximum events to print, hard-capped at {MAX_EVENTS}",
29
+ min=1,
30
+ max=MAX_EVENTS,
31
+ ),
32
+ ] = None,
33
+ since: Annotated[
34
+ str | None,
35
+ typer.Option(help="Only include events at or after this ISO timestamp"),
36
+ ] = None,
37
+ until: Annotated[
38
+ str | None,
39
+ typer.Option(help="Only include events at or before this ISO timestamp"),
40
+ ] = None,
41
+ query: Annotated[
42
+ str | None,
43
+ typer.Option(
44
+ "--query",
45
+ "-q",
46
+ help="Only include events containing this text, e.g. a URL or keyword",
47
+ ),
48
+ ] = None,
49
+ ) -> None:
50
+ echo_jsonl(
51
+ EventStore(get_config()).list_events(
52
+ limit=limit or MAX_EVENTS,
53
+ since=since,
54
+ until=until,
55
+ query=query,
56
+ )
57
+ )
58
+
59
+
60
+ @app.command("show", help="Show one event by id or unique prefix")
61
+ def show_command(
62
+ event_id: Annotated[str, typer.Argument(help="Event id or unique prefix")],
63
+ ) -> None:
64
+ event = EventStore(get_config()).get_event(event_id)
65
+ if event is None:
66
+ fail("unknown or ambiguous event", id=event_id)
67
+ echo_json(event)
@@ -0,0 +1,63 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Annotated
4
+
5
+ import typer
6
+
7
+ from keep_up_with.cli.agent.output import echo_json, echo_jsonl, fail
8
+ from keep_up_with.core.config import get_config
9
+ from keep_up_with.core.events import EventStore
10
+
11
+ app = typer.Typer(
12
+ add_completion=False,
13
+ invoke_without_command=True,
14
+ help="Handle pending events",
15
+ no_args_is_help=True,
16
+ )
17
+
18
+
19
+ @app.command("list", help="Print pending events as JSONL")
20
+ def list_command(
21
+ unnotified: Annotated[
22
+ bool,
23
+ typer.Option("--unnotified", help="Only show items not yet sent into Codex"),
24
+ ] = False,
25
+ dismissed: Annotated[
26
+ bool,
27
+ typer.Option(
28
+ "--dismissed",
29
+ help="Show dismissed items with their dispositions instead of pending ones",
30
+ ),
31
+ ] = False,
32
+ ) -> None:
33
+ echo_jsonl(
34
+ EventStore(get_config()).list_inbox(
35
+ only_unnotified=unnotified,
36
+ dismissed=dismissed,
37
+ )
38
+ )
39
+
40
+
41
+ @app.command("dismiss", help="Resolve pending events, recording their disposition")
42
+ def dismiss_command(
43
+ event_ids: Annotated[
44
+ list[str],
45
+ typer.Argument(help="Event ids or unique prefixes; batch ids that share one disposition"),
46
+ ],
47
+ reason: Annotated[
48
+ str,
49
+ typer.Option(
50
+ "--reason",
51
+ help="Disposition: the published message/thread link, the prior coverage, or why it was skipped",
52
+ ),
53
+ ],
54
+ ) -> None:
55
+ store = EventStore(get_config())
56
+ unknown = []
57
+ for event_id in event_ids:
58
+ if store.dismiss_inbox(event_id, reason=reason):
59
+ echo_json({"dismissed": True, "id": event_id, "reason": reason})
60
+ else:
61
+ unknown.append(event_id)
62
+ if unknown:
63
+ fail("unknown inbox items", ids=unknown)
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ import typer
4
+
5
+ from keep_up_with.cli.agent import events, inbox, message, space, subscriptions, thread, tools
6
+
7
+ app = typer.Typer(
8
+ add_completion=False,
9
+ invoke_without_command=True,
10
+ help="Agent-side commands for keep-up-with",
11
+ no_args_is_help=True,
12
+ )
13
+
14
+
15
+ app.add_typer(events.app, name="events")
16
+ app.add_typer(inbox.app, name="inbox")
17
+ app.add_typer(message.app, name="message")
18
+ app.add_typer(space.app, name="space")
19
+ app.add_typer(thread.app, name="thread")
20
+ app.add_typer(tools.app, name="tools")
21
+ app.add_typer(subscriptions.app, name="subs")
@@ -0,0 +1,151 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from typing import Annotated
5
+
6
+ import typer
7
+
8
+ from keep_up_with.cli.agent.output import echo_json, echo_jsonl, fail
9
+ from keep_up_with.core.config import get_config
10
+ from keep_up_with.integrations.registry import messaging_client
11
+
12
+ app = typer.Typer(
13
+ add_completion=False,
14
+ invoke_without_command=True,
15
+ help="Send and list messages",
16
+ no_args_is_help=True,
17
+ )
18
+
19
+
20
+ @app.command("send", help="Send a message")
21
+ def send_command(
22
+ text: Annotated[str, typer.Option("--text", "-t", help="Message text")] = "",
23
+ channel: Annotated[str | None, typer.Option(help="Channel name or id")] = None,
24
+ reply_to: Annotated[str | None, typer.Option(help="Message id to reply to")] = None,
25
+ attachment: Annotated[
26
+ list[str] | None,
27
+ typer.Option(
28
+ "--attachment",
29
+ "-a",
30
+ help="File path to attach, repeat for multiple files",
31
+ ),
32
+ ] = None,
33
+ ) -> None:
34
+ client = messaging_client(get_config())
35
+ try:
36
+ result = asyncio.run(
37
+ client.send_message(
38
+ text=text,
39
+ channel=channel,
40
+ reply_to=reply_to,
41
+ attachments=attachment or [],
42
+ )
43
+ )
44
+ except ValueError as error:
45
+ fail(str(error))
46
+ echo_json(result)
47
+
48
+
49
+ @app.command("list", help="List recent messages")
50
+ def list_command(
51
+ channel: Annotated[str | None, typer.Option(help="Channel name or id")] = None,
52
+ thread_id: Annotated[str | None, typer.Option(help="Thread id")] = None,
53
+ limit: Annotated[
54
+ int,
55
+ typer.Option(
56
+ "--limit", "-n", help="Maximum recent messages to scan per channel"
57
+ ),
58
+ ] = 25,
59
+ query: Annotated[
60
+ str | None,
61
+ typer.Option(
62
+ "--query",
63
+ "-q",
64
+ help="Only include messages containing text; without --channel or --thread-id this searches every channel and the DM",
65
+ ),
66
+ ] = None,
67
+ author: Annotated[
68
+ str | None,
69
+ typer.Option(help="Only include messages by author id or exact name"),
70
+ ] = None,
71
+ ) -> None:
72
+ client = messaging_client(get_config())
73
+ try:
74
+ messages = asyncio.run(
75
+ client.list_messages(
76
+ channel=channel,
77
+ thread_id=thread_id,
78
+ limit=limit,
79
+ query=query,
80
+ author=author,
81
+ )
82
+ )
83
+ except ValueError as error:
84
+ fail(str(error))
85
+ echo_jsonl(messages)
86
+
87
+
88
+ @app.command("around", help="Show messages around a message")
89
+ def around_command(
90
+ message_id: Annotated[str, typer.Argument(help="Message id")],
91
+ channel: Annotated[str | None, typer.Option(help="Channel name or id")] = None,
92
+ thread_id: Annotated[str | None, typer.Option(help="Thread id")] = None,
93
+ before: Annotated[int, typer.Option(help="Messages before the target")] = 10,
94
+ after: Annotated[int, typer.Option(help="Messages after the target")] = 20,
95
+ ) -> None:
96
+ client = messaging_client(get_config())
97
+ try:
98
+ messages = asyncio.run(
99
+ client.messages_around(
100
+ message_id=message_id,
101
+ channel=channel,
102
+ thread_id=thread_id,
103
+ before=before,
104
+ after=after,
105
+ )
106
+ )
107
+ except ValueError as error:
108
+ fail(str(error))
109
+ echo_jsonl(messages)
110
+
111
+
112
+ @app.command("edit", help="Edit a message")
113
+ def edit_command(
114
+ message_id: Annotated[str, typer.Argument(help="Message id")],
115
+ text: Annotated[str, typer.Option("--text", "-t", help="Replacement text")],
116
+ channel: Annotated[str | None, typer.Option(help="Channel name or id")] = None,
117
+ thread_id: Annotated[str | None, typer.Option(help="Thread id")] = None,
118
+ ) -> None:
119
+ client = messaging_client(get_config())
120
+ try:
121
+ result = asyncio.run(
122
+ client.edit_message(
123
+ message_id=message_id,
124
+ text=text,
125
+ channel=channel,
126
+ thread_id=thread_id,
127
+ )
128
+ )
129
+ except ValueError as error:
130
+ fail(str(error))
131
+ echo_json(result)
132
+
133
+
134
+ @app.command("delete", help="Delete a message")
135
+ def delete_command(
136
+ message_id: Annotated[str, typer.Argument(help="Message id")],
137
+ channel: Annotated[str | None, typer.Option(help="Channel name or id")] = None,
138
+ thread_id: Annotated[str | None, typer.Option(help="Thread id")] = None,
139
+ ) -> None:
140
+ client = messaging_client(get_config())
141
+ try:
142
+ asyncio.run(
143
+ client.delete_message(
144
+ message_id=message_id,
145
+ channel=channel,
146
+ thread_id=thread_id,
147
+ )
148
+ )
149
+ except ValueError as error:
150
+ fail(str(error))
151
+ echo_json({"deleted": True, "id": message_id})
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import asdict, is_dataclass
5
+ from typing import Any
6
+
7
+ import typer
8
+
9
+
10
+ def echo_json(value: Any, *, err: bool = False) -> None:
11
+ typer.echo(
12
+ json.dumps(_jsonable(value), ensure_ascii=False, sort_keys=True),
13
+ err=err,
14
+ )
15
+
16
+
17
+ def echo_jsonl(values: Any) -> None:
18
+ for value in values:
19
+ echo_json(value)
20
+
21
+
22
+ def fail(message: str, **extra: Any) -> None:
23
+ echo_json({"error": message, **extra}, err=True)
24
+ raise typer.Exit(1)
25
+
26
+
27
+ def _jsonable(value: Any) -> Any:
28
+ if is_dataclass(value):
29
+ return _jsonable(asdict(value))
30
+ if isinstance(value, dict):
31
+ return {str(key): _jsonable(item) for key, item in value.items()}
32
+ if isinstance(value, (list, tuple, set)):
33
+ return [_jsonable(item) for item in value]
34
+ return value
@@ -0,0 +1,142 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from typing import Annotated
5
+
6
+ import typer
7
+
8
+ from keep_up_with.cli.agent.output import echo_json, echo_jsonl
9
+ from keep_up_with.core.config import get_config
10
+ from keep_up_with.integrations.registry import messaging_client
11
+
12
+ app = typer.Typer(
13
+ add_completion=False,
14
+ invoke_without_command=True,
15
+ help="Manage message space layout",
16
+ no_args_is_help=True,
17
+ )
18
+ channels_app = typer.Typer(
19
+ add_completion=False,
20
+ invoke_without_command=True,
21
+ help="Manage channels",
22
+ no_args_is_help=True,
23
+ )
24
+ sections_app = typer.Typer(
25
+ add_completion=False,
26
+ invoke_without_command=True,
27
+ help="Manage channel sections",
28
+ no_args_is_help=True,
29
+ )
30
+
31
+
32
+ @channels_app.command("list", help="List channels")
33
+ def list_channels_command() -> None:
34
+ client = messaging_client(get_config())
35
+ echo_jsonl(asyncio.run(client.list_channels()))
36
+
37
+
38
+ @channels_app.command("create", help="Create a channel")
39
+ def create_channel_command(
40
+ name: Annotated[str, typer.Option(help="Channel name")],
41
+ section: Annotated[str | None, typer.Option(help="Section name or id")] = None,
42
+ description: Annotated[
43
+ str | None,
44
+ typer.Option(help="Channel description"),
45
+ ] = None,
46
+ ) -> None:
47
+ client = messaging_client(get_config())
48
+ echo_json(
49
+ asyncio.run(
50
+ client.create_channel(
51
+ name=name,
52
+ section=section,
53
+ description=description,
54
+ )
55
+ )
56
+ )
57
+
58
+
59
+ @channels_app.command("rename", help="Rename a channel")
60
+ def rename_channel_command(
61
+ channel: Annotated[str, typer.Option(help="Channel name or id")],
62
+ name: Annotated[str, typer.Option(help="New channel name")],
63
+ ) -> None:
64
+ client = messaging_client(get_config())
65
+ echo_json(asyncio.run(client.rename_channel(channel=channel, name=name)))
66
+
67
+
68
+ @channels_app.command("move", help="Move a channel")
69
+ def move_channel_command(
70
+ channel: Annotated[str, typer.Option(help="Channel name or id")],
71
+ section: Annotated[str | None, typer.Option(help="Section name or id")] = None,
72
+ before: Annotated[
73
+ str | None,
74
+ typer.Option(help="Place before this channel name or id"),
75
+ ] = None,
76
+ after: Annotated[
77
+ str | None,
78
+ typer.Option(help="Place after this channel name or id"),
79
+ ] = None,
80
+ ) -> None:
81
+ client = messaging_client(get_config())
82
+ echo_json(
83
+ asyncio.run(
84
+ client.move_channel(
85
+ channel=channel,
86
+ section=section,
87
+ before=before,
88
+ after=after,
89
+ )
90
+ )
91
+ )
92
+
93
+
94
+ @sections_app.command("list", help="List channel sections")
95
+ def list_sections_command() -> None:
96
+ client = messaging_client(get_config())
97
+ echo_jsonl(asyncio.run(client.list_sections()))
98
+
99
+
100
+ @sections_app.command("create", help="Create a channel section")
101
+ def create_section_command(
102
+ name: Annotated[str, typer.Option(help="Section name")],
103
+ ) -> None:
104
+ client = messaging_client(get_config())
105
+ echo_json(asyncio.run(client.create_section(name=name)))
106
+
107
+
108
+ @sections_app.command("rename", help="Rename a channel section")
109
+ def rename_section_command(
110
+ section: Annotated[str, typer.Option(help="Section name or id")],
111
+ name: Annotated[str, typer.Option(help="New section name")],
112
+ ) -> None:
113
+ client = messaging_client(get_config())
114
+ echo_json(asyncio.run(client.rename_section(section=section, name=name)))
115
+
116
+
117
+ @sections_app.command("move", help="Move a channel section")
118
+ def move_section_command(
119
+ section: Annotated[str, typer.Option(help="Section name or id")],
120
+ before: Annotated[
121
+ str | None,
122
+ typer.Option(help="Place before this section name or id"),
123
+ ] = None,
124
+ after: Annotated[
125
+ str | None,
126
+ typer.Option(help="Place after this section name or id"),
127
+ ] = None,
128
+ ) -> None:
129
+ client = messaging_client(get_config())
130
+ echo_json(
131
+ asyncio.run(
132
+ client.move_section(
133
+ section=section,
134
+ before=before,
135
+ after=after,
136
+ )
137
+ )
138
+ )
139
+
140
+
141
+ app.add_typer(channels_app, name="channels")
142
+ app.add_typer(sections_app, name="sections")
@@ -0,0 +1,87 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import typer
6
+
7
+ from keep_up_with.cli.agent.output import echo_jsonl
8
+ from keep_up_with.core.config import get_config
9
+ from keep_up_with.integrations.base import DataIntegration, Subscription
10
+ from keep_up_with.integrations.registry import (
11
+ available_data_integrations,
12
+ missing_env,
13
+ )
14
+
15
+ app = typer.Typer(
16
+ add_completion=False,
17
+ invoke_without_command=True,
18
+ help="List enabled subscriptions",
19
+ no_args_is_help=True,
20
+ )
21
+
22
+
23
+ @app.command("list", help="Print enabled subscriptions as JSONL")
24
+ def list_command() -> None:
25
+ echo_jsonl(subscription_rows())
26
+
27
+
28
+ def subscription_rows() -> list[dict[str, Any]]:
29
+ config = get_config()
30
+ rows: list[dict[str, Any]] = []
31
+ for integration in sorted(available_data_integrations(), key=lambda item: item.name):
32
+ settings = config.integration(integration.name)
33
+ enabled = config.integration_enabled(integration.name)
34
+ if not enabled:
35
+ continue
36
+ missing = list(missing_env(config, integration))
37
+ rows.extend(
38
+ subscription_row(
39
+ integration=integration.name,
40
+ kind="data",
41
+ enabled=enabled,
42
+ settings=settings,
43
+ missing=missing,
44
+ subscription=subscription,
45
+ watches=watches(integration, settings),
46
+ )
47
+ for subscription in integration.subscriptions
48
+ )
49
+ return rows
50
+
51
+
52
+ def subscription_row(
53
+ *,
54
+ integration: str,
55
+ kind: str,
56
+ enabled: bool,
57
+ settings: dict[str, Any],
58
+ missing: list[str],
59
+ subscription: Subscription,
60
+ watches: dict[str, Any],
61
+ ) -> dict[str, Any]:
62
+ return {
63
+ "integration": integration,
64
+ "kind": kind,
65
+ "subscription": subscription.name,
66
+ "enabled": enabled,
67
+ "runnable": enabled and not missing,
68
+ "interval_seconds": interval_seconds(subscription, settings),
69
+ "missing_env": missing,
70
+ "watches": watches,
71
+ }
72
+
73
+
74
+ def interval_seconds(
75
+ subscription: Subscription,
76
+ settings: dict[str, Any],
77
+ ) -> float | None:
78
+ if subscription.default_interval_seconds is None:
79
+ return None
80
+ return float(settings.get("interval_seconds") or subscription.default_interval_seconds)
81
+
82
+
83
+ def watches(integration: DataIntegration, settings: dict[str, Any]) -> dict[str, Any]:
84
+ return {
85
+ parameter.name: settings.get(parameter.name) or []
86
+ for parameter in integration.parameters
87
+ }