hyperagents 1.2.3rc5.post3__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.
- agents/__init__.py +31 -0
- agents/background_monitor.py +453 -0
- agents/learning.py +460 -0
- agents/planner.py +421 -0
- agents/transcriber.py +341 -0
- civilization/__init__.py +51 -0
- civilization/api/__init__.py +7 -0
- civilization/api/civilization_api.py +361 -0
- civilization/graph/__init__.py +19 -0
- civilization/graph/knowledge_linker.py +348 -0
- civilization/graph/org_graph.py +348 -0
- civilization/graph/process_graph.py +386 -0
- civilization/ingestion/__init__.py +20 -0
- civilization/ingestion/chunker.py +273 -0
- civilization/ingestion/document_ingestor.py +358 -0
- civilization/ingestion/embedder.py +262 -0
- civilization/ingestion/extractor.py +224 -0
- civilization/interview/__init__.py +19 -0
- civilization/interview/gap_detector.py +380 -0
- civilization/interview/interviewer.py +323 -0
- civilization/interview/questioner.py +363 -0
- civilization/retrieval/__init__.py +17 -0
- civilization/retrieval/civilization_rag.py +316 -0
- civilization/retrieval/context_injector.py +342 -0
- civilization/retrieval/relevance_ranker.py +287 -0
- civilization/schema.py +299 -0
- civilization/store.py +177 -0
- civilization/sync/__init__.py +16 -0
- civilization/sync/confluence_sync.py +317 -0
- civilization/sync/gdrive_sync.py +262 -0
- civilization/sync/notion_sync.py +237 -0
- civilization/versioning/__init__.py +19 -0
- civilization/versioning/diff_engine.py +351 -0
- civilization/versioning/staleness_detector.py +303 -0
- civilization/versioning/version_manager.py +293 -0
- cli/__init__.py +1 -0
- cli/doctor.py +334 -0
- cli/hyperclaw.py +1357 -0
- cli/onboarding.py +469 -0
- core/__init__.py +1 -0
- core/connectors/__init__.py +119 -0
- core/connectors/infrastructure.py +772 -0
- core/connectors/messaging.py +586 -0
- core/connectors/productivity.py +764 -0
- core/connectors/smarthome.py +528 -0
- core/connectors/social.py +730 -0
- core/hyperrouter/__init__.py +23 -0
- core/hyperrouter/bandit.py +218 -0
- core/hyperrouter/fast_loop.py +68 -0
- core/hyperrouter/slow_loop.py +143 -0
- core/hyperstate/__init__.py +24 -0
- core/hyperstate/certifier.py +154 -0
- core/hyperstate/schema.py +160 -0
- core/hyperstate/state_manager.py +103 -0
- core/hyperstate/store.py +163 -0
- core/providers/__init__.py +121 -0
- core/providers/embeddings.py +389 -0
- core/providers/image.py +534 -0
- core/providers/llm.py +1935 -0
- core/providers/search.py +608 -0
- core/providers/voice.py +474 -0
- hyperagents-1.2.3rc5.post3.dist-info/METADATA +469 -0
- hyperagents-1.2.3rc5.post3.dist-info/RECORD +361 -0
- hyperagents-1.2.3rc5.post3.dist-info/WHEEL +5 -0
- hyperagents-1.2.3rc5.post3.dist-info/entry_points.txt +6 -0
- hyperagents-1.2.3rc5.post3.dist-info/licenses/LICENSE +21 -0
- hyperagents-1.2.3rc5.post3.dist-info/top_level.txt +13 -0
- hyperclaw/__init__.py +67 -0
- hyperclaw/__main__.py +220 -0
- hyperclaw/agent.py +775 -0
- hyperclaw/agent_coordinator.py +577 -0
- hyperclaw/anthropic_params.py +225 -0
- hyperclaw/api_utils.py +85 -0
- hyperclaw/approval_telegram.py +209 -0
- hyperclaw/approval_terminal.py +80 -0
- hyperclaw/attachments.py +177 -0
- hyperclaw/branding.py +55 -0
- hyperclaw/brief/__init__.py +11 -0
- hyperclaw/brief/cli.py +204 -0
- hyperclaw/brief/collect.py +146 -0
- hyperclaw/brief/compose.py +333 -0
- hyperclaw/brief/diff.py +110 -0
- hyperclaw/brief/errors.py +99 -0
- hyperclaw/brief/google.py +269 -0
- hyperclaw/brief/google_auth.py +294 -0
- hyperclaw/brief/links.py +124 -0
- hyperclaw/brief/service.py +424 -0
- hyperclaw/brief/store.py +205 -0
- hyperclaw/brief/tasks.py +200 -0
- hyperclaw/chat_scope.py +115 -0
- hyperclaw/cli_tui.py +497 -0
- hyperclaw/client_guard.py +717 -0
- hyperclaw/connection_check.py +113 -0
- hyperclaw/cost_guard.py +1728 -0
- hyperclaw/default_config/agents.yaml +534 -0
- hyperclaw/default_config/hyperclaw.yaml +27 -0
- hyperclaw/default_config/models.yaml +161 -0
- hyperclaw/deployment.py +39 -0
- hyperclaw/digest/__init__.py +9 -0
- hyperclaw/digest/cli.py +117 -0
- hyperclaw/digest/errors.py +100 -0
- hyperclaw/digest/gitlab.py +187 -0
- hyperclaw/digest/http.py +113 -0
- hyperclaw/digest/redact.py +32 -0
- hyperclaw/digest/service.py +271 -0
- hyperclaw/digest/slack.py +223 -0
- hyperclaw/digest/state.py +124 -0
- hyperclaw/digest/summarize.py +198 -0
- hyperclaw/digest/tokens.py +143 -0
- hyperclaw/distribution.py +46 -0
- hyperclaw/documents.py +351 -0
- hyperclaw/feeds.py +339 -0
- hyperclaw/google_workspace/__init__.py +5 -0
- hyperclaw/google_workspace/api.py +151 -0
- hyperclaw/google_workspace/auth.py +397 -0
- hyperclaw/google_workspace/calendar.py +157 -0
- hyperclaw/google_workspace/cli.py +72 -0
- hyperclaw/google_workspace/drive.py +236 -0
- hyperclaw/google_workspace/errors.py +58 -0
- hyperclaw/google_workspace/gmail.py +295 -0
- hyperclaw/google_workspace/meet.py +122 -0
- hyperclaw/google_workspace/tools.py +319 -0
- hyperclaw/hyperspeed.py +57 -0
- hyperclaw/integrations_layer.py +1427 -0
- hyperclaw/live_state.py +188 -0
- hyperclaw/managed_config.py +245 -0
- hyperclaw/media_hub.py +179 -0
- hyperclaw/memory_manager.py +584 -0
- hyperclaw/model_catalog.py +551 -0
- hyperclaw/model_policy.py +211 -0
- hyperclaw/model_router.py +595 -0
- hyperclaw/model_selector.py +72 -0
- hyperclaw/naming.py +284 -0
- hyperclaw/notices.py +65 -0
- hyperclaw/oai_stream.py +106 -0
- hyperclaw/orchestrator.py +591 -0
- hyperclaw/outbox.py +82 -0
- hyperclaw/persona.py +367 -0
- hyperclaw/prometheus.py +219 -0
- hyperclaw/prompt_assembly.py +157 -0
- hyperclaw/provider_setup.py +146 -0
- hyperclaw/providers.py +340 -0
- hyperclaw/qualification.py +341 -0
- hyperclaw/qualification_keys/hyper-d292fd127cef5a3e.pub +1 -0
- hyperclaw/qualification_records/anthropic--claude-sonnet-5-5.json +14 -0
- hyperclaw/reasoning_display.py +35 -0
- hyperclaw/scheduler.py +454 -0
- hyperclaw/server.py +847 -0
- hyperclaw/setup.py +627 -0
- hyperclaw/skills.py +717 -0
- hyperclaw/solomon.py +442 -0
- hyperclaw/swarm.py +533 -0
- hyperclaw/taint.py +832 -0
- hyperclaw/team/__init__.py +28 -0
- hyperclaw/team/dispatcher.py +431 -0
- hyperclaw/team/genesis.py +106 -0
- hyperclaw/team/lead.py +110 -0
- hyperclaw/team/model.py +135 -0
- hyperclaw/team/morning.py +238 -0
- hyperclaw/team/repos.py +66 -0
- hyperclaw/team/roster.py +264 -0
- hyperclaw/team/specialist.py +235 -0
- hyperclaw/team/status.py +163 -0
- hyperclaw/team/watchers.py +112 -0
- hyperclaw/teamsetup/__init__.py +17 -0
- hyperclaw/teamsetup/checks.py +208 -0
- hyperclaw/teamsetup/item.py +391 -0
- hyperclaw/teamsetup/oauth_loopback.py +84 -0
- hyperclaw/teamsetup/onboard.py +550 -0
- hyperclaw/teamsetup/profile.py +82 -0
- hyperclaw/teamsetup/secrets.py +313 -0
- hyperclaw/teamsetup/selfcheck.py +295 -0
- hyperclaw/teamsetup/service.py +495 -0
- hyperclaw/teamsetup/slack_oauth.py +442 -0
- hyperclaw/teamsetup/tty.py +62 -0
- hyperclaw/telegram_bot.py +1106 -0
- hyperclaw/telegram_intake.py +126 -0
- hyperclaw/telegram_pairing.py +360 -0
- hyperclaw/terminal_provider.py +323 -0
- hyperclaw/tools/__init__.py +1 -0
- hyperclaw/tools/approval.py +949 -0
- hyperclaw/tools/args.py +212 -0
- hyperclaw/tools/catalog.py +244 -0
- hyperclaw/tools/classify.py +266 -0
- hyperclaw/tools/effects.py +724 -0
- hyperclaw/tools/egress.py +210 -0
- hyperclaw/tools/envelope.py +188 -0
- hyperclaw/tools/guard.py +367 -0
- hyperclaw/tools/ledger.py +339 -0
- hyperclaw/tools/receipts.py +70 -0
- hyperclaw/tools/registry.py +318 -0
- hyperclaw/tools/service_guard.py +151 -0
- hyperclaw/tools/surfaces.py +54 -0
- hyperclaw/tools/workspace.py +87 -0
- hyperclaw/trading/__init__.py +77 -0
- hyperclaw/truth_guard.py +791 -0
- hyperclaw/tts.py +124 -0
- hyperclaw/tui.py +4255 -0
- hyperclaw/tui_bridge.py +938 -0
- hyperclaw/unattended.py +60 -0
- hyperclaw/updates/__init__.py +54 -0
- hyperclaw/updates/check.py +131 -0
- hyperclaw/updates/index.py +178 -0
- hyperclaw/updates/install.py +110 -0
- hyperclaw/updates/managed.py +161 -0
- hyperclaw/updates/manifest.py +138 -0
- hyperclaw/updates/notices.py +86 -0
- hyperclaw/updates/paths.py +53 -0
- hyperclaw/updates/release.py +146 -0
- hyperclaw/updates/upgrade.py +253 -0
- hyperclaw/updates/versioning.py +104 -0
- hyperclaw/userconfig.py +119 -0
- hyperclaw/xai.py +83 -0
- integrations/__init__.py +31 -0
- integrations/automation/__init__.py +1 -0
- integrations/automation/make.py +87 -0
- integrations/automation/zapier.py +87 -0
- integrations/base.py +476 -0
- integrations/communication/__init__.py +1 -0
- integrations/communication/twilio.py +279 -0
- integrations/communication/zoom.py +290 -0
- integrations/data/__init__.py +1 -0
- integrations/data/google_analytics.py +135 -0
- integrations/data/postgres.py +182 -0
- integrations/data/supabase_connector.py +202 -0
- integrations/data/weather_feeds.py +450 -0
- integrations/developer/__init__.py +9 -0
- integrations/developer/github.py +406 -0
- integrations/developer/gitlab.py +298 -0
- integrations/enterprise/__init__.py +13 -0
- integrations/enterprise/confluence.py +266 -0
- integrations/enterprise/hubspot.py +293 -0
- integrations/enterprise/jira.py +314 -0
- integrations/enterprise/salesforce.py +286 -0
- integrations/finance/__init__.py +1 -0
- integrations/finance/polymarket_client.py +247 -0
- integrations/finance/quickbooks.py +281 -0
- integrations/finance/stripe.py +261 -0
- integrations/gateway.py +238 -0
- integrations/messaging/__init__.py +32 -0
- integrations/messaging/discord.py +159 -0
- integrations/messaging/email.py +401 -0
- integrations/messaging/imessage.py +443 -0
- integrations/messaging/signal.py +175 -0
- integrations/messaging/slack.py +217 -0
- integrations/messaging/sms.py +175 -0
- integrations/messaging/teams.py +208 -0
- integrations/messaging/telegram.py +260 -0
- integrations/messaging/whatsapp.py +203 -0
- integrations/productivity/__init__.py +17 -0
- integrations/productivity/airtable.py +217 -0
- integrations/productivity/asana.py +248 -0
- integrations/productivity/google/__init__.py +19 -0
- integrations/productivity/google/base.py +89 -0
- integrations/productivity/google/calendar.py +272 -0
- integrations/productivity/google/docs.py +202 -0
- integrations/productivity/google/drive.py +268 -0
- integrations/productivity/google/gmail.py +279 -0
- integrations/productivity/google/meet.py +218 -0
- integrations/productivity/google/sheets.py +222 -0
- integrations/productivity/google/tasks.py +199 -0
- integrations/productivity/linear.py +279 -0
- integrations/productivity/microsoft/__init__.py +13 -0
- integrations/productivity/microsoft/base.py +93 -0
- integrations/productivity/microsoft/calendar.py +207 -0
- integrations/productivity/microsoft/onedrive.py +222 -0
- integrations/productivity/microsoft/outlook.py +241 -0
- integrations/productivity/microsoft/sharepoint.py +211 -0
- integrations/productivity/notion.py +243 -0
- integrations/productivity/todoist.py +250 -0
- integrations/productivity/trello.py +244 -0
- integrations/storage/__init__.py +1 -0
- integrations/storage/box.py +279 -0
- integrations/storage/dropbox.py +215 -0
- integrations/telegram_inbound.py +179 -0
- integrations/telegram_poller.py +151 -0
- memory/__init__.py +1 -0
- memory/causal_graph.py +125 -0
- memory/impact_tracker.py +88 -0
- memory/vector_memory.py +436 -0
- models/__init__.py +12 -0
- models/chatjimmy_client.py +210 -0
- models/claude_client.py +235 -0
- models/claude_code_subagent.py +192 -0
- models/openai_client.py +120 -0
- models/router.py +162 -0
- recursive/discovery_loop.py +118 -0
- recursive/optimization_loop.py +135 -0
- recursive/validation_loop.py +38 -0
- security/__init__.py +1 -0
- security/audit_logger.py +106 -0
- security/filesystem_guard.py +100 -0
- security/hypershield.py +99 -0
- security/network_guard.py +97 -0
- security/policy_engine.py +198 -0
- swarm/__init__.py +1 -0
- swarm/agents/__init__.py +0 -0
- swarm/agents/base.py +230 -0
- swarm/agents/business/__init__.py +0 -0
- swarm/agents/business/counsel.py +31 -0
- swarm/agents/business/herald.py +32 -0
- swarm/agents/business/ledger.py +26 -0
- swarm/agents/business/nexus.py +30 -0
- swarm/agents/business/ops.py +30 -0
- swarm/agents/business/pipeline.py +28 -0
- swarm/agents/business/revenue.py +30 -0
- swarm/agents/business/sovereign.py +31 -0
- swarm/agents/business/strategos.py +27 -0
- swarm/agents/business/talent.py +27 -0
- swarm/agents/business/venture.py +32 -0
- swarm/agents/comms/__init__.py +1 -0
- swarm/agents/comms/cipher.py +32 -0
- swarm/agents/comms/echo_comms.py +31 -0
- swarm/agents/comms/envoy.py +31 -0
- swarm/agents/comms/herald_scheduler.py +33 -0
- swarm/agents/comms/pulse.py +29 -0
- swarm/agents/creative/__init__.py +0 -0
- swarm/agents/creative/author.py +27 -0
- swarm/agents/creative/lens.py +25 -0
- swarm/agents/executive/__init__.py +1 -0
- swarm/agents/intelligence/__init__.py +1 -0
- swarm/agents/intelligence/arbiter.py +30 -0
- swarm/agents/intelligence/sentinel.py +30 -0
- swarm/agents/media/__init__.py +0 -0
- swarm/agents/personal/__init__.py +0 -0
- swarm/agents/personal/atlas.py +27 -0
- swarm/agents/personal/hearth.py +26 -0
- swarm/agents/personal/midas.py +27 -0
- swarm/agents/personal/navigator.py +27 -0
- swarm/agents/personal/nourish.py +31 -0
- swarm/agents/personal/vitals.py +33 -0
- swarm/agents/recursive/__init__.py +0 -0
- swarm/agents/recursive/alchemist.py +73 -0
- swarm/agents/recursive/calibrator.py +45 -0
- swarm/agents/recursive/scout.py +97 -0
- swarm/agents/scientific/__init__.py +0 -0
- swarm/agents/scientific/cosmos.py +36 -0
- swarm/agents/scientific/gaia.py +26 -0
- swarm/agents/scientific/medicus.py +33 -0
- swarm/agents/scientific/oracle_agent.py +39 -0
- swarm/agents/scientific/scribe.py +39 -0
- swarm/agents/talent/__init__.py +0 -0
- swarm/agents/talent/deal.py +28 -0
- swarm/agents/talent/roster.py +29 -0
- swarm/agents/talent/scout.py +29 -0
- swarm/agents/talent/stage.py +27 -0
- swarm/agents/tech/__init__.py +0 -0
- swarm/agents/tech/aegis.py +28 -0
- swarm/agents/tech/bridge.py +28 -0
- swarm/agents/tech/forge.py +28 -0
- swarm/agents/trading/__init__.py +1 -0
- swarm/agents/trading/global_prediction_engine.py +611 -0
- swarm/agents/trading/polymarket_trader.py +165 -0
- swarm/agents/trading/prediction_strategist.py +218 -0
- swarm/autogen_bridge.py +68 -0
- swarm/bid_protocol.py +196 -0
- swarm/dispatcher.py +527 -0
- swarm/nexus.py +181 -0
- swarm/registry.py +112 -0
- telegram_listener.py +403 -0
- ui/tui/app.py +214 -0
|
@@ -0,0 +1,772 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Infrastructure Connectors.
|
|
3
|
+
Integration with Docker, AWS, GCP, Azure, Tailscale, Snowflake, BigQuery, Datadog, PagerDuty.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import logging
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class Container:
|
|
21
|
+
"""Docker container."""
|
|
22
|
+
id: str
|
|
23
|
+
name: str
|
|
24
|
+
image: str
|
|
25
|
+
status: str
|
|
26
|
+
ports: dict = field(default_factory=dict)
|
|
27
|
+
raw: dict = field(default_factory=dict)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class CloudResource:
|
|
32
|
+
"""Cloud resource."""
|
|
33
|
+
id: str
|
|
34
|
+
name: str
|
|
35
|
+
type: str
|
|
36
|
+
region: str | None = None
|
|
37
|
+
status: str = ""
|
|
38
|
+
raw: dict = field(default_factory=dict)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class InfrastructureConnector(ABC):
|
|
42
|
+
"""Abstract base for infrastructure connectors."""
|
|
43
|
+
|
|
44
|
+
name: str = "base"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
48
|
+
# DOCKER
|
|
49
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
50
|
+
|
|
51
|
+
class DockerConnector(InfrastructureConnector):
|
|
52
|
+
"""Docker Engine API connector."""
|
|
53
|
+
|
|
54
|
+
name = "docker"
|
|
55
|
+
|
|
56
|
+
def __init__(self, host: str | None = None):
|
|
57
|
+
self.host = host or os.environ.get("DOCKER_HOST", "unix:///var/run/docker.sock")
|
|
58
|
+
if self.host.startswith("unix://"):
|
|
59
|
+
self.transport = httpx.HTTPTransport(uds=self.host[7:])
|
|
60
|
+
self.base_url = "http://localhost"
|
|
61
|
+
else:
|
|
62
|
+
self.transport = None
|
|
63
|
+
self.base_url = self.host
|
|
64
|
+
|
|
65
|
+
async def list_containers(self, all: bool = False) -> list[Container]:
|
|
66
|
+
async with httpx.AsyncClient(transport=self.transport) as client:
|
|
67
|
+
response = await client.get(
|
|
68
|
+
f"{self.base_url}/containers/json",
|
|
69
|
+
params={"all": str(all).lower()},
|
|
70
|
+
timeout=30.0,
|
|
71
|
+
)
|
|
72
|
+
response.raise_for_status()
|
|
73
|
+
data = response.json()
|
|
74
|
+
|
|
75
|
+
return [
|
|
76
|
+
Container(
|
|
77
|
+
id=c["Id"][:12],
|
|
78
|
+
name=c["Names"][0].lstrip("/") if c.get("Names") else "",
|
|
79
|
+
image=c["Image"],
|
|
80
|
+
status=c["Status"],
|
|
81
|
+
ports={p["PrivatePort"]: p.get("PublicPort") for p in c.get("Ports", [])},
|
|
82
|
+
raw=c,
|
|
83
|
+
)
|
|
84
|
+
for c in data
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
async def start_container(self, container_id: str) -> dict:
|
|
88
|
+
async with httpx.AsyncClient(transport=self.transport) as client:
|
|
89
|
+
response = await client.post(
|
|
90
|
+
f"{self.base_url}/containers/{container_id}/start",
|
|
91
|
+
timeout=30.0,
|
|
92
|
+
)
|
|
93
|
+
if response.status_code == 204:
|
|
94
|
+
return {"status": "started"}
|
|
95
|
+
response.raise_for_status()
|
|
96
|
+
return response.json()
|
|
97
|
+
|
|
98
|
+
async def stop_container(self, container_id: str, timeout: int = 10) -> dict:
|
|
99
|
+
async with httpx.AsyncClient(transport=self.transport) as client:
|
|
100
|
+
response = await client.post(
|
|
101
|
+
f"{self.base_url}/containers/{container_id}/stop",
|
|
102
|
+
params={"t": timeout},
|
|
103
|
+
timeout=30.0,
|
|
104
|
+
)
|
|
105
|
+
if response.status_code == 204:
|
|
106
|
+
return {"status": "stopped"}
|
|
107
|
+
response.raise_for_status()
|
|
108
|
+
return response.json()
|
|
109
|
+
|
|
110
|
+
async def logs(self, container_id: str, tail: int = 100) -> str:
|
|
111
|
+
async with httpx.AsyncClient(transport=self.transport) as client:
|
|
112
|
+
response = await client.get(
|
|
113
|
+
f"{self.base_url}/containers/{container_id}/logs",
|
|
114
|
+
params={"stdout": "true", "stderr": "true", "tail": tail},
|
|
115
|
+
timeout=30.0,
|
|
116
|
+
)
|
|
117
|
+
response.raise_for_status()
|
|
118
|
+
return response.text
|
|
119
|
+
|
|
120
|
+
async def run(self, image: str, command: str | None = None, **kwargs) -> Container:
|
|
121
|
+
"""Create and start a container."""
|
|
122
|
+
async with httpx.AsyncClient(transport=self.transport) as client:
|
|
123
|
+
# Create container
|
|
124
|
+
create_response = await client.post(
|
|
125
|
+
f"{self.base_url}/containers/create",
|
|
126
|
+
json={
|
|
127
|
+
"Image": image,
|
|
128
|
+
"Cmd": command.split() if command else None,
|
|
129
|
+
**kwargs,
|
|
130
|
+
},
|
|
131
|
+
timeout=60.0,
|
|
132
|
+
)
|
|
133
|
+
create_response.raise_for_status()
|
|
134
|
+
container_id = create_response.json()["Id"]
|
|
135
|
+
|
|
136
|
+
# Start container
|
|
137
|
+
await client.post(
|
|
138
|
+
f"{self.base_url}/containers/{container_id}/start",
|
|
139
|
+
timeout=30.0,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
return Container(id=container_id[:12], name="", image=image, status="running")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
146
|
+
# AWS
|
|
147
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
148
|
+
|
|
149
|
+
class AWSConnector(InfrastructureConnector):
|
|
150
|
+
"""AWS connector (via boto3)."""
|
|
151
|
+
|
|
152
|
+
name = "aws"
|
|
153
|
+
|
|
154
|
+
def __init__(
|
|
155
|
+
self,
|
|
156
|
+
access_key: str | None = None,
|
|
157
|
+
secret_key: str | None = None,
|
|
158
|
+
region: str | None = None,
|
|
159
|
+
):
|
|
160
|
+
self.access_key = access_key or os.environ.get("AWS_ACCESS_KEY_ID")
|
|
161
|
+
self.secret_key = secret_key or os.environ.get("AWS_SECRET_ACCESS_KEY")
|
|
162
|
+
self.region = region or os.environ.get("AWS_REGION", "us-east-1")
|
|
163
|
+
|
|
164
|
+
def _get_client(self, service: str):
|
|
165
|
+
try:
|
|
166
|
+
import boto3
|
|
167
|
+
except ImportError:
|
|
168
|
+
raise ImportError("boto3 required: pip install boto3")
|
|
169
|
+
|
|
170
|
+
return boto3.client(
|
|
171
|
+
service,
|
|
172
|
+
region_name=self.region,
|
|
173
|
+
aws_access_key_id=self.access_key,
|
|
174
|
+
aws_secret_access_key=self.secret_key,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
async def list_ec2_instances(self) -> list[CloudResource]:
|
|
178
|
+
import asyncio
|
|
179
|
+
|
|
180
|
+
ec2 = self._get_client("ec2")
|
|
181
|
+
|
|
182
|
+
def _list():
|
|
183
|
+
response = ec2.describe_instances()
|
|
184
|
+
instances = []
|
|
185
|
+
for reservation in response["Reservations"]:
|
|
186
|
+
for instance in reservation["Instances"]:
|
|
187
|
+
name = ""
|
|
188
|
+
for tag in instance.get("Tags", []):
|
|
189
|
+
if tag["Key"] == "Name":
|
|
190
|
+
name = tag["Value"]
|
|
191
|
+
break
|
|
192
|
+
instances.append(
|
|
193
|
+
CloudResource(
|
|
194
|
+
id=instance["InstanceId"],
|
|
195
|
+
name=name,
|
|
196
|
+
type="ec2",
|
|
197
|
+
region=self.region,
|
|
198
|
+
status=instance["State"]["Name"],
|
|
199
|
+
raw=instance,
|
|
200
|
+
)
|
|
201
|
+
)
|
|
202
|
+
return instances
|
|
203
|
+
|
|
204
|
+
return await asyncio.to_thread(_list)
|
|
205
|
+
|
|
206
|
+
async def list_s3_buckets(self) -> list[CloudResource]:
|
|
207
|
+
import asyncio
|
|
208
|
+
|
|
209
|
+
s3 = self._get_client("s3")
|
|
210
|
+
|
|
211
|
+
def _list():
|
|
212
|
+
response = s3.list_buckets()
|
|
213
|
+
return [
|
|
214
|
+
CloudResource(
|
|
215
|
+
id=bucket["Name"],
|
|
216
|
+
name=bucket["Name"],
|
|
217
|
+
type="s3",
|
|
218
|
+
raw=bucket,
|
|
219
|
+
)
|
|
220
|
+
for bucket in response["Buckets"]
|
|
221
|
+
]
|
|
222
|
+
|
|
223
|
+
return await asyncio.to_thread(_list)
|
|
224
|
+
|
|
225
|
+
async def invoke_lambda(self, function_name: str, payload: dict) -> dict:
|
|
226
|
+
import asyncio
|
|
227
|
+
import json
|
|
228
|
+
|
|
229
|
+
lambda_client = self._get_client("lambda")
|
|
230
|
+
|
|
231
|
+
def _invoke():
|
|
232
|
+
response = lambda_client.invoke(
|
|
233
|
+
FunctionName=function_name,
|
|
234
|
+
Payload=json.dumps(payload),
|
|
235
|
+
)
|
|
236
|
+
return json.loads(response["Payload"].read())
|
|
237
|
+
|
|
238
|
+
return await asyncio.to_thread(_invoke)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
242
|
+
# GCP
|
|
243
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
244
|
+
|
|
245
|
+
class GCPConnector(InfrastructureConnector):
|
|
246
|
+
"""Google Cloud Platform connector."""
|
|
247
|
+
|
|
248
|
+
name = "gcp"
|
|
249
|
+
|
|
250
|
+
def __init__(
|
|
251
|
+
self,
|
|
252
|
+
project_id: str | None = None,
|
|
253
|
+
credentials_path: str | None = None,
|
|
254
|
+
):
|
|
255
|
+
self.project_id = project_id or os.environ.get("GCP_PROJECT_ID")
|
|
256
|
+
self.credentials_path = credentials_path or os.environ.get("GOOGLE_APPLICATION_CREDENTIALS")
|
|
257
|
+
self._token: str | None = None
|
|
258
|
+
|
|
259
|
+
async def _get_token(self) -> str:
|
|
260
|
+
if self._token:
|
|
261
|
+
return self._token
|
|
262
|
+
|
|
263
|
+
# Use gcloud CLI for token
|
|
264
|
+
import subprocess
|
|
265
|
+
|
|
266
|
+
result = subprocess.run(
|
|
267
|
+
["gcloud", "auth", "print-access-token"],
|
|
268
|
+
capture_output=True,
|
|
269
|
+
text=True,
|
|
270
|
+
)
|
|
271
|
+
if result.returncode == 0:
|
|
272
|
+
self._token = result.stdout.strip()
|
|
273
|
+
return self._token
|
|
274
|
+
raise RuntimeError("Failed to get GCP token")
|
|
275
|
+
|
|
276
|
+
async def list_compute_instances(self, zone: str = "us-central1-a") -> list[CloudResource]:
|
|
277
|
+
token = await self._get_token()
|
|
278
|
+
|
|
279
|
+
async with httpx.AsyncClient() as client:
|
|
280
|
+
response = await client.get(
|
|
281
|
+
f"https://compute.googleapis.com/compute/v1/projects/{self.project_id}/zones/{zone}/instances",
|
|
282
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
283
|
+
timeout=30.0,
|
|
284
|
+
)
|
|
285
|
+
response.raise_for_status()
|
|
286
|
+
data = response.json()
|
|
287
|
+
|
|
288
|
+
return [
|
|
289
|
+
CloudResource(
|
|
290
|
+
id=instance["id"],
|
|
291
|
+
name=instance["name"],
|
|
292
|
+
type="compute",
|
|
293
|
+
region=zone,
|
|
294
|
+
status=instance["status"],
|
|
295
|
+
raw=instance,
|
|
296
|
+
)
|
|
297
|
+
for instance in data.get("items", [])
|
|
298
|
+
]
|
|
299
|
+
|
|
300
|
+
async def list_gcs_buckets(self) -> list[CloudResource]:
|
|
301
|
+
token = await self._get_token()
|
|
302
|
+
|
|
303
|
+
async with httpx.AsyncClient() as client:
|
|
304
|
+
response = await client.get(
|
|
305
|
+
f"https://storage.googleapis.com/storage/v1/b",
|
|
306
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
307
|
+
params={"project": self.project_id},
|
|
308
|
+
timeout=30.0,
|
|
309
|
+
)
|
|
310
|
+
response.raise_for_status()
|
|
311
|
+
data = response.json()
|
|
312
|
+
|
|
313
|
+
return [
|
|
314
|
+
CloudResource(
|
|
315
|
+
id=bucket["id"],
|
|
316
|
+
name=bucket["name"],
|
|
317
|
+
type="gcs",
|
|
318
|
+
region=bucket.get("location"),
|
|
319
|
+
raw=bucket,
|
|
320
|
+
)
|
|
321
|
+
for bucket in data.get("items", [])
|
|
322
|
+
]
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
326
|
+
# AZURE
|
|
327
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
328
|
+
|
|
329
|
+
class AzureConnector(InfrastructureConnector):
|
|
330
|
+
"""Microsoft Azure connector."""
|
|
331
|
+
|
|
332
|
+
name = "azure"
|
|
333
|
+
|
|
334
|
+
def __init__(
|
|
335
|
+
self,
|
|
336
|
+
subscription_id: str | None = None,
|
|
337
|
+
tenant_id: str | None = None,
|
|
338
|
+
client_id: str | None = None,
|
|
339
|
+
client_secret: str | None = None,
|
|
340
|
+
):
|
|
341
|
+
self.subscription_id = subscription_id or os.environ.get("AZURE_SUBSCRIPTION_ID")
|
|
342
|
+
self.tenant_id = tenant_id or os.environ.get("AZURE_TENANT_ID")
|
|
343
|
+
self.client_id = client_id or os.environ.get("AZURE_CLIENT_ID")
|
|
344
|
+
self.client_secret = client_secret or os.environ.get("AZURE_CLIENT_SECRET")
|
|
345
|
+
self._token: str | None = None
|
|
346
|
+
|
|
347
|
+
async def _get_token(self) -> str:
|
|
348
|
+
if self._token:
|
|
349
|
+
return self._token
|
|
350
|
+
|
|
351
|
+
async with httpx.AsyncClient() as client:
|
|
352
|
+
response = await client.post(
|
|
353
|
+
f"https://login.microsoftonline.com/{self.tenant_id}/oauth2/v2.0/token",
|
|
354
|
+
data={
|
|
355
|
+
"grant_type": "client_credentials",
|
|
356
|
+
"client_id": self.client_id,
|
|
357
|
+
"client_secret": self.client_secret,
|
|
358
|
+
"scope": "https://management.azure.com/.default",
|
|
359
|
+
},
|
|
360
|
+
timeout=30.0,
|
|
361
|
+
)
|
|
362
|
+
response.raise_for_status()
|
|
363
|
+
data = response.json()
|
|
364
|
+
self._token = data["access_token"]
|
|
365
|
+
return self._token
|
|
366
|
+
|
|
367
|
+
async def list_vms(self, resource_group: str | None = None) -> list[CloudResource]:
|
|
368
|
+
token = await self._get_token()
|
|
369
|
+
|
|
370
|
+
url = f"https://management.azure.com/subscriptions/{self.subscription_id}"
|
|
371
|
+
if resource_group:
|
|
372
|
+
url += f"/resourceGroups/{resource_group}"
|
|
373
|
+
url += "/providers/Microsoft.Compute/virtualMachines?api-version=2023-03-01"
|
|
374
|
+
|
|
375
|
+
async with httpx.AsyncClient() as client:
|
|
376
|
+
response = await client.get(
|
|
377
|
+
url,
|
|
378
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
379
|
+
timeout=30.0,
|
|
380
|
+
)
|
|
381
|
+
response.raise_for_status()
|
|
382
|
+
data = response.json()
|
|
383
|
+
|
|
384
|
+
return [
|
|
385
|
+
CloudResource(
|
|
386
|
+
id=vm["id"],
|
|
387
|
+
name=vm["name"],
|
|
388
|
+
type="vm",
|
|
389
|
+
region=vm["location"],
|
|
390
|
+
raw=vm,
|
|
391
|
+
)
|
|
392
|
+
for vm in data.get("value", [])
|
|
393
|
+
]
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
397
|
+
# TAILSCALE
|
|
398
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
399
|
+
|
|
400
|
+
class TailscaleConnector(InfrastructureConnector):
|
|
401
|
+
"""Tailscale VPN connector."""
|
|
402
|
+
|
|
403
|
+
name = "tailscale"
|
|
404
|
+
|
|
405
|
+
def __init__(self, api_key: str | None = None, tailnet: str | None = None):
|
|
406
|
+
self.api_key = api_key or os.environ.get("TAILSCALE_API_KEY")
|
|
407
|
+
self.tailnet = tailnet or os.environ.get("TAILSCALE_TAILNET", "-")
|
|
408
|
+
self.base_url = "https://api.tailscale.com/api/v2"
|
|
409
|
+
|
|
410
|
+
async def list_devices(self) -> list[CloudResource]:
|
|
411
|
+
async with httpx.AsyncClient() as client:
|
|
412
|
+
response = await client.get(
|
|
413
|
+
f"{self.base_url}/tailnet/{self.tailnet}/devices",
|
|
414
|
+
auth=(self.api_key, ""),
|
|
415
|
+
timeout=30.0,
|
|
416
|
+
)
|
|
417
|
+
response.raise_for_status()
|
|
418
|
+
data = response.json()
|
|
419
|
+
|
|
420
|
+
return [
|
|
421
|
+
CloudResource(
|
|
422
|
+
id=device["id"],
|
|
423
|
+
name=device["name"],
|
|
424
|
+
type="tailscale",
|
|
425
|
+
status="online" if device.get("online") else "offline",
|
|
426
|
+
raw=device,
|
|
427
|
+
)
|
|
428
|
+
for device in data.get("devices", [])
|
|
429
|
+
]
|
|
430
|
+
|
|
431
|
+
async def get_device(self, device_id: str) -> CloudResource:
|
|
432
|
+
async with httpx.AsyncClient() as client:
|
|
433
|
+
response = await client.get(
|
|
434
|
+
f"{self.base_url}/device/{device_id}",
|
|
435
|
+
auth=(self.api_key, ""),
|
|
436
|
+
timeout=30.0,
|
|
437
|
+
)
|
|
438
|
+
response.raise_for_status()
|
|
439
|
+
device = response.json()
|
|
440
|
+
|
|
441
|
+
return CloudResource(
|
|
442
|
+
id=device["id"],
|
|
443
|
+
name=device["name"],
|
|
444
|
+
type="tailscale",
|
|
445
|
+
status="online" if device.get("online") else "offline",
|
|
446
|
+
raw=device,
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
451
|
+
# SNOWFLAKE
|
|
452
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
453
|
+
|
|
454
|
+
class SnowflakeConnector(InfrastructureConnector):
|
|
455
|
+
"""Snowflake data warehouse connector."""
|
|
456
|
+
|
|
457
|
+
name = "snowflake"
|
|
458
|
+
|
|
459
|
+
def __init__(
|
|
460
|
+
self,
|
|
461
|
+
account: str | None = None,
|
|
462
|
+
user: str | None = None,
|
|
463
|
+
password: str | None = None,
|
|
464
|
+
warehouse: str | None = None,
|
|
465
|
+
database: str | None = None,
|
|
466
|
+
):
|
|
467
|
+
self.account = account or os.environ.get("SNOWFLAKE_ACCOUNT")
|
|
468
|
+
self.user = user or os.environ.get("SNOWFLAKE_USER")
|
|
469
|
+
self.password = password or os.environ.get("SNOWFLAKE_PASSWORD")
|
|
470
|
+
self.warehouse = warehouse or os.environ.get("SNOWFLAKE_WAREHOUSE")
|
|
471
|
+
self.database = database or os.environ.get("SNOWFLAKE_DATABASE")
|
|
472
|
+
|
|
473
|
+
def _get_connection(self):
|
|
474
|
+
try:
|
|
475
|
+
import snowflake.connector
|
|
476
|
+
except ImportError:
|
|
477
|
+
raise ImportError("snowflake-connector-python required: pip install snowflake-connector-python")
|
|
478
|
+
|
|
479
|
+
return snowflake.connector.connect(
|
|
480
|
+
account=self.account,
|
|
481
|
+
user=self.user,
|
|
482
|
+
password=self.password,
|
|
483
|
+
warehouse=self.warehouse,
|
|
484
|
+
database=self.database,
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
async def query(self, sql: str) -> list[dict]:
|
|
488
|
+
import asyncio
|
|
489
|
+
|
|
490
|
+
def _query():
|
|
491
|
+
conn = self._get_connection()
|
|
492
|
+
try:
|
|
493
|
+
cursor = conn.cursor()
|
|
494
|
+
cursor.execute(sql)
|
|
495
|
+
columns = [desc[0] for desc in cursor.description]
|
|
496
|
+
return [dict(zip(columns, row)) for row in cursor.fetchall()]
|
|
497
|
+
finally:
|
|
498
|
+
conn.close()
|
|
499
|
+
|
|
500
|
+
return await asyncio.to_thread(_query)
|
|
501
|
+
|
|
502
|
+
async def list_warehouses(self) -> list[CloudResource]:
|
|
503
|
+
results = await self.query("SHOW WAREHOUSES")
|
|
504
|
+
return [
|
|
505
|
+
CloudResource(
|
|
506
|
+
id=row["name"],
|
|
507
|
+
name=row["name"],
|
|
508
|
+
type="warehouse",
|
|
509
|
+
status=row.get("state", ""),
|
|
510
|
+
raw=row,
|
|
511
|
+
)
|
|
512
|
+
for row in results
|
|
513
|
+
]
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
517
|
+
# BIGQUERY
|
|
518
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
519
|
+
|
|
520
|
+
class BigQueryConnector(InfrastructureConnector):
|
|
521
|
+
"""Google BigQuery connector."""
|
|
522
|
+
|
|
523
|
+
name = "bigquery"
|
|
524
|
+
|
|
525
|
+
def __init__(self, project_id: str | None = None):
|
|
526
|
+
self.project_id = project_id or os.environ.get("GCP_PROJECT_ID")
|
|
527
|
+
self.base_url = "https://bigquery.googleapis.com/bigquery/v2"
|
|
528
|
+
|
|
529
|
+
async def _get_token(self) -> str:
|
|
530
|
+
import subprocess
|
|
531
|
+
|
|
532
|
+
result = subprocess.run(
|
|
533
|
+
["gcloud", "auth", "print-access-token"],
|
|
534
|
+
capture_output=True,
|
|
535
|
+
text=True,
|
|
536
|
+
)
|
|
537
|
+
if result.returncode == 0:
|
|
538
|
+
return result.stdout.strip()
|
|
539
|
+
raise RuntimeError("Failed to get GCP token")
|
|
540
|
+
|
|
541
|
+
async def query(self, sql: str) -> list[dict]:
|
|
542
|
+
token = await self._get_token()
|
|
543
|
+
|
|
544
|
+
async with httpx.AsyncClient() as client:
|
|
545
|
+
response = await client.post(
|
|
546
|
+
f"{self.base_url}/projects/{self.project_id}/queries",
|
|
547
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
548
|
+
json={"query": sql, "useLegacySql": False},
|
|
549
|
+
timeout=120.0,
|
|
550
|
+
)
|
|
551
|
+
response.raise_for_status()
|
|
552
|
+
data = response.json()
|
|
553
|
+
|
|
554
|
+
if not data.get("jobComplete"):
|
|
555
|
+
# Poll for completion
|
|
556
|
+
job_id = data["jobReference"]["jobId"]
|
|
557
|
+
while True:
|
|
558
|
+
import asyncio
|
|
559
|
+
await asyncio.sleep(1)
|
|
560
|
+
status_response = await client.get(
|
|
561
|
+
f"{self.base_url}/projects/{self.project_id}/queries/{job_id}",
|
|
562
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
563
|
+
timeout=30.0,
|
|
564
|
+
)
|
|
565
|
+
status_response.raise_for_status()
|
|
566
|
+
data = status_response.json()
|
|
567
|
+
if data.get("jobComplete"):
|
|
568
|
+
break
|
|
569
|
+
|
|
570
|
+
schema = data.get("schema", {}).get("fields", [])
|
|
571
|
+
columns = [field["name"] for field in schema]
|
|
572
|
+
|
|
573
|
+
return [
|
|
574
|
+
dict(zip(columns, [cell.get("v") for cell in row.get("f", [])]))
|
|
575
|
+
for row in data.get("rows", [])
|
|
576
|
+
]
|
|
577
|
+
|
|
578
|
+
async def list_datasets(self) -> list[CloudResource]:
|
|
579
|
+
token = await self._get_token()
|
|
580
|
+
|
|
581
|
+
async with httpx.AsyncClient() as client:
|
|
582
|
+
response = await client.get(
|
|
583
|
+
f"{self.base_url}/projects/{self.project_id}/datasets",
|
|
584
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
585
|
+
timeout=30.0,
|
|
586
|
+
)
|
|
587
|
+
response.raise_for_status()
|
|
588
|
+
data = response.json()
|
|
589
|
+
|
|
590
|
+
return [
|
|
591
|
+
CloudResource(
|
|
592
|
+
id=ds["id"],
|
|
593
|
+
name=ds["datasetReference"]["datasetId"],
|
|
594
|
+
type="dataset",
|
|
595
|
+
raw=ds,
|
|
596
|
+
)
|
|
597
|
+
for ds in data.get("datasets", [])
|
|
598
|
+
]
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
602
|
+
# DATADOG
|
|
603
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
604
|
+
|
|
605
|
+
class DatadogConnector(InfrastructureConnector):
|
|
606
|
+
"""Datadog monitoring connector."""
|
|
607
|
+
|
|
608
|
+
name = "datadog"
|
|
609
|
+
|
|
610
|
+
def __init__(
|
|
611
|
+
self,
|
|
612
|
+
api_key: str | None = None,
|
|
613
|
+
app_key: str | None = None,
|
|
614
|
+
site: str = "datadoghq.com",
|
|
615
|
+
):
|
|
616
|
+
self.api_key = api_key or os.environ.get("DATADOG_API_KEY")
|
|
617
|
+
self.app_key = app_key or os.environ.get("DATADOG_APP_KEY")
|
|
618
|
+
self.base_url = f"https://api.{site}/api/v1"
|
|
619
|
+
|
|
620
|
+
async def get_metrics(self, query: str, start: int, end: int) -> dict:
|
|
621
|
+
async with httpx.AsyncClient() as client:
|
|
622
|
+
response = await client.get(
|
|
623
|
+
f"{self.base_url}/query",
|
|
624
|
+
headers={
|
|
625
|
+
"DD-API-KEY": self.api_key,
|
|
626
|
+
"DD-APPLICATION-KEY": self.app_key,
|
|
627
|
+
},
|
|
628
|
+
params={"query": query, "from": start, "to": end},
|
|
629
|
+
timeout=30.0,
|
|
630
|
+
)
|
|
631
|
+
response.raise_for_status()
|
|
632
|
+
return response.json()
|
|
633
|
+
|
|
634
|
+
async def list_monitors(self) -> list[dict]:
|
|
635
|
+
async with httpx.AsyncClient() as client:
|
|
636
|
+
response = await client.get(
|
|
637
|
+
f"{self.base_url}/monitor",
|
|
638
|
+
headers={
|
|
639
|
+
"DD-API-KEY": self.api_key,
|
|
640
|
+
"DD-APPLICATION-KEY": self.app_key,
|
|
641
|
+
},
|
|
642
|
+
timeout=30.0,
|
|
643
|
+
)
|
|
644
|
+
response.raise_for_status()
|
|
645
|
+
return response.json()
|
|
646
|
+
|
|
647
|
+
async def send_event(self, title: str, text: str, **kwargs) -> dict:
|
|
648
|
+
async with httpx.AsyncClient() as client:
|
|
649
|
+
response = await client.post(
|
|
650
|
+
f"{self.base_url}/events",
|
|
651
|
+
headers={
|
|
652
|
+
"DD-API-KEY": self.api_key,
|
|
653
|
+
"DD-APPLICATION-KEY": self.app_key,
|
|
654
|
+
},
|
|
655
|
+
json={"title": title, "text": text, **kwargs},
|
|
656
|
+
timeout=30.0,
|
|
657
|
+
)
|
|
658
|
+
response.raise_for_status()
|
|
659
|
+
return response.json()
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
663
|
+
# PAGERDUTY
|
|
664
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
665
|
+
|
|
666
|
+
class PagerDutyConnector(InfrastructureConnector):
|
|
667
|
+
"""PagerDuty incident management connector."""
|
|
668
|
+
|
|
669
|
+
name = "pagerduty"
|
|
670
|
+
|
|
671
|
+
def __init__(self, api_key: str | None = None):
|
|
672
|
+
self.api_key = api_key or os.environ.get("PAGERDUTY_API_KEY")
|
|
673
|
+
self.base_url = "https://api.pagerduty.com"
|
|
674
|
+
|
|
675
|
+
async def list_incidents(self, status: str = "triggered,acknowledged") -> list[dict]:
|
|
676
|
+
async with httpx.AsyncClient() as client:
|
|
677
|
+
response = await client.get(
|
|
678
|
+
f"{self.base_url}/incidents",
|
|
679
|
+
headers={
|
|
680
|
+
"Authorization": f"Token token={self.api_key}",
|
|
681
|
+
"Content-Type": "application/json",
|
|
682
|
+
},
|
|
683
|
+
params={"statuses[]": status.split(",")},
|
|
684
|
+
timeout=30.0,
|
|
685
|
+
)
|
|
686
|
+
response.raise_for_status()
|
|
687
|
+
return response.json().get("incidents", [])
|
|
688
|
+
|
|
689
|
+
async def create_incident(
|
|
690
|
+
self,
|
|
691
|
+
title: str,
|
|
692
|
+
service_id: str,
|
|
693
|
+
urgency: str = "high",
|
|
694
|
+
**kwargs,
|
|
695
|
+
) -> dict:
|
|
696
|
+
async with httpx.AsyncClient() as client:
|
|
697
|
+
response = await client.post(
|
|
698
|
+
f"{self.base_url}/incidents",
|
|
699
|
+
headers={
|
|
700
|
+
"Authorization": f"Token token={self.api_key}",
|
|
701
|
+
"Content-Type": "application/json",
|
|
702
|
+
},
|
|
703
|
+
json={
|
|
704
|
+
"incident": {
|
|
705
|
+
"type": "incident",
|
|
706
|
+
"title": title,
|
|
707
|
+
"service": {"id": service_id, "type": "service_reference"},
|
|
708
|
+
"urgency": urgency,
|
|
709
|
+
**kwargs,
|
|
710
|
+
}
|
|
711
|
+
},
|
|
712
|
+
timeout=30.0,
|
|
713
|
+
)
|
|
714
|
+
response.raise_for_status()
|
|
715
|
+
return response.json().get("incident", {})
|
|
716
|
+
|
|
717
|
+
async def acknowledge_incident(self, incident_id: str) -> dict:
|
|
718
|
+
async with httpx.AsyncClient() as client:
|
|
719
|
+
response = await client.put(
|
|
720
|
+
f"{self.base_url}/incidents/{incident_id}",
|
|
721
|
+
headers={
|
|
722
|
+
"Authorization": f"Token token={self.api_key}",
|
|
723
|
+
"Content-Type": "application/json",
|
|
724
|
+
},
|
|
725
|
+
json={"incident": {"type": "incident", "status": "acknowledged"}},
|
|
726
|
+
timeout=30.0,
|
|
727
|
+
)
|
|
728
|
+
response.raise_for_status()
|
|
729
|
+
return response.json().get("incident", {})
|
|
730
|
+
|
|
731
|
+
async def resolve_incident(self, incident_id: str) -> dict:
|
|
732
|
+
async with httpx.AsyncClient() as client:
|
|
733
|
+
response = await client.put(
|
|
734
|
+
f"{self.base_url}/incidents/{incident_id}",
|
|
735
|
+
headers={
|
|
736
|
+
"Authorization": f"Token token={self.api_key}",
|
|
737
|
+
"Content-Type": "application/json",
|
|
738
|
+
},
|
|
739
|
+
json={"incident": {"type": "incident", "status": "resolved"}},
|
|
740
|
+
timeout=30.0,
|
|
741
|
+
)
|
|
742
|
+
response.raise_for_status()
|
|
743
|
+
return response.json().get("incident", {})
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
747
|
+
# PROVIDER FACTORY
|
|
748
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
749
|
+
|
|
750
|
+
INFRASTRUCTURE_CONNECTORS: dict[str, type[InfrastructureConnector]] = {
|
|
751
|
+
"docker": DockerConnector,
|
|
752
|
+
"aws": AWSConnector,
|
|
753
|
+
"gcp": GCPConnector,
|
|
754
|
+
"google_cloud": GCPConnector,
|
|
755
|
+
"azure": AzureConnector,
|
|
756
|
+
"tailscale": TailscaleConnector,
|
|
757
|
+
"snowflake": SnowflakeConnector,
|
|
758
|
+
"bigquery": BigQueryConnector,
|
|
759
|
+
"datadog": DatadogConnector,
|
|
760
|
+
"pagerduty": PagerDutyConnector,
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def get_infrastructure_connector(name: str, **kwargs) -> InfrastructureConnector:
|
|
765
|
+
"""Get an infrastructure connector by name."""
|
|
766
|
+
name = name.lower()
|
|
767
|
+
|
|
768
|
+
if name not in INFRASTRUCTURE_CONNECTORS:
|
|
769
|
+
available = ", ".join(sorted(INFRASTRUCTURE_CONNECTORS.keys()))
|
|
770
|
+
raise ValueError(f"Unknown infrastructure connector: {name}. Available: {available}")
|
|
771
|
+
|
|
772
|
+
return INFRASTRUCTURE_CONNECTORS[name](**kwargs)
|