cortexdb-connectors 0.2.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.
- cortexdb_connectors/__init__.py +40 -0
- cortexdb_connectors/base.py +364 -0
- cortexdb_connectors/cli.py +476 -0
- cortexdb_connectors/confluence/__init__.py +359 -0
- cortexdb_connectors/discord/__init__.py +646 -0
- cortexdb_connectors/github/__init__.py +309 -0
- cortexdb_connectors/gitlab/__init__.py +749 -0
- cortexdb_connectors/google_workspace/__init__.py +1033 -0
- cortexdb_connectors/hubspot/__init__.py +489 -0
- cortexdb_connectors/insights/__init__.py +40 -0
- cortexdb_connectors/insights/api.py +252 -0
- cortexdb_connectors/insights/detectors.py +771 -0
- cortexdb_connectors/insights/engine.py +308 -0
- cortexdb_connectors/intercom/__init__.py +631 -0
- cortexdb_connectors/jira/__init__.py +298 -0
- cortexdb_connectors/linear/__init__.py +705 -0
- cortexdb_connectors/notion/__init__.py +702 -0
- cortexdb_connectors/pagerduty/__init__.py +298 -0
- cortexdb_connectors/salesforce/__init__.py +399 -0
- cortexdb_connectors/servicenow/__init__.py +460 -0
- cortexdb_connectors/slack/__init__.py +331 -0
- cortexdb_connectors/state.py +145 -0
- cortexdb_connectors/teams/__init__.py +656 -0
- cortexdb_connectors/webhooks.py +672 -0
- cortexdb_connectors/worker.py +192 -0
- cortexdb_connectors/zendesk/__init__.py +496 -0
- cortexdb_connectors-0.2.0.dist-info/METADATA +207 -0
- cortexdb_connectors-0.2.0.dist-info/RECORD +31 -0
- cortexdb_connectors-0.2.0.dist-info/WHEEL +5 -0
- cortexdb_connectors-0.2.0.dist-info/entry_points.txt +2 -0
- cortexdb_connectors-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GitHub connector for CortexDB.
|
|
3
|
+
|
|
4
|
+
Polls GitHub repositories for events (pushes, PRs, issues, comments,
|
|
5
|
+
reviews) and ingests them as CortexDB Episodes via the Connector SDK.
|
|
6
|
+
Uses the ``PyGithub`` library for API access.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from cortexdb_connectors.base import (
|
|
16
|
+
Actor,
|
|
17
|
+
CortexConnector,
|
|
18
|
+
EntityRef,
|
|
19
|
+
Episode,
|
|
20
|
+
EpisodeType,
|
|
21
|
+
RawEvent,
|
|
22
|
+
Source,
|
|
23
|
+
Visibility,
|
|
24
|
+
VisibilityLevel,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
_SOURCE = Source(system="github")
|
|
30
|
+
|
|
31
|
+
_DEFAULT_EVENT_TYPES = frozenset(
|
|
32
|
+
[
|
|
33
|
+
"push",
|
|
34
|
+
"pull_request",
|
|
35
|
+
"issues",
|
|
36
|
+
"issue_comment",
|
|
37
|
+
"pull_request_review",
|
|
38
|
+
]
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Map GitHub event type strings to CortexDB episode types.
|
|
42
|
+
_EVENT_TYPE_MAP: dict[str, EpisodeType] = {
|
|
43
|
+
"PushEvent": EpisodeType.CODE_CHANGE,
|
|
44
|
+
"PullRequestEvent": EpisodeType.CODE_CHANGE,
|
|
45
|
+
"IssuesEvent": EpisodeType.ISSUE,
|
|
46
|
+
"IssueCommentEvent": EpisodeType.COMMENT,
|
|
47
|
+
"PullRequestReviewEvent": EpisodeType.REVIEW,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class GitHubConnector(CortexConnector):
|
|
52
|
+
"""Connector that syncs GitHub repository events into CortexDB.
|
|
53
|
+
|
|
54
|
+
Parameters
|
|
55
|
+
----------
|
|
56
|
+
cortex_url:
|
|
57
|
+
Base URL of the CortexDB API.
|
|
58
|
+
cortex_api_key:
|
|
59
|
+
Bearer token for CortexDB authentication.
|
|
60
|
+
github_token:
|
|
61
|
+
GitHub personal access token or GitHub App installation token.
|
|
62
|
+
repos:
|
|
63
|
+
List of repository full names (``owner/repo``) to sync.
|
|
64
|
+
events:
|
|
65
|
+
GitHub event types to capture. Defaults to push, PR, issues,
|
|
66
|
+
issue_comment, and pull_request_review.
|
|
67
|
+
tenant_id:
|
|
68
|
+
CortexDB tenant identifier.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
connector_name: str = "github"
|
|
72
|
+
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
cortex_url: str,
|
|
76
|
+
cortex_api_key: str,
|
|
77
|
+
github_token: str,
|
|
78
|
+
repos: list[str] | None = None,
|
|
79
|
+
events: list[str] | None = None,
|
|
80
|
+
tenant_id: str = "default",
|
|
81
|
+
) -> None:
|
|
82
|
+
super().__init__(cortex_url, cortex_api_key, tenant_id)
|
|
83
|
+
self.github_token = github_token
|
|
84
|
+
self.repos = repos or []
|
|
85
|
+
self.event_filter = set(events) if events else set(_DEFAULT_EVENT_TYPES)
|
|
86
|
+
|
|
87
|
+
# -- CortexConnector interface -------------------------------------------
|
|
88
|
+
|
|
89
|
+
async def fetch_events(
|
|
90
|
+
self, since: datetime | None = None
|
|
91
|
+
) -> list[RawEvent]:
|
|
92
|
+
"""Fetch repository events from GitHub since *since*.
|
|
93
|
+
|
|
94
|
+
Uses the repository events API endpoint which returns up to 300
|
|
95
|
+
events (paginated, 30 per page, max 10 pages).
|
|
96
|
+
"""
|
|
97
|
+
from github import Github
|
|
98
|
+
|
|
99
|
+
gh = Github(self.github_token)
|
|
100
|
+
raw_events: list[RawEvent] = []
|
|
101
|
+
|
|
102
|
+
for repo_name in self.repos:
|
|
103
|
+
try:
|
|
104
|
+
repo = gh.get_repo(repo_name)
|
|
105
|
+
except Exception:
|
|
106
|
+
logger.error("Could not access repository %s", repo_name)
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
for event in repo.get_events():
|
|
110
|
+
created_at = event.created_at.replace(tzinfo=timezone.utc)
|
|
111
|
+
|
|
112
|
+
if since and created_at <= since:
|
|
113
|
+
# Events are returned newest-first; once we pass the
|
|
114
|
+
# cursor we can stop for this repo.
|
|
115
|
+
break
|
|
116
|
+
|
|
117
|
+
if not self._matches_filter(event.type):
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
raw = RawEvent(
|
|
121
|
+
source="github",
|
|
122
|
+
external_id=str(event.id),
|
|
123
|
+
timestamp=created_at,
|
|
124
|
+
data={
|
|
125
|
+
"type": event.type,
|
|
126
|
+
"actor": {
|
|
127
|
+
"login": event.actor.login if event.actor else "unknown",
|
|
128
|
+
"id": event.actor.id if event.actor else 0,
|
|
129
|
+
"avatar_url": (
|
|
130
|
+
event.actor.avatar_url if event.actor else None
|
|
131
|
+
),
|
|
132
|
+
},
|
|
133
|
+
"repo": repo_name,
|
|
134
|
+
"payload": event.payload,
|
|
135
|
+
"public": not repo.private,
|
|
136
|
+
},
|
|
137
|
+
permissions={"private": repo.private},
|
|
138
|
+
)
|
|
139
|
+
raw_events.append(raw)
|
|
140
|
+
|
|
141
|
+
return raw_events
|
|
142
|
+
|
|
143
|
+
def normalize(self, raw: RawEvent) -> Episode:
|
|
144
|
+
"""Convert a GitHub event ``RawEvent`` into a CortexDB ``Episode``."""
|
|
145
|
+
data = raw.data
|
|
146
|
+
payload = data.get("payload", {})
|
|
147
|
+
event_type_str = data.get("type", "")
|
|
148
|
+
episode_type = _EVENT_TYPE_MAP.get(event_type_str, EpisodeType.CUSTOM)
|
|
149
|
+
|
|
150
|
+
actor_data = data.get("actor", {})
|
|
151
|
+
actor = Actor(
|
|
152
|
+
id=str(actor_data.get("id", "")),
|
|
153
|
+
name=actor_data.get("login", "unknown"),
|
|
154
|
+
avatar_url=actor_data.get("avatar_url"),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
content = self._build_content(event_type_str, payload)
|
|
158
|
+
entities = self._extract_entities(data, payload)
|
|
159
|
+
tags = ["github", event_type_str.lower()]
|
|
160
|
+
|
|
161
|
+
metadata: dict[str, Any] = {"github_event_type": event_type_str}
|
|
162
|
+
if "action" in payload:
|
|
163
|
+
metadata["action"] = payload["action"]
|
|
164
|
+
|
|
165
|
+
# Thread grouping: PR events share the PR number as thread_id.
|
|
166
|
+
thread_id = self._derive_thread_id(event_type_str, payload, data)
|
|
167
|
+
|
|
168
|
+
return Episode(
|
|
169
|
+
actor=actor,
|
|
170
|
+
source=_SOURCE,
|
|
171
|
+
episode_type=episode_type,
|
|
172
|
+
occurred_at=raw.timestamp,
|
|
173
|
+
content=content,
|
|
174
|
+
raw_payload=data,
|
|
175
|
+
tenant_id=self.tenant_id,
|
|
176
|
+
namespace="github",
|
|
177
|
+
entities=entities,
|
|
178
|
+
tags=tags,
|
|
179
|
+
metadata=metadata,
|
|
180
|
+
thread_id=thread_id,
|
|
181
|
+
idempotency_key=raw.external_id,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
def map_permissions(self, raw: RawEvent) -> Visibility:
|
|
185
|
+
"""Map GitHub repository visibility to CortexDB visibility.
|
|
186
|
+
|
|
187
|
+
Public repositories yield ``Organization`` visibility; private
|
|
188
|
+
repositories yield ``Restricted``.
|
|
189
|
+
"""
|
|
190
|
+
if raw.permissions.get("private"):
|
|
191
|
+
return Visibility(level=VisibilityLevel.RESTRICTED)
|
|
192
|
+
return Visibility(level=VisibilityLevel.ORGANIZATION)
|
|
193
|
+
|
|
194
|
+
# -- helpers -------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
def _matches_filter(self, event_type: str) -> bool:
|
|
197
|
+
"""Check whether *event_type* matches the configured filter set."""
|
|
198
|
+
mapping = {
|
|
199
|
+
"PushEvent": "push",
|
|
200
|
+
"PullRequestEvent": "pull_request",
|
|
201
|
+
"IssuesEvent": "issues",
|
|
202
|
+
"IssueCommentEvent": "issue_comment",
|
|
203
|
+
"PullRequestReviewEvent": "pull_request_review",
|
|
204
|
+
}
|
|
205
|
+
short = mapping.get(event_type, event_type)
|
|
206
|
+
return short in self.event_filter
|
|
207
|
+
|
|
208
|
+
@staticmethod
|
|
209
|
+
def _build_content(event_type: str, payload: dict[str, Any]) -> str:
|
|
210
|
+
"""Produce a human-readable summary of the event."""
|
|
211
|
+
if event_type == "PushEvent":
|
|
212
|
+
commits = payload.get("commits", [])
|
|
213
|
+
count = len(commits)
|
|
214
|
+
ref = payload.get("ref", "")
|
|
215
|
+
messages = "; ".join(
|
|
216
|
+
c.get("message", "").split("\n")[0] for c in commits[:5]
|
|
217
|
+
)
|
|
218
|
+
return f"Pushed {count} commit(s) to {ref}: {messages}"
|
|
219
|
+
|
|
220
|
+
if event_type == "PullRequestEvent":
|
|
221
|
+
pr = payload.get("pull_request", {})
|
|
222
|
+
action = payload.get("action", "")
|
|
223
|
+
title = pr.get("title", "")
|
|
224
|
+
return f"PR {action}: {title}"
|
|
225
|
+
|
|
226
|
+
if event_type == "IssuesEvent":
|
|
227
|
+
issue = payload.get("issue", {})
|
|
228
|
+
action = payload.get("action", "")
|
|
229
|
+
title = issue.get("title", "")
|
|
230
|
+
return f"Issue {action}: {title}"
|
|
231
|
+
|
|
232
|
+
if event_type == "IssueCommentEvent":
|
|
233
|
+
comment = payload.get("comment", {})
|
|
234
|
+
body = (comment.get("body") or "")[:200]
|
|
235
|
+
return f"Comment: {body}"
|
|
236
|
+
|
|
237
|
+
if event_type == "PullRequestReviewEvent":
|
|
238
|
+
review = payload.get("review", {})
|
|
239
|
+
state = review.get("state", "")
|
|
240
|
+
pr = payload.get("pull_request", {})
|
|
241
|
+
title = pr.get("title", "")
|
|
242
|
+
return f"Review ({state}) on PR: {title}"
|
|
243
|
+
|
|
244
|
+
return f"GitHub event: {event_type}"
|
|
245
|
+
|
|
246
|
+
@staticmethod
|
|
247
|
+
def _extract_entities(
|
|
248
|
+
data: dict[str, Any], payload: dict[str, Any]
|
|
249
|
+
) -> list[EntityRef]:
|
|
250
|
+
"""Extract entity references from the event."""
|
|
251
|
+
entities: list[EntityRef] = []
|
|
252
|
+
|
|
253
|
+
repo_name = data.get("repo", "")
|
|
254
|
+
if repo_name:
|
|
255
|
+
entities.append(
|
|
256
|
+
EntityRef(
|
|
257
|
+
entity_type="repository",
|
|
258
|
+
entity_id=repo_name,
|
|
259
|
+
display_name=repo_name,
|
|
260
|
+
)
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
# PR author and reviewers.
|
|
264
|
+
pr = payload.get("pull_request", {})
|
|
265
|
+
if pr:
|
|
266
|
+
pr_user = pr.get("user", {})
|
|
267
|
+
if pr_user:
|
|
268
|
+
entities.append(
|
|
269
|
+
EntityRef(
|
|
270
|
+
entity_type="user",
|
|
271
|
+
entity_id=str(pr_user.get("id", "")),
|
|
272
|
+
display_name=pr_user.get("login", ""),
|
|
273
|
+
)
|
|
274
|
+
)
|
|
275
|
+
for reviewer in pr.get("requested_reviewers", []):
|
|
276
|
+
entities.append(
|
|
277
|
+
EntityRef(
|
|
278
|
+
entity_type="user",
|
|
279
|
+
entity_id=str(reviewer.get("id", "")),
|
|
280
|
+
display_name=reviewer.get("login", ""),
|
|
281
|
+
)
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
# Issue references.
|
|
285
|
+
issue = payload.get("issue", {})
|
|
286
|
+
if issue:
|
|
287
|
+
entities.append(
|
|
288
|
+
EntityRef(
|
|
289
|
+
entity_type="issue",
|
|
290
|
+
entity_id=str(issue.get("number", "")),
|
|
291
|
+
display_name=issue.get("title", ""),
|
|
292
|
+
)
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
return entities
|
|
296
|
+
|
|
297
|
+
@staticmethod
|
|
298
|
+
def _derive_thread_id(
|
|
299
|
+
event_type: str, payload: dict[str, Any], data: dict[str, Any]
|
|
300
|
+
) -> str | None:
|
|
301
|
+
"""Derive a thread_id for grouping related events."""
|
|
302
|
+
repo = data.get("repo", "")
|
|
303
|
+
pr = payload.get("pull_request", {})
|
|
304
|
+
if pr and pr.get("number"):
|
|
305
|
+
return f"{repo}:pr:{pr['number']}"
|
|
306
|
+
issue = payload.get("issue", {})
|
|
307
|
+
if issue and issue.get("number"):
|
|
308
|
+
return f"{repo}:issue:{issue['number']}"
|
|
309
|
+
return None
|