flyteplugins-clickup 2.7.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.
@@ -0,0 +1,31 @@
1
+ """ClickUp webhooks for Flyte.
2
+
3
+ Hand a `ClickUpProvider()` to a `WebhookAppEnvironment` and register handlers with the
4
+ typed constants in `events`. Calling the ClickUp API is not this plugin's job —
5
+ it is a handful of REST calls, so use `httpx` from your tasks. See
6
+ `examples/external_saas_integrations`.
7
+ """
8
+
9
+ import hashlib
10
+ import hmac
11
+
12
+ from . import events
13
+ from ._provider import ClickUpProvider, parse, verify
14
+
15
+ __all__ = ["SAMPLE_DELIVERY", "ClickUpProvider", "events", "parse", "verify"]
16
+
17
+
18
+ def _sample_headers(body: bytes, secret: str) -> dict[str, str]:
19
+ return {"X-Clickup-Signature": hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()}
20
+
21
+
22
+ #: A real `taskCreated` delivery, trimmed to the fields the parser reads.
23
+ SAMPLE_DELIVERY = (
24
+ _sample_headers,
25
+ (
26
+ b'{"event": "taskCreated", "task_id": "abc123", "list_id": "9000",'
27
+ b' "webhook_id": "wh-000", "timestamp": 1700000000000,'
28
+ b' "task": {"id": "abc123", "name": "Fix the thing",'
29
+ b' "url": "https://app.clickup.com/t/abc123"}}'
30
+ ),
31
+ )
@@ -0,0 +1,75 @@
1
+ """ClickUp webhook verification and payload normalization."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import ClassVar, Mapping
6
+
7
+ from flyte.extras.webhooks import (
8
+ Provider,
9
+ WebhookEvent,
10
+ constant_time_equals,
11
+ hex_hmac_sha256,
12
+ json_body,
13
+ lower_headers,
14
+ )
15
+
16
+
17
+ def verify(body: bytes, headers: Mapping[str, str], secret: str) -> bool:
18
+ """Verify the `X-Clickup-Signature` HMAC over the raw body."""
19
+ signature = lower_headers(headers).get("x-clickup-signature")
20
+ if not signature:
21
+ return False
22
+ return constant_time_equals(hex_hmac_sha256(secret, body), signature.strip())
23
+
24
+
25
+ def parse(headers: Mapping[str, str], body: bytes) -> WebhookEvent:
26
+ """Normalize a ClickUp delivery into a `WebhookEvent`."""
27
+ payload = json_body(body)
28
+ task = payload.get("task") or {}
29
+ # ClickUp puts the list id at the top level on list-scoped events and only on
30
+ # the nested task for task-scoped ones; read both or a `scopes` allowlist
31
+ # cannot attribute task events.
32
+ list_id = payload.get("list_id") or (task.get("list") or {}).get("id")
33
+ task_id = payload.get("task_id") or task.get("id")
34
+ return WebhookEvent(
35
+ provider="clickup",
36
+ event_type=payload.get("event", "unknown"),
37
+ delivery_id=str(payload.get("webhook_id") or ""),
38
+ resource_id=str(task_id) if task_id is not None else None,
39
+ occurred_at=str(payload.get("timestamp")) if payload.get("timestamp") is not None else None,
40
+ scope=str(list_id) if list_id is not None else None,
41
+ title=task.get("name"),
42
+ url=task.get("url"),
43
+ payload=payload,
44
+ )
45
+
46
+
47
+ class ClickUpProvider(Provider):
48
+ """ClickUp's webhook provider, with its defaults pre-wired.
49
+
50
+ ```python
51
+ from flyte.extras.webhooks import WebhookAppEnvironment
52
+ from flyteplugins.clickup import ClickUpProvider
53
+
54
+ app_env = WebhookAppEnvironment(name="webhooks", providers=[ClickUpProvider()])
55
+ ```
56
+
57
+ `WebhookAppEnvironment` mounts `default_secret_env` for you, so it does not
58
+ need naming again in `secrets=`.
59
+
60
+ Args:
61
+ secret_env: Environment variable holding the secret. Pass one only to
62
+ point this provider at a secret stored under a different name;
63
+ otherwise `default_secret_env` applies.
64
+ """
65
+
66
+ default_secret_env: ClassVar[str] = "CLICKUP_WEBHOOK_SECRET"
67
+
68
+ def __init__(self, *, secret_env: str | None = None) -> None:
69
+ super().__init__(
70
+ name="clickup",
71
+ secret_env=secret_env or self.default_secret_env,
72
+ verify=verify,
73
+ parse=parse,
74
+ setup_hint="Space Settings -> Integrations -> Webhooks",
75
+ )
@@ -0,0 +1,65 @@
1
+ """ClickUp webhook events. Names are flat — ClickUp sends no separate action."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from flyte.extras.webhooks import EventType
6
+
7
+ __all__ = ["Folder", "Goal", "KeyResult", "List", "Space", "Task"]
8
+
9
+
10
+ class Task(EventType):
11
+ """Task events."""
12
+
13
+ CREATED = "taskCreated"
14
+ UPDATED = "taskUpdated"
15
+ DELETED = "taskDeleted"
16
+ PRIORITY_UPDATED = "taskPriorityUpdated"
17
+ STATUS_UPDATED = "taskStatusUpdated"
18
+ ASSIGNEE_UPDATED = "taskAssigneeUpdated"
19
+ DUE_DATE_UPDATED = "taskDueDateUpdated"
20
+ TAG_UPDATED = "taskTagUpdated"
21
+ MOVED = "taskMoved"
22
+ COMMENT_POSTED = "taskCommentPosted"
23
+ COMMENT_UPDATED = "taskCommentUpdated"
24
+ TIME_ESTIMATE_UPDATED = "taskTimeEstimateUpdated"
25
+ TIME_TRACKED_UPDATED = "taskTimeTrackedUpdated"
26
+
27
+
28
+ class List(EventType):
29
+ """List events."""
30
+
31
+ CREATED = "listCreated"
32
+ UPDATED = "listUpdated"
33
+ DELETED = "listDeleted"
34
+
35
+
36
+ class Folder(EventType):
37
+ """Folder events."""
38
+
39
+ CREATED = "folderCreated"
40
+ UPDATED = "folderUpdated"
41
+ DELETED = "folderDeleted"
42
+
43
+
44
+ class Space(EventType):
45
+ """Space events."""
46
+
47
+ CREATED = "spaceCreated"
48
+ UPDATED = "spaceUpdated"
49
+ DELETED = "spaceDeleted"
50
+
51
+
52
+ class Goal(EventType):
53
+ """Goal events."""
54
+
55
+ CREATED = "goalCreated"
56
+ UPDATED = "goalUpdated"
57
+ DELETED = "goalDeleted"
58
+
59
+
60
+ class KeyResult(EventType):
61
+ """Key-result (goal target) events."""
62
+
63
+ CREATED = "keyResultCreated"
64
+ UPDATED = "keyResultUpdated"
65
+ DELETED = "keyResultDeleted"
@@ -0,0 +1,91 @@
1
+ Metadata-Version: 2.4
2
+ Name: flyteplugins-clickup
3
+ Version: 2.7.0
4
+ Summary: Receive ClickUp webhooks in Flyte.
5
+ Author: Flyte Contributors
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: flyte
9
+ Provides-Extra: app
10
+ Requires-Dist: fastapi>=0.115; extra == "app"
11
+ Requires-Dist: uvicorn>=0.30; extra == "app"
12
+
13
+ # flyteplugins-clickup
14
+
15
+ Receive ClickUp webhooks in Flyte.
16
+
17
+ ```bash
18
+ pip install "flyteplugins-clickup[app]"
19
+ ```
20
+
21
+ ## Using it
22
+
23
+ Hand a `ClickUpProvider()` to a `WebhookAppEnvironment` and register handlers with the
24
+ typed constants in `events`:
25
+
26
+ ```python
27
+ import flyte
28
+ from flyte.extras.webhooks import WebhookAppEnvironment, run_once
29
+ from flyteplugins.clickup import ClickUpProvider, events
30
+
31
+ # ClickUpProvider.default_secret_env is mounted for you.
32
+ app_env = WebhookAppEnvironment(name="clickup-webhooks", providers=[ClickUpProvider()])
33
+
34
+
35
+ @app_env.on_event(events.Task.STATUS_UPDATED)
36
+ async def handle(event):
37
+ import flyte.remote as remote
38
+
39
+ task = remote.Task.get(name="my-env.my_task", auto_version="latest")
40
+ result = await run_once.aio(task, key=event.dedupe_key(), resource=event.resource_id)
41
+ if not result.created:
42
+ return {"skipped": result.run.name, "url": result.run.url}
43
+ return {"run": result.run.name}
44
+
45
+
46
+ flyte.serve(app_env)
47
+ ```
48
+
49
+ Handlers must `await run_once.aio(...)`. The blocking form stalls the
50
+ app's event loop, and ClickUp times deliveries out in seconds.
51
+
52
+ One app can serve several products at once — hand it one provider per product.
53
+
54
+ ## Try it
55
+
56
+ `examples/clickup_webhooks.py` runs two ways. The first needs no ClickUp account:
57
+
58
+ ```bash
59
+ python examples/clickup_webhooks.py --local # replay a real sample delivery in-process
60
+ python examples/clickup_webhooks.py # deploy the receiver to Flyte
61
+ ```
62
+
63
+ `--local` posts this plugin's `SAMPLE_DELIVERY` through the app with FastAPI's
64
+ test client, so you see a delivery verified, normalized, and dispatched — plus
65
+ an unsigned one refused with a 401, and the same delivery replayed to show the
66
+ dedupe key is stable.
67
+
68
+ ## Setup
69
+
70
+ 1. Store the secret and mount it on the app:
71
+ ```bash
72
+ flyte create secret CLICKUP_WEBHOOK_SECRET --value <secret>
73
+ ```
74
+ 2. Point ClickUp at `<app-url>/webhook/clickup`, from
75
+ Space Settings → Integrations → Webhooks (it shows the signing secret on creation).
76
+
77
+ **Verification:** HMAC-SHA256 over the raw body (`X-Clickup-Signature`).
78
+
79
+ The list id is at the top level on list-scoped events and on the nested task for task-scoped ones; the parser reads both.
80
+
81
+ ## Event constants
82
+
83
+ `events` spells every event this plugin can dispatch, as `str` enums grouped by
84
+ event type, so a typo fails at import rather than by silently never matching.
85
+ Raw strings still work, for events the constants do not cover yet.
86
+
87
+ ## What this plugin does not do
88
+
89
+ Call the ClickUp API. Use `httpx` — ClickUp ships no Python SDK, and its API is a handful of REST calls directly from your tasks — see
90
+ `examples/external_saas_integrations`. This plugin owns only the part that is
91
+ Flyte's: authenticating an inbound delivery and turning it into a run.
@@ -0,0 +1,7 @@
1
+ flyteplugins/clickup/__init__.py,sha256=bA33dd1hfWj9iawbYLEmIMg3A9jFj_niJPOxLcNca8Q,1047
2
+ flyteplugins/clickup/_provider.py,sha256=nBQ0ngMhCsVbJHpTjP_ZFNXELI9WJ_BSqjSfIVFuqDU,2673
3
+ flyteplugins/clickup/events.py,sha256=9u_zfdkxdRV5-aZbvpn97TKK2YQe7wcNcfVwbGi5IvU,1509
4
+ flyteplugins_clickup-2.7.0.dist-info/METADATA,sha256=pWY0QRsMyRH6W-XTFueCR1h-5IfLpBWwM7RvP9_1l0Y,3093
5
+ flyteplugins_clickup-2.7.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
6
+ flyteplugins_clickup-2.7.0.dist-info/top_level.txt,sha256=cgd779rPu9EsvdtuYgUxNHHgElaQvPn74KhB5XSeMBE,13
7
+ flyteplugins_clickup-2.7.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ flyteplugins