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,476 @@
|
|
|
1
|
+
"""CortexDB connectors CLI — `cortexdb-sync`.
|
|
2
|
+
|
|
3
|
+
Commands:
|
|
4
|
+
|
|
5
|
+
cortexdb-sync sync slack one-shot sync (uses stored cursor)
|
|
6
|
+
cortexdb-sync sync slack --since 2026-01-01T00:00:00Z
|
|
7
|
+
cortexdb-sync watch slack --interval 60 poll-and-sleep loop
|
|
8
|
+
cortexdb-sync status cursor table for every connector
|
|
9
|
+
cortexdb-sync list list available connectors + env vars
|
|
10
|
+
cortexdb-sync auth show resolved CortexDB credentials
|
|
11
|
+
|
|
12
|
+
Reads CortexDB creds in this order:
|
|
13
|
+
|
|
14
|
+
1. CLI flags (--api-url, --api-key, --actor, --scope-template)
|
|
15
|
+
2. Env vars (CORTEXDB_URL, CORTEXDB_API_KEY, CORTEXDB_ACTOR, CORTEXDB_SCOPE_TEMPLATE)
|
|
16
|
+
3. `~/.cortexdb/state.json` — written by `cortexdb init` from the cortexdb-cli
|
|
17
|
+
package; same format the cortexdb-mcp 0.3.0 server uses, so you can mint
|
|
18
|
+
a free-tier identity once and use it from everywhere.
|
|
19
|
+
|
|
20
|
+
Per-connector secrets (Slack bot token, Jira API token, …) come from env
|
|
21
|
+
vars by default; override with a YAML config file via --config.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import argparse
|
|
27
|
+
import asyncio
|
|
28
|
+
import importlib
|
|
29
|
+
import json
|
|
30
|
+
import logging
|
|
31
|
+
import os
|
|
32
|
+
import sys
|
|
33
|
+
import time
|
|
34
|
+
from datetime import datetime, timezone
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
from typing import Any, Callable
|
|
37
|
+
|
|
38
|
+
from cortexdb_connectors.state import SyncStateStore
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger("cortexdb_connectors")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
# Connector registry
|
|
45
|
+
#
|
|
46
|
+
# Each entry maps a CLI slug to:
|
|
47
|
+
# - module + class to import
|
|
48
|
+
# - secret env vars (required for the connector to function)
|
|
49
|
+
# - optional list/scalar config knobs the CLI surfaces
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
CONNECTORS: dict[str, dict[str, Any]] = {
|
|
53
|
+
"slack": {
|
|
54
|
+
"import": "cortexdb_connectors.slack:SlackConnector",
|
|
55
|
+
"secrets": [("slack_bot_token", "SLACK_BOT_TOKEN")],
|
|
56
|
+
"options": [("channels", "SLACK_CHANNELS", "list")],
|
|
57
|
+
},
|
|
58
|
+
"github": {
|
|
59
|
+
"import": "cortexdb_connectors.github:GitHubConnector",
|
|
60
|
+
"secrets": [("github_token", "GITHUB_TOKEN")],
|
|
61
|
+
"options": [
|
|
62
|
+
("repos", "GITHUB_REPOS", "list"),
|
|
63
|
+
("events", "GITHUB_EVENTS", "list"),
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
"gitlab": {
|
|
67
|
+
"import": "cortexdb_connectors.gitlab:GitLabConnector",
|
|
68
|
+
"secrets": [("gitlab_token", "GITLAB_TOKEN")],
|
|
69
|
+
"options": [
|
|
70
|
+
("gitlab_url", "GITLAB_URL", "str"),
|
|
71
|
+
("project_ids", "GITLAB_PROJECT_IDS", "list-int"),
|
|
72
|
+
("group_ids", "GITLAB_GROUP_IDS", "list-int"),
|
|
73
|
+
("events", "GITLAB_EVENTS", "list"),
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
"jira": {
|
|
77
|
+
"import": "cortexdb_connectors.jira:JiraConnector",
|
|
78
|
+
"secrets": [
|
|
79
|
+
("jira_url", "JIRA_URL"),
|
|
80
|
+
("jira_email", "JIRA_EMAIL"),
|
|
81
|
+
("jira_api_token", "JIRA_API_TOKEN"),
|
|
82
|
+
],
|
|
83
|
+
"options": [("project_keys", "JIRA_PROJECT_KEYS", "list")],
|
|
84
|
+
},
|
|
85
|
+
"linear": {
|
|
86
|
+
"import": "cortexdb_connectors.linear:LinearConnector",
|
|
87
|
+
"secrets": [("linear_api_key", "LINEAR_API_KEY")],
|
|
88
|
+
"options": [("team_ids", "LINEAR_TEAMS", "list")],
|
|
89
|
+
},
|
|
90
|
+
"confluence": {
|
|
91
|
+
"import": "cortexdb_connectors.confluence:ConfluenceConnector",
|
|
92
|
+
"secrets": [
|
|
93
|
+
("confluence_url", "CONFLUENCE_URL"),
|
|
94
|
+
("email", "CONFLUENCE_EMAIL"),
|
|
95
|
+
("api_token", "CONFLUENCE_API_TOKEN"),
|
|
96
|
+
],
|
|
97
|
+
"options": [("space_keys", "CONFLUENCE_SPACES", "list")],
|
|
98
|
+
},
|
|
99
|
+
"notion": {
|
|
100
|
+
"import": "cortexdb_connectors.notion:NotionConnector",
|
|
101
|
+
"secrets": [("notion_token", "NOTION_TOKEN")],
|
|
102
|
+
"options": [
|
|
103
|
+
("database_ids", "NOTION_DATABASES", "list"),
|
|
104
|
+
("page_ids", "NOTION_PAGES", "list"),
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
"pagerduty": {
|
|
108
|
+
"import": "cortexdb_connectors.pagerduty:PagerDutyConnector",
|
|
109
|
+
"secrets": [("pagerduty_api_key", "PAGERDUTY_API_KEY")],
|
|
110
|
+
"options": [("service_ids", "PAGERDUTY_SERVICES", "list")],
|
|
111
|
+
},
|
|
112
|
+
"discord": {
|
|
113
|
+
"import": "cortexdb_connectors.discord:DiscordConnector",
|
|
114
|
+
"secrets": [("bot_token", "DISCORD_BOT_TOKEN")],
|
|
115
|
+
"options": [
|
|
116
|
+
("guild_ids", "DISCORD_GUILDS", "list"),
|
|
117
|
+
("channel_ids", "DISCORD_CHANNELS", "list"),
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
"teams": {
|
|
121
|
+
"import": "cortexdb_connectors.teams:TeamsConnector",
|
|
122
|
+
"secrets": [
|
|
123
|
+
("azure_tenant_id", "TEAMS_TENANT_ID"),
|
|
124
|
+
("client_id", "TEAMS_CLIENT_ID"),
|
|
125
|
+
("client_secret", "TEAMS_CLIENT_SECRET"),
|
|
126
|
+
],
|
|
127
|
+
"options": [
|
|
128
|
+
("team_ids", "TEAMS_TEAM_IDS", "list"),
|
|
129
|
+
("include_chats", "TEAMS_INCLUDE_CHATS", "bool"),
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
"google-workspace": {
|
|
133
|
+
"import": "cortexdb_connectors.google_workspace:GoogleWorkspaceConnector",
|
|
134
|
+
"secrets": [
|
|
135
|
+
("service_account_key", "GW_SERVICE_ACCOUNT_KEY"),
|
|
136
|
+
("delegated_user", "GW_DELEGATED_USER"),
|
|
137
|
+
],
|
|
138
|
+
"options": [
|
|
139
|
+
("gmail_enabled", "GW_GMAIL", "bool"),
|
|
140
|
+
("drive_enabled", "GW_DRIVE", "bool"),
|
|
141
|
+
("calendar_enabled", "GW_CALENDAR", "bool"),
|
|
142
|
+
("shared_drives", "GW_SHARED_DRIVES", "list"),
|
|
143
|
+
("calendar_ids", "GW_CALENDARS", "list"),
|
|
144
|
+
],
|
|
145
|
+
},
|
|
146
|
+
"salesforce": {
|
|
147
|
+
"import": "cortexdb_connectors.salesforce:SalesforceConnector",
|
|
148
|
+
"secrets": [
|
|
149
|
+
("instance_url", "SF_INSTANCE_URL"),
|
|
150
|
+
("client_id", "SF_CLIENT_ID"),
|
|
151
|
+
("client_secret", "SF_CLIENT_SECRET"),
|
|
152
|
+
("username", "SF_USERNAME"),
|
|
153
|
+
("password", "SF_PASSWORD"),
|
|
154
|
+
],
|
|
155
|
+
"options": [("objects", "SF_OBJECTS", "list")],
|
|
156
|
+
},
|
|
157
|
+
"hubspot": {
|
|
158
|
+
"import": "cortexdb_connectors.hubspot:HubSpotConnector",
|
|
159
|
+
"secrets": [("access_token", "HUBSPOT_TOKEN")],
|
|
160
|
+
"options": [("objects", "HUBSPOT_OBJECTS", "list")],
|
|
161
|
+
},
|
|
162
|
+
"zendesk": {
|
|
163
|
+
"import": "cortexdb_connectors.zendesk:ZendeskConnector",
|
|
164
|
+
"secrets": [
|
|
165
|
+
("subdomain", "ZENDESK_SUBDOMAIN"),
|
|
166
|
+
("email", "ZENDESK_EMAIL"),
|
|
167
|
+
("api_token", "ZENDESK_TOKEN"),
|
|
168
|
+
],
|
|
169
|
+
"options": [("include_help_center", "ZENDESK_HELP_CENTER", "bool")],
|
|
170
|
+
},
|
|
171
|
+
"intercom": {
|
|
172
|
+
"import": "cortexdb_connectors.intercom:IntercomConnector",
|
|
173
|
+
"secrets": [("access_token", "INTERCOM_TOKEN")],
|
|
174
|
+
"options": [("include_articles", "INTERCOM_ARTICLES", "bool")],
|
|
175
|
+
},
|
|
176
|
+
"servicenow": {
|
|
177
|
+
"import": "cortexdb_connectors.servicenow:ServiceNowConnector",
|
|
178
|
+
"secrets": [
|
|
179
|
+
("instance", "SNOW_INSTANCE"),
|
|
180
|
+
("username", "SNOW_USERNAME"),
|
|
181
|
+
("password", "SNOW_PASSWORD"),
|
|
182
|
+
],
|
|
183
|
+
"options": [("tables", "SNOW_TABLES", "list")],
|
|
184
|
+
},
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# ---------------------------------------------------------------------------
|
|
189
|
+
# CortexDB credential resolution
|
|
190
|
+
# ---------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _state_file_path() -> Path:
|
|
194
|
+
return Path.home() / ".cortexdb" / "state.json"
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _load_cortex_creds(args: argparse.Namespace) -> dict[str, str]:
|
|
198
|
+
"""Resolve CortexDB url/api_key/actor/scope_template from flags → env → state.json."""
|
|
199
|
+
creds: dict[str, str] = {
|
|
200
|
+
"api_url": "",
|
|
201
|
+
"api_key": "",
|
|
202
|
+
"actor": "",
|
|
203
|
+
"scope_template": "",
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
sf = _state_file_path()
|
|
207
|
+
if sf.exists():
|
|
208
|
+
try:
|
|
209
|
+
data = json.loads(sf.read_text(encoding="utf-8"))
|
|
210
|
+
creds["api_key"] = data.get("token", "") or creds["api_key"]
|
|
211
|
+
creds["actor"] = data.get("user_id", "") or creds["actor"]
|
|
212
|
+
creds["scope_template"] = data.get("scope", "") or creds["scope_template"]
|
|
213
|
+
except (OSError, json.JSONDecodeError):
|
|
214
|
+
pass
|
|
215
|
+
|
|
216
|
+
env_map = {
|
|
217
|
+
"api_url": ("CORTEXDB_URL", "CORTEX_URL"),
|
|
218
|
+
"api_key": ("CORTEXDB_API_KEY", "CORTEX_API_KEY"),
|
|
219
|
+
"actor": ("CORTEXDB_ACTOR",),
|
|
220
|
+
"scope_template": ("CORTEXDB_SCOPE_TEMPLATE", "CORTEXDB_SCOPE"),
|
|
221
|
+
}
|
|
222
|
+
for key, vars_ in env_map.items():
|
|
223
|
+
for v in vars_:
|
|
224
|
+
if os.environ.get(v):
|
|
225
|
+
creds[key] = os.environ[v]
|
|
226
|
+
break
|
|
227
|
+
|
|
228
|
+
if getattr(args, "api_url", None):
|
|
229
|
+
creds["api_url"] = args.api_url
|
|
230
|
+
if getattr(args, "api_key", None):
|
|
231
|
+
creds["api_key"] = args.api_key
|
|
232
|
+
if getattr(args, "actor", None):
|
|
233
|
+
creds["actor"] = args.actor
|
|
234
|
+
if getattr(args, "scope_template", None):
|
|
235
|
+
creds["scope_template"] = args.scope_template
|
|
236
|
+
|
|
237
|
+
if not creds["api_url"]:
|
|
238
|
+
creds["api_url"] = "https://api-v1.cortexdb.ai"
|
|
239
|
+
if not creds["scope_template"] and creds["actor"]:
|
|
240
|
+
creds["scope_template"] = creds["actor"]
|
|
241
|
+
return creds
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# ---------------------------------------------------------------------------
|
|
245
|
+
# Connector factory
|
|
246
|
+
# ---------------------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
_OPTION_PARSERS: dict[str, Callable[[str], Any]] = {
|
|
249
|
+
"str": lambda v: v,
|
|
250
|
+
"list": lambda v: [x.strip() for x in v.split(",") if x.strip()],
|
|
251
|
+
"list-int": lambda v: [int(x.strip()) for x in v.split(",") if x.strip()],
|
|
252
|
+
"bool": lambda v: v.lower() in ("1", "true", "yes", "on"),
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _resolve_kwarg(env_var: str, config_value: Any, parser: str) -> Any:
|
|
257
|
+
"""Pull a value from config or env, parse it according to its type."""
|
|
258
|
+
if config_value is not None:
|
|
259
|
+
return config_value
|
|
260
|
+
raw = os.environ.get(env_var, "")
|
|
261
|
+
if not raw:
|
|
262
|
+
return None
|
|
263
|
+
return _OPTION_PARSERS[parser](raw)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _build_connector(
|
|
267
|
+
slug: str,
|
|
268
|
+
creds: dict[str, str],
|
|
269
|
+
config: dict[str, Any],
|
|
270
|
+
tenant_id: str,
|
|
271
|
+
) -> Any:
|
|
272
|
+
if slug not in CONNECTORS:
|
|
273
|
+
raise ValueError(f"Unknown connector: {slug!r}. Try `cortexdb-sync list`.")
|
|
274
|
+
spec = CONNECTORS[slug]
|
|
275
|
+
module_name, class_name = spec["import"].split(":")
|
|
276
|
+
module = importlib.import_module(module_name)
|
|
277
|
+
cls = getattr(module, class_name)
|
|
278
|
+
|
|
279
|
+
connector_cfg = (config or {}).get(slug, {})
|
|
280
|
+
|
|
281
|
+
kwargs: dict[str, Any] = {
|
|
282
|
+
"cortex_url": creds["api_url"],
|
|
283
|
+
"cortex_api_key": creds["api_key"],
|
|
284
|
+
"tenant_id": tenant_id,
|
|
285
|
+
"actor": creds["actor"],
|
|
286
|
+
"scope_template": creds["scope_template"],
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
for kw_name, env_var in spec.get("secrets", []):
|
|
290
|
+
value = _resolve_kwarg(env_var, connector_cfg.get(kw_name), "str")
|
|
291
|
+
if not value:
|
|
292
|
+
raise SystemExit(
|
|
293
|
+
f"Missing required secret for {slug}: set --{slug}-{kw_name} via "
|
|
294
|
+
f"config or env {env_var}."
|
|
295
|
+
)
|
|
296
|
+
kwargs[kw_name] = value
|
|
297
|
+
|
|
298
|
+
for kw_name, env_var, parser in spec.get("options", []):
|
|
299
|
+
value = _resolve_kwarg(env_var, connector_cfg.get(kw_name), parser)
|
|
300
|
+
if value is not None:
|
|
301
|
+
kwargs[kw_name] = value
|
|
302
|
+
|
|
303
|
+
return cls(**kwargs)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
# ---------------------------------------------------------------------------
|
|
307
|
+
# Commands
|
|
308
|
+
# ---------------------------------------------------------------------------
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _cmd_sync(args: argparse.Namespace) -> None:
|
|
312
|
+
creds = _load_cortex_creds(args)
|
|
313
|
+
config = _load_config(args.config)
|
|
314
|
+
state_store = SyncStateStore(path=args.state_file)
|
|
315
|
+
|
|
316
|
+
since: datetime | None = None
|
|
317
|
+
if args.since:
|
|
318
|
+
since = datetime.fromisoformat(args.since)
|
|
319
|
+
if since.tzinfo is None:
|
|
320
|
+
since = since.replace(tzinfo=timezone.utc)
|
|
321
|
+
else:
|
|
322
|
+
st = state_store.get(args.connector, args.tenant_id)
|
|
323
|
+
since = st.last_synced_at
|
|
324
|
+
|
|
325
|
+
connector = _build_connector(args.connector, creds, config, args.tenant_id)
|
|
326
|
+
|
|
327
|
+
logger.info(
|
|
328
|
+
"sync start connector=%s tenant=%s scope=%s since=%s",
|
|
329
|
+
args.connector,
|
|
330
|
+
args.tenant_id,
|
|
331
|
+
creds["scope_template"],
|
|
332
|
+
since,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
result = asyncio.run(connector.sync(since=since))
|
|
336
|
+
|
|
337
|
+
state_store.update_last_synced(args.connector, args.tenant_id)
|
|
338
|
+
|
|
339
|
+
logger.info(
|
|
340
|
+
"sync done fetched=%d ingested=%d errors=%d duration=%.1fs",
|
|
341
|
+
result.episodes_fetched,
|
|
342
|
+
result.episodes_ingested,
|
|
343
|
+
len(result.errors),
|
|
344
|
+
result.duration_seconds,
|
|
345
|
+
)
|
|
346
|
+
for err in result.errors:
|
|
347
|
+
logger.error(" %s", err)
|
|
348
|
+
if result.errors:
|
|
349
|
+
sys.exit(1)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _cmd_watch(args: argparse.Namespace) -> None:
|
|
353
|
+
"""Repeat sync forever with a sleep between cycles."""
|
|
354
|
+
while True:
|
|
355
|
+
try:
|
|
356
|
+
_cmd_sync(args)
|
|
357
|
+
except SystemExit as exc:
|
|
358
|
+
# Don't exit the watch loop on per-cycle errors.
|
|
359
|
+
logger.error("cycle failed: %s", exc)
|
|
360
|
+
logger.info("sleeping %ds before next cycle", args.interval)
|
|
361
|
+
time.sleep(args.interval)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _cmd_status(args: argparse.Namespace) -> None:
|
|
365
|
+
state_store = SyncStateStore(path=args.state_file)
|
|
366
|
+
states = state_store.list_all()
|
|
367
|
+
if not states:
|
|
368
|
+
print("No sync state recorded yet.")
|
|
369
|
+
return
|
|
370
|
+
print(f"{'Connector':<20} {'Tenant':<20} {'Last synced':<30} {'Cursor'}")
|
|
371
|
+
print("-" * 90)
|
|
372
|
+
for s in states:
|
|
373
|
+
last = s.last_synced_at.isoformat() if s.last_synced_at else "never"
|
|
374
|
+
cursor = s.cursor or "-"
|
|
375
|
+
print(f"{s.connector_name:<20} {s.tenant_id:<20} {last:<30} {cursor}")
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _cmd_list(_: argparse.Namespace) -> None:
|
|
379
|
+
print("Available connectors:\n")
|
|
380
|
+
for slug, spec in CONNECTORS.items():
|
|
381
|
+
env_vars = [v for _, v in spec.get("secrets", [])]
|
|
382
|
+
print(f" {slug:<20} required env: {', '.join(env_vars)}")
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _cmd_auth(args: argparse.Namespace) -> None:
|
|
386
|
+
creds = _load_cortex_creds(args)
|
|
387
|
+
redacted = {
|
|
388
|
+
"api_url": creds["api_url"],
|
|
389
|
+
"api_key": (creds["api_key"][:8] + "..." + creds["api_key"][-4:]) if creds["api_key"] else "(unset)",
|
|
390
|
+
"actor": creds["actor"] or "(unset)",
|
|
391
|
+
"scope_template": creds["scope_template"] or "(unset)",
|
|
392
|
+
}
|
|
393
|
+
print(json.dumps(redacted, indent=2))
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _load_config(path: str | None) -> dict[str, Any]:
|
|
397
|
+
if path is None:
|
|
398
|
+
candidates = [
|
|
399
|
+
Path.cwd() / "cortexdb-connectors.yaml",
|
|
400
|
+
Path.cwd() / "cortexdb-connectors.yml",
|
|
401
|
+
Path.home() / ".cortexdb" / "connectors.yaml",
|
|
402
|
+
]
|
|
403
|
+
for candidate in candidates:
|
|
404
|
+
if candidate.exists():
|
|
405
|
+
path = str(candidate)
|
|
406
|
+
break
|
|
407
|
+
if path is None:
|
|
408
|
+
return {}
|
|
409
|
+
try:
|
|
410
|
+
import yaml # type: ignore[import-untyped]
|
|
411
|
+
except ImportError:
|
|
412
|
+
logger.warning("PyYAML is not installed; ignoring config file %s", path)
|
|
413
|
+
return {}
|
|
414
|
+
with open(path, "r", encoding="utf-8") as fh:
|
|
415
|
+
data = yaml.safe_load(fh)
|
|
416
|
+
return data if isinstance(data, dict) else {}
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
# ---------------------------------------------------------------------------
|
|
420
|
+
# Entry point
|
|
421
|
+
# ---------------------------------------------------------------------------
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def main(argv: list[str] | None = None) -> None:
|
|
425
|
+
parser = argparse.ArgumentParser(
|
|
426
|
+
prog="cortexdb-sync",
|
|
427
|
+
description="CortexDB v1 data connector CLI",
|
|
428
|
+
)
|
|
429
|
+
parser.add_argument("--config", type=str, default=None, help="YAML config file path.")
|
|
430
|
+
parser.add_argument("--state-file", type=str, default=None, help="Sync state JSON file path.")
|
|
431
|
+
parser.add_argument("--api-url", type=str, help="CortexDB v1 API base URL.")
|
|
432
|
+
parser.add_argument("--api-key", type=str, help="PASETO bearer token.")
|
|
433
|
+
parser.add_argument("--actor", type=str, help="X-Cortex-Actor value.")
|
|
434
|
+
parser.add_argument("--scope-template", type=str, help='Scope template, e.g. "org:acme/source:slack".')
|
|
435
|
+
parser.add_argument("-v", "--verbose", action="store_true", help="Debug logging.")
|
|
436
|
+
sub = parser.add_subparsers(dest="command")
|
|
437
|
+
|
|
438
|
+
sync_p = sub.add_parser("sync", help="One-shot sync for a connector.")
|
|
439
|
+
sync_p.add_argument("connector", choices=list(CONNECTORS.keys()))
|
|
440
|
+
sync_p.add_argument("--since", type=str, default=None)
|
|
441
|
+
sync_p.add_argument("--tenant-id", type=str, default="default")
|
|
442
|
+
|
|
443
|
+
watch_p = sub.add_parser("watch", help="Sync forever with a sleep between cycles.")
|
|
444
|
+
watch_p.add_argument("connector", choices=list(CONNECTORS.keys()))
|
|
445
|
+
watch_p.add_argument("--since", type=str, default=None)
|
|
446
|
+
watch_p.add_argument("--tenant-id", type=str, default="default")
|
|
447
|
+
watch_p.add_argument("--interval", type=int, default=300, help="Seconds between cycles (default 300).")
|
|
448
|
+
|
|
449
|
+
sub.add_parser("status", help="Show cursor state for every connector.")
|
|
450
|
+
sub.add_parser("list", help="List available connectors and required env vars.")
|
|
451
|
+
sub.add_parser("auth", help="Show resolved CortexDB credentials (redacted).")
|
|
452
|
+
|
|
453
|
+
args = parser.parse_args(argv)
|
|
454
|
+
|
|
455
|
+
logging.basicConfig(
|
|
456
|
+
level=logging.DEBUG if args.verbose else logging.INFO,
|
|
457
|
+
format="%(asctime)s %(levelname)-8s %(name)s: %(message)s",
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
if args.command == "sync":
|
|
461
|
+
_cmd_sync(args)
|
|
462
|
+
elif args.command == "watch":
|
|
463
|
+
_cmd_watch(args)
|
|
464
|
+
elif args.command == "status":
|
|
465
|
+
_cmd_status(args)
|
|
466
|
+
elif args.command == "list":
|
|
467
|
+
_cmd_list(args)
|
|
468
|
+
elif args.command == "auth":
|
|
469
|
+
_cmd_auth(args)
|
|
470
|
+
else:
|
|
471
|
+
parser.print_help()
|
|
472
|
+
sys.exit(1)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
if __name__ == "__main__":
|
|
476
|
+
main()
|