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.
- flyteplugins/clickup/__init__.py +31 -0
- flyteplugins/clickup/_provider.py +75 -0
- flyteplugins/clickup/events.py +65 -0
- flyteplugins_clickup-2.7.0.dist-info/METADATA +91 -0
- flyteplugins_clickup-2.7.0.dist-info/RECORD +7 -0
- flyteplugins_clickup-2.7.0.dist-info/WHEEL +5 -0
- flyteplugins_clickup-2.7.0.dist-info/top_level.txt +1 -0
|
@@ -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 @@
|
|
|
1
|
+
flyteplugins
|