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,298 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Jira connector for CortexDB.
|
|
3
|
+
|
|
4
|
+
Polls Jira projects for recently updated issues using JQL and ingests
|
|
5
|
+
them as CortexDB Episodes. Uses the ``jira`` library for API access.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from cortexdb_connectors.base import (
|
|
15
|
+
Actor,
|
|
16
|
+
CortexConnector,
|
|
17
|
+
EntityRef,
|
|
18
|
+
Episode,
|
|
19
|
+
EpisodeType,
|
|
20
|
+
RawEvent,
|
|
21
|
+
Source,
|
|
22
|
+
Visibility,
|
|
23
|
+
VisibilityLevel,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
_SOURCE = Source(system="jira")
|
|
29
|
+
|
|
30
|
+
_PAGE_SIZE = 50
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class JiraConnector(CortexConnector):
|
|
34
|
+
"""Connector that syncs Jira issues into CortexDB.
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
cortex_url:
|
|
39
|
+
Base URL of the CortexDB API.
|
|
40
|
+
cortex_api_key:
|
|
41
|
+
Bearer token for CortexDB authentication.
|
|
42
|
+
jira_url:
|
|
43
|
+
Base URL of the Jira instance (e.g. ``https://myteam.atlassian.net``).
|
|
44
|
+
jira_email:
|
|
45
|
+
Email address for Jira authentication.
|
|
46
|
+
jira_api_token:
|
|
47
|
+
Jira API token (Atlassian account token).
|
|
48
|
+
project_keys:
|
|
49
|
+
List of Jira project keys to sync (e.g. ``["ENG", "INFRA"]``).
|
|
50
|
+
tenant_id:
|
|
51
|
+
CortexDB tenant identifier.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
connector_name: str = "jira"
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
cortex_url: str,
|
|
59
|
+
cortex_api_key: str,
|
|
60
|
+
jira_url: str,
|
|
61
|
+
jira_email: str,
|
|
62
|
+
jira_api_token: str,
|
|
63
|
+
project_keys: list[str] | None = None,
|
|
64
|
+
tenant_id: str = "default",
|
|
65
|
+
) -> None:
|
|
66
|
+
super().__init__(cortex_url, cortex_api_key, tenant_id)
|
|
67
|
+
self.jira_url = jira_url.rstrip("/")
|
|
68
|
+
self.jira_email = jira_email
|
|
69
|
+
self.jira_api_token = jira_api_token
|
|
70
|
+
self.project_keys = project_keys or []
|
|
71
|
+
|
|
72
|
+
# -- CortexConnector interface -------------------------------------------
|
|
73
|
+
|
|
74
|
+
async def fetch_events(
|
|
75
|
+
self, since: datetime | None = None
|
|
76
|
+
) -> list[RawEvent]:
|
|
77
|
+
"""Fetch issues updated since *since* from configured Jira projects.
|
|
78
|
+
|
|
79
|
+
Uses JQL ``project in (...) AND updated >= "..."`` with pagination.
|
|
80
|
+
"""
|
|
81
|
+
from jira import JIRA
|
|
82
|
+
|
|
83
|
+
client = JIRA(
|
|
84
|
+
server=self.jira_url,
|
|
85
|
+
basic_auth=(self.jira_email, self.jira_api_token),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
jql_parts: list[str] = []
|
|
89
|
+
if self.project_keys:
|
|
90
|
+
keys_str = ", ".join(self.project_keys)
|
|
91
|
+
jql_parts.append(f"project in ({keys_str})")
|
|
92
|
+
if since:
|
|
93
|
+
jql_date = since.strftime("%Y-%m-%d %H:%M")
|
|
94
|
+
jql_parts.append(f'updated >= "{jql_date}"')
|
|
95
|
+
|
|
96
|
+
jql = " AND ".join(jql_parts) if jql_parts else "ORDER BY updated DESC"
|
|
97
|
+
if jql_parts:
|
|
98
|
+
jql += " ORDER BY updated DESC"
|
|
99
|
+
|
|
100
|
+
raw_events: list[RawEvent] = []
|
|
101
|
+
start_at = 0
|
|
102
|
+
|
|
103
|
+
while True:
|
|
104
|
+
results = client.search_issues(
|
|
105
|
+
jql,
|
|
106
|
+
startAt=start_at,
|
|
107
|
+
maxResults=_PAGE_SIZE,
|
|
108
|
+
expand="changelog",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
for issue in results:
|
|
112
|
+
fields = issue.fields
|
|
113
|
+
updated_str = getattr(fields, "updated", None) or ""
|
|
114
|
+
try:
|
|
115
|
+
updated_at = datetime.fromisoformat(
|
|
116
|
+
updated_str.replace("Z", "+00:00")
|
|
117
|
+
)
|
|
118
|
+
except (ValueError, TypeError):
|
|
119
|
+
updated_at = datetime.now(timezone.utc)
|
|
120
|
+
|
|
121
|
+
issue_data = self._serialize_issue(issue)
|
|
122
|
+
project_data = issue_data.get("project", {})
|
|
123
|
+
|
|
124
|
+
raw_events.append(
|
|
125
|
+
RawEvent(
|
|
126
|
+
source="jira",
|
|
127
|
+
external_id=issue.key,
|
|
128
|
+
timestamp=updated_at,
|
|
129
|
+
data=issue_data,
|
|
130
|
+
permissions={
|
|
131
|
+
"project_key": project_data.get("key", ""),
|
|
132
|
+
"project_type": project_data.get(
|
|
133
|
+
"projectTypeKey", "software"
|
|
134
|
+
),
|
|
135
|
+
},
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if start_at + _PAGE_SIZE >= results.total:
|
|
140
|
+
break
|
|
141
|
+
start_at += _PAGE_SIZE
|
|
142
|
+
|
|
143
|
+
return raw_events
|
|
144
|
+
|
|
145
|
+
def normalize(self, raw: RawEvent) -> Episode:
|
|
146
|
+
"""Convert a Jira issue ``RawEvent`` into a CortexDB ``Episode``."""
|
|
147
|
+
data = raw.data
|
|
148
|
+
actor = self._extract_actor(data)
|
|
149
|
+
entities = self._extract_entities(data)
|
|
150
|
+
|
|
151
|
+
status = data.get("status", "Unknown")
|
|
152
|
+
priority = data.get("priority", "Medium")
|
|
153
|
+
summary = data.get("summary", "Untitled")
|
|
154
|
+
issue_key = data.get("key", raw.external_id)
|
|
155
|
+
issue_type = data.get("issuetype", "Task")
|
|
156
|
+
description = data.get("description", "") or ""
|
|
157
|
+
|
|
158
|
+
content = f"[{issue_key}] {summary}"
|
|
159
|
+
if description:
|
|
160
|
+
content += f"\n\n{description[:500]}"
|
|
161
|
+
|
|
162
|
+
return Episode(
|
|
163
|
+
actor=actor,
|
|
164
|
+
source=_SOURCE,
|
|
165
|
+
episode_type=EpisodeType.ISSUE,
|
|
166
|
+
occurred_at=raw.timestamp,
|
|
167
|
+
content=content,
|
|
168
|
+
raw_payload=data,
|
|
169
|
+
tenant_id=self.tenant_id,
|
|
170
|
+
namespace="jira",
|
|
171
|
+
entities=entities,
|
|
172
|
+
tags=["jira", status.lower().replace(" ", "_"), issue_type.lower()],
|
|
173
|
+
metadata={
|
|
174
|
+
"issue_key": issue_key,
|
|
175
|
+
"status": status,
|
|
176
|
+
"priority": priority,
|
|
177
|
+
"issue_type": issue_type,
|
|
178
|
+
"labels": data.get("labels", []),
|
|
179
|
+
"components": data.get("components", []),
|
|
180
|
+
"sprint": data.get("sprint"),
|
|
181
|
+
"story_points": data.get("story_points"),
|
|
182
|
+
},
|
|
183
|
+
thread_id=f"jira:{issue_key}",
|
|
184
|
+
idempotency_key=f"{raw.external_id}:{raw.timestamp.isoformat()}",
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def map_permissions(self, raw: RawEvent) -> Visibility:
|
|
188
|
+
"""Map Jira project visibility to CortexDB visibility.
|
|
189
|
+
|
|
190
|
+
Projects with ``projectTypeKey`` of ``service_desk`` are treated as
|
|
191
|
+
potentially customer-facing and marked ``Restricted``. All others
|
|
192
|
+
default to ``Organization``.
|
|
193
|
+
"""
|
|
194
|
+
project_type = raw.permissions.get("project_type", "software")
|
|
195
|
+
if project_type == "service_desk":
|
|
196
|
+
return Visibility(level=VisibilityLevel.RESTRICTED)
|
|
197
|
+
return Visibility(level=VisibilityLevel.ORGANIZATION)
|
|
198
|
+
|
|
199
|
+
# -- helpers -------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
@staticmethod
|
|
202
|
+
def _serialize_issue(issue: Any) -> dict[str, Any]:
|
|
203
|
+
"""Flatten a ``jira.Issue`` into a plain dict."""
|
|
204
|
+
fields = issue.fields
|
|
205
|
+
assignee = getattr(fields, "assignee", None)
|
|
206
|
+
reporter = getattr(fields, "reporter", None)
|
|
207
|
+
priority = getattr(fields, "priority", None)
|
|
208
|
+
status = getattr(fields, "status", None)
|
|
209
|
+
issuetype = getattr(fields, "issuetype", None)
|
|
210
|
+
project = getattr(fields, "project", None)
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
"key": issue.key,
|
|
214
|
+
"id": issue.id,
|
|
215
|
+
"summary": getattr(fields, "summary", ""),
|
|
216
|
+
"description": getattr(fields, "description", ""),
|
|
217
|
+
"status": str(status) if status else "Unknown",
|
|
218
|
+
"priority": str(priority) if priority else "Medium",
|
|
219
|
+
"issuetype": str(issuetype) if issuetype else "Task",
|
|
220
|
+
"assignee": {
|
|
221
|
+
"name": getattr(assignee, "displayName", "Unassigned"),
|
|
222
|
+
"email": getattr(assignee, "emailAddress", None),
|
|
223
|
+
"account_id": getattr(assignee, "accountId", None),
|
|
224
|
+
}
|
|
225
|
+
if assignee
|
|
226
|
+
else None,
|
|
227
|
+
"reporter": {
|
|
228
|
+
"name": getattr(reporter, "displayName", "Unknown"),
|
|
229
|
+
"email": getattr(reporter, "emailAddress", None),
|
|
230
|
+
"account_id": getattr(reporter, "accountId", None),
|
|
231
|
+
}
|
|
232
|
+
if reporter
|
|
233
|
+
else None,
|
|
234
|
+
"project": {
|
|
235
|
+
"key": getattr(project, "key", ""),
|
|
236
|
+
"name": getattr(project, "name", ""),
|
|
237
|
+
"projectTypeKey": getattr(project, "projectTypeKey", "software"),
|
|
238
|
+
}
|
|
239
|
+
if project
|
|
240
|
+
else {},
|
|
241
|
+
"labels": list(getattr(fields, "labels", []) or []),
|
|
242
|
+
"components": [
|
|
243
|
+
str(c) for c in (getattr(fields, "components", []) or [])
|
|
244
|
+
],
|
|
245
|
+
"created": getattr(fields, "created", ""),
|
|
246
|
+
"updated": getattr(fields, "updated", ""),
|
|
247
|
+
"sprint": None, # Populated from custom fields if available.
|
|
248
|
+
"story_points": None,
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
@staticmethod
|
|
252
|
+
def _extract_actor(data: dict[str, Any]) -> Actor:
|
|
253
|
+
"""Extract the primary actor (reporter) from issue data."""
|
|
254
|
+
reporter = data.get("reporter")
|
|
255
|
+
if reporter:
|
|
256
|
+
return Actor(
|
|
257
|
+
id=reporter.get("account_id", "unknown"),
|
|
258
|
+
name=reporter.get("name", "Unknown"),
|
|
259
|
+
email=reporter.get("email"),
|
|
260
|
+
)
|
|
261
|
+
return Actor(id="unknown", name="Unknown")
|
|
262
|
+
|
|
263
|
+
@staticmethod
|
|
264
|
+
def _extract_entities(data: dict[str, Any]) -> list[EntityRef]:
|
|
265
|
+
"""Extract entity references from issue data."""
|
|
266
|
+
entities: list[EntityRef] = []
|
|
267
|
+
|
|
268
|
+
project = data.get("project", {})
|
|
269
|
+
if project.get("key"):
|
|
270
|
+
entities.append(
|
|
271
|
+
EntityRef(
|
|
272
|
+
entity_type="project",
|
|
273
|
+
entity_id=project["key"],
|
|
274
|
+
display_name=project.get("name", project["key"]),
|
|
275
|
+
)
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
assignee = data.get("assignee")
|
|
279
|
+
if assignee and assignee.get("account_id"):
|
|
280
|
+
entities.append(
|
|
281
|
+
EntityRef(
|
|
282
|
+
entity_type="user",
|
|
283
|
+
entity_id=assignee["account_id"],
|
|
284
|
+
display_name=assignee.get("name", ""),
|
|
285
|
+
)
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
reporter = data.get("reporter")
|
|
289
|
+
if reporter and reporter.get("account_id"):
|
|
290
|
+
entities.append(
|
|
291
|
+
EntityRef(
|
|
292
|
+
entity_type="user",
|
|
293
|
+
entity_id=reporter["account_id"],
|
|
294
|
+
display_name=reporter.get("name", ""),
|
|
295
|
+
)
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
return entities
|