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.
Files changed (59) hide show
  1. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/PKG-INFO +1 -1
  2. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/__init__.py +1 -1
  3. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/base.py +28 -2
  4. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/cli.py +60 -37
  5. cortexdb_connectors-0.2.18/cortexdb_connectors/slack/__init__.py +1030 -0
  6. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/webhooks.py +307 -19
  7. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors.egg-info/PKG-INFO +1 -1
  8. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors.egg-info/SOURCES.txt +2 -0
  9. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/pyproject.toml +1 -1
  10. cortexdb_connectors-0.2.18/tests/test_slack_file_ingestion.py +599 -0
  11. cortexdb_connectors-0.2.18/tests/test_slack_findings_0_2_17.py +474 -0
  12. cortexdb_connectors-0.2.16/cortexdb_connectors/slack/__init__.py +0 -577
  13. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/README.md +0 -0
  14. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/confluence/__init__.py +0 -0
  15. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/discord/__init__.py +0 -0
  16. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/freshdesk/__init__.py +0 -0
  17. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/github/__init__.py +0 -0
  18. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/gitlab/__init__.py +0 -0
  19. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/google_workspace/__init__.py +0 -0
  20. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/hubspot/__init__.py +0 -0
  21. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/insights/__init__.py +0 -0
  22. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/insights/api.py +0 -0
  23. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/insights/detectors.py +0 -0
  24. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/insights/engine.py +0 -0
  25. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/intercom/__init__.py +0 -0
  26. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/jira/__init__.py +0 -0
  27. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/linear/__init__.py +0 -0
  28. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/notion/__init__.py +0 -0
  29. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/pagerduty/__init__.py +0 -0
  30. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/salesforce/__init__.py +0 -0
  31. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/servicenow/__init__.py +0 -0
  32. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/state.py +0 -0
  33. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/teams/__init__.py +0 -0
  34. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/tldv/__init__.py +0 -0
  35. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/worker.py +0 -0
  36. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors/zendesk/__init__.py +0 -0
  37. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors.egg-info/dependency_links.txt +0 -0
  38. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors.egg-info/entry_points.txt +0 -0
  39. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors.egg-info/requires.txt +0 -0
  40. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/cortexdb_connectors.egg-info/top_level.txt +0 -0
  41. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/setup.cfg +0 -0
  42. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/tests/test_base.py +0 -0
  43. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/tests/test_connector_coverage.py +0 -0
  44. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/tests/test_discord_normalize.py +0 -0
  45. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/tests/test_github.py +0 -0
  46. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/tests/test_ingest_e2e.py +0 -0
  47. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/tests/test_jira_attachments.py +0 -0
  48. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/tests/test_jira_findings_0_2_11.py +0 -0
  49. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/tests/test_jira_findings_0_2_9.py +0 -0
  50. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/tests/test_notion_comments.py +0 -0
  51. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/tests/test_product_findings_0_2_12.py +0 -0
  52. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/tests/test_product_findings_0_2_14.py +0 -0
  53. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/tests/test_slack.py +0 -0
  54. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/tests/test_slack_findings_0_2_16.py +0 -0
  55. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/tests/test_streaming_cursor.py +0 -0
  56. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/tests/test_tldv_findings_0_2_12.py +0 -0
  57. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/tests/test_tldv_findings_0_2_15.py +0 -0
  58. {cortexdb_connectors-0.2.16 → cortexdb_connectors-0.2.18}/tests/test_webhooks.py +0 -0
  59. {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.16
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
@@ -8,7 +8,7 @@ poll loop; or `from cortexdb_connectors.slack import SlackConnector`
8
8
  to drive it programmatically.
9
9
  """
10
10
 
11
- __version__ = "0.2.16"
11
+ __version__ = "0.2.18"
12
12
 
13
13
  from cortexdb_connectors.base import (
14
14
  Actor,
@@ -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.16"
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
- result.errors.append(
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": [("channels", "SLACK_CHANNELS", "list")],
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 after) is
435
- # re-attempted; on a clean run advance to the latest item we ingested.
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 — the cursor can never pass it, and
439
- # every later item is refetched every cycle for eternity. Track how many
440
- # consecutive cycles the cursor has been pinned at the same failure; at
441
- # _DEAD_LETTER_AFTER, advance past it with a LOUD error naming what was
442
- # abandoned, so the sync heals and the operator has a precise artifact
443
- # to chase.
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
- if result.earliest_failure is not None:
447
- pinned_at = result.earliest_failure.isoformat()
448
- streak = int(state.extra.get("failure_streak", 0)) if (
449
- state.extra.get("failure_pinned_at") == pinned_at
450
- ) else 0
451
- streak += 1
452
- if streak >= _DEAD_LETTER_AFTER:
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) at %s failed %d consecutive cycles — "
455
- "advancing the cursor past them. They will NOT be retried; "
456
- "see the per-item errors above for what was abandoned.",
457
- pinned_at,
458
- streak,
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 > result.earliest_failure
464
- else result.earliest_failure + timedelta(seconds=1)
487
+ and result.latest_success > max_abandoned
488
+ else max_abandoned + timedelta(seconds=1)
465
489
  )
466
- state.extra.pop("failure_pinned_at", None)
467
- state.extra.pop("failure_streak", None)
468
- else:
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
- new_cursor = result.latest_success
474
- state.extra.pop("failure_pinned_at", None)
475
- state.extra.pop("failure_streak", None)
494
+ state.extra.pop("failure_streaks", None)
495
+ state.last_synced_at = result.latest_success
496
+ state_store.save(state)
476
497
  else:
477
- new_cursor = None # empty window — nothing to miss; default to now()
478
- state.extra.pop("failure_pinned_at", None)
479
- state.extra.pop("failure_streak", None)
480
- state.last_synced_at = new_cursor or datetime.now(timezone.utc)
481
- state_store.save(state)
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",