cortexdb-connectors 0.2.16__tar.gz → 0.2.18__tar.gz
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-0.2.16 → cortexdb_connectors-0.2.18}/PKG-INFO +1 -1
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/__init__.py +1 -1
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/base.py +28 -2
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/cli.py +60 -37
- cortexdb_connectors-0.2.18/cortexdb_connectors/slack/__init__.py +1030 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/webhooks.py +307 -19
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors.egg-info/PKG-INFO +1 -1
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors.egg-info/SOURCES.txt +2 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/pyproject.toml +1 -1
- cortexdb_connectors-0.2.18/tests/test_slack_file_ingestion.py +599 -0
- cortexdb_connectors-0.2.18/tests/test_slack_findings_0_2_17.py +474 -0
- cortexdb_connectors-0.2.16/cortexdb_connectors/slack/__init__.py +0 -577
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/README.md +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/confluence/__init__.py +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/discord/__init__.py +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/freshdesk/__init__.py +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/github/__init__.py +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/gitlab/__init__.py +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/google_workspace/__init__.py +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/hubspot/__init__.py +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/insights/__init__.py +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/insights/api.py +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/insights/detectors.py +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/insights/engine.py +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/intercom/__init__.py +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/jira/__init__.py +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/linear/__init__.py +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/notion/__init__.py +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/pagerduty/__init__.py +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/salesforce/__init__.py +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/servicenow/__init__.py +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/state.py +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/teams/__init__.py +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/tldv/__init__.py +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/worker.py +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/zendesk/__init__.py +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors.egg-info/dependency_links.txt +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors.egg-info/entry_points.txt +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors.egg-info/requires.txt +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors.egg-info/top_level.txt +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/setup.cfg +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/tests/test_base.py +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/tests/test_connector_coverage.py +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/tests/test_discord_normalize.py +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/tests/test_github.py +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/tests/test_ingest_e2e.py +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/tests/test_jira_attachments.py +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/tests/test_jira_findings_0_2_11.py +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/tests/test_jira_findings_0_2_9.py +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/tests/test_notion_comments.py +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/tests/test_product_findings_0_2_12.py +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/tests/test_product_findings_0_2_14.py +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/tests/test_slack.py +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/tests/test_slack_findings_0_2_16.py +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/tests/test_streaming_cursor.py +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/tests/test_tldv_findings_0_2_12.py +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/tests/test_tldv_findings_0_2_15.py +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/tests/test_webhooks.py +0 -0
- {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/tests/test_webhooks_replay.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cortexdb-connectors
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.18
|
|
4
4
|
Summary: Data connectors for CortexDB — ingest from Slack, GitHub, GitLab, Jira, Linear, Confluence, Notion, PagerDuty, Discord, Teams, Google Workspace, Salesforce, HubSpot, Zendesk, Intercom, and ServiceNow into the v1 memory API.
|
|
5
5
|
Author-email: CortexDB Team <team@cortexdb.ai>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -114,7 +114,7 @@ class VisibilityLevel(str, Enum):
|
|
|
114
114
|
@dataclass(frozen=True)
|
|
115
115
|
class Source:
|
|
116
116
|
system: str
|
|
117
|
-
connector_version: str = "0.2.
|
|
117
|
+
connector_version: str = "0.2.18"
|
|
118
118
|
|
|
119
119
|
|
|
120
120
|
@dataclass(frozen=True)
|
|
@@ -196,6 +196,20 @@ class Episode:
|
|
|
196
196
|
blob: Attachment | None = None
|
|
197
197
|
|
|
198
198
|
|
|
199
|
+
@dataclass(frozen=True)
|
|
200
|
+
class SyncFailure:
|
|
201
|
+
"""One item that failed to ingest this cycle — carries enough identity
|
|
202
|
+
(external_id + source timestamp) for the CLI's dead-letter cursor to
|
|
203
|
+
track a PER-ITEM retry budget and to name everything it abandons (R1;
|
|
204
|
+
the old single-watermark scheme under-reported the abandoned cohort
|
|
205
|
+
and could expire a late co-window failure on the earliest item's
|
|
206
|
+
budget)."""
|
|
207
|
+
|
|
208
|
+
external_id: str
|
|
209
|
+
timestamp: datetime
|
|
210
|
+
error: str
|
|
211
|
+
|
|
212
|
+
|
|
199
213
|
@dataclass
|
|
200
214
|
class SyncResult:
|
|
201
215
|
episodes_fetched: int = 0
|
|
@@ -214,6 +228,10 @@ class SyncResult:
|
|
|
214
228
|
# cursor before `earliest_failure` so a failed item is never skipped.
|
|
215
229
|
latest_success: datetime | None = None
|
|
216
230
|
earliest_failure: datetime | None = None
|
|
231
|
+
# Per-item failure records (R1): parallel to `errors`, but structured —
|
|
232
|
+
# the dead-letter cursor keys retry budgets on these instead of the
|
|
233
|
+
# single earliest_failure watermark.
|
|
234
|
+
failures: list[SyncFailure] = field(default_factory=list)
|
|
217
235
|
# True when `fetch_events` itself failed (nothing was listed, so the
|
|
218
236
|
# watermarks are meaningless): the caller must HOLD the cursor — the
|
|
219
237
|
# empty-window default of "advance to now()" would silently skip
|
|
@@ -362,10 +380,18 @@ class CortexConnector(ABC):
|
|
|
362
380
|
# Include the exception TYPE: httpx timeout/transport errors
|
|
363
381
|
# stringify to "" and produced blank error lines that hid the
|
|
364
382
|
# real failure from operators.
|
|
365
|
-
|
|
383
|
+
error_line = (
|
|
366
384
|
f"Error processing event {raw.external_id}: "
|
|
367
385
|
f"{type(exc).__name__}: {exc}"
|
|
368
386
|
)
|
|
387
|
+
result.errors.append(error_line)
|
|
388
|
+
result.failures.append(
|
|
389
|
+
SyncFailure(
|
|
390
|
+
external_id=raw.external_id,
|
|
391
|
+
timestamp=raw.timestamp,
|
|
392
|
+
error=error_line,
|
|
393
|
+
)
|
|
394
|
+
)
|
|
369
395
|
if result.earliest_failure is None or raw.timestamp < result.earliest_failure:
|
|
370
396
|
result.earliest_failure = raw.timestamp
|
|
371
397
|
|
|
@@ -53,7 +53,11 @@ CONNECTORS: dict[str, dict[str, Any]] = {
|
|
|
53
53
|
"slack": {
|
|
54
54
|
"import": "cortexdb_connectors.slack:SlackConnector",
|
|
55
55
|
"secrets": [("slack_bot_token", "SLACK_BOT_TOKEN")],
|
|
56
|
-
"options": [
|
|
56
|
+
"options": [
|
|
57
|
+
("channels", "SLACK_CHANNELS", "list"),
|
|
58
|
+
("ingest_files", "SLACK_INGEST_FILES", "bool"),
|
|
59
|
+
("file_types", "SLACK_FILE_TYPES", "list"),
|
|
60
|
+
],
|
|
57
61
|
},
|
|
58
62
|
"github": {
|
|
59
63
|
"import": "cortexdb_connectors.github:GitHubConnector",
|
|
@@ -431,54 +435,73 @@ def _cmd_sync(args: argparse.Namespace) -> None:
|
|
|
431
435
|
|
|
432
436
|
# Cursor safety: never advance past an item that FAILED to ingest — it
|
|
433
437
|
# would be silently skipped on the next incremental run. Hold the cursor
|
|
434
|
-
# just before the earliest failure so it (and everything
|
|
435
|
-
# re-attempted; on a clean run advance to the latest item we
|
|
438
|
+
# just before the earliest STILL-RETRYING failure so it (and everything
|
|
439
|
+
# after) is re-attempted; on a clean run advance to the latest item we
|
|
440
|
+
# ingested.
|
|
436
441
|
#
|
|
437
442
|
# T15 (dead-letter cap): "hold forever" turns one PERMANENTLY-failing
|
|
438
|
-
# item into a head-of-line block
|
|
439
|
-
#
|
|
440
|
-
#
|
|
441
|
-
#
|
|
442
|
-
#
|
|
443
|
-
#
|
|
443
|
+
# item into a head-of-line block. R1 refinement: the retry budget is
|
|
444
|
+
# tracked PER ITEM (keyed by external_id + timestamp), not per window —
|
|
445
|
+
# the old single-watermark streak abandoned the entire failure cohort
|
|
446
|
+
# on the earliest item's budget and named only one timestamp in the
|
|
447
|
+
# log. Now each item gets its own _DEAD_LETTER_AFTER cycles, the
|
|
448
|
+
# cursor advances only past exhausted items, and every abandoned item
|
|
449
|
+
# is named.
|
|
444
450
|
_DEAD_LETTER_AFTER = 5
|
|
445
451
|
state = state_store.get(args.connector, args.scope)
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
)
|
|
451
|
-
|
|
452
|
-
|
|
452
|
+
# Legacy single-watermark keys from pre-0.2.17 states.
|
|
453
|
+
state.extra.pop("failure_pinned_at", None)
|
|
454
|
+
state.extra.pop("failure_streak", None)
|
|
455
|
+
if result.failures:
|
|
456
|
+
prior = state.extra.get("failure_streaks") or {}
|
|
457
|
+
streaks: dict[str, int] = {}
|
|
458
|
+
for f in result.failures:
|
|
459
|
+
key = f"{f.timestamp.isoformat()}|{f.external_id}"
|
|
460
|
+
streaks[key] = int(prior.get(key, 0)) + 1
|
|
461
|
+
exhausted = {k for k, v in streaks.items() if v >= _DEAD_LETTER_AFTER}
|
|
462
|
+
if exhausted:
|
|
453
463
|
logger.error(
|
|
454
|
-
"DEAD-LETTER: item(s)
|
|
455
|
-
"
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
464
|
+
"DEAD-LETTER: abandoning %d item(s) after %d consecutive "
|
|
465
|
+
"failed cycles — they will NOT be retried: %s",
|
|
466
|
+
len(exhausted),
|
|
467
|
+
_DEAD_LETTER_AFTER,
|
|
468
|
+
", ".join(sorted(k.split("|", 1)[1] + " @ " + k.split("|", 1)[0]
|
|
469
|
+
for k in exhausted)),
|
|
459
470
|
)
|
|
471
|
+
surviving = [
|
|
472
|
+
f
|
|
473
|
+
for f in result.failures
|
|
474
|
+
if f"{f.timestamp.isoformat()}|{f.external_id}" not in exhausted
|
|
475
|
+
]
|
|
476
|
+
if surviving:
|
|
477
|
+
new_cursor = min(f.timestamp for f in surviving) - timedelta(seconds=1)
|
|
478
|
+
state.extra["failure_streaks"] = {
|
|
479
|
+
k: v for k, v in streaks.items() if k not in exhausted
|
|
480
|
+
}
|
|
481
|
+
else:
|
|
482
|
+
# Every remaining failure just dead-lettered: advance past them.
|
|
483
|
+
max_abandoned = max(f.timestamp for f in result.failures)
|
|
460
484
|
new_cursor = (
|
|
461
485
|
result.latest_success
|
|
462
486
|
if result.latest_success is not None
|
|
463
|
-
and result.latest_success >
|
|
464
|
-
else
|
|
487
|
+
and result.latest_success > max_abandoned
|
|
488
|
+
else max_abandoned + timedelta(seconds=1)
|
|
465
489
|
)
|
|
466
|
-
state.extra.pop("
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
new_cursor = result.earliest_failure - timedelta(seconds=1)
|
|
470
|
-
state.extra["failure_pinned_at"] = pinned_at
|
|
471
|
-
state.extra["failure_streak"] = streak
|
|
490
|
+
state.extra.pop("failure_streaks", None)
|
|
491
|
+
state.last_synced_at = new_cursor
|
|
492
|
+
state_store.save(state)
|
|
472
493
|
elif result.latest_success is not None:
|
|
473
|
-
|
|
474
|
-
state.
|
|
475
|
-
|
|
494
|
+
state.extra.pop("failure_streaks", None)
|
|
495
|
+
state.last_synced_at = result.latest_success
|
|
496
|
+
state_store.save(state)
|
|
476
497
|
else:
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
498
|
+
# N4: an empty window used to stamp wall-clock now(), which loses a
|
|
499
|
+
# message posted between fetch and save (or any host-ahead-of-source
|
|
500
|
+
# clock skew). Nothing was fetched, so there is nothing to advance
|
|
501
|
+
# past — hold the previous cursor unchanged (None on a first run
|
|
502
|
+
# simply means the next run fetches from the start again).
|
|
503
|
+
state.extra.pop("failure_streaks", None)
|
|
504
|
+
state_store.save(state)
|
|
482
505
|
|
|
483
506
|
logger.info(
|
|
484
507
|
"sync done fetched=%d ingested=%d skipped=%d errors=%d duration=%.1fs",
|