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,279 @@
|
|
|
1
|
+
"""TwilioConnector — Twilio messaging integration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from integrations.base import (
|
|
10
|
+
BaseConnector,
|
|
11
|
+
ConnectorCapability,
|
|
12
|
+
ConnectorInfo,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger("hyperclaw.integrations.twilio")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TwilioConnector(BaseConnector):
|
|
19
|
+
"""
|
|
20
|
+
Twilio messaging connector for SMS, WhatsApp, and voice.
|
|
21
|
+
|
|
22
|
+
Required config:
|
|
23
|
+
- account_sid: Twilio account SID
|
|
24
|
+
- auth_token: Twilio auth token
|
|
25
|
+
Optional config:
|
|
26
|
+
- from_number: Default from phone number
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, config: dict) -> None:
|
|
30
|
+
self.config = config
|
|
31
|
+
self.account_sid = config.get("account_sid", "")
|
|
32
|
+
self.auth_token = config.get("auth_token", "")
|
|
33
|
+
self.from_number = config.get("from_number", "")
|
|
34
|
+
|
|
35
|
+
if config.get("enabled", False):
|
|
36
|
+
if not self.account_sid or not self.auth_token:
|
|
37
|
+
raise ValueError(
|
|
38
|
+
"TwilioConnector requires account_sid and auth_token"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
self._client: httpx.AsyncClient | None = None
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def info(self) -> ConnectorInfo:
|
|
45
|
+
return ConnectorInfo(
|
|
46
|
+
connector_id="twilio",
|
|
47
|
+
platform="twilio",
|
|
48
|
+
category="communication",
|
|
49
|
+
capabilities=frozenset(
|
|
50
|
+
[
|
|
51
|
+
ConnectorCapability.SEND_MESSAGE,
|
|
52
|
+
ConnectorCapability.RECEIVE_MESSAGE,
|
|
53
|
+
ConnectorCapability.ACTION,
|
|
54
|
+
ConnectorCapability.WEBHOOKS,
|
|
55
|
+
]
|
|
56
|
+
),
|
|
57
|
+
auth_type="api_key",
|
|
58
|
+
rate_limit_per_minute=100,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def _base_url(self) -> str:
|
|
63
|
+
return f"https://api.twilio.com/2010-04-01/Accounts/{self.account_sid}"
|
|
64
|
+
|
|
65
|
+
async def _get_client(self) -> httpx.AsyncClient:
|
|
66
|
+
if self._client is None:
|
|
67
|
+
self._client = httpx.AsyncClient(
|
|
68
|
+
timeout=30.0,
|
|
69
|
+
auth=(self.account_sid, self.auth_token),
|
|
70
|
+
)
|
|
71
|
+
return self._client
|
|
72
|
+
|
|
73
|
+
async def health(self) -> bool:
|
|
74
|
+
"""Check if Twilio API is accessible."""
|
|
75
|
+
try:
|
|
76
|
+
client = await self._get_client()
|
|
77
|
+
response = await client.get(f"{self._base_url}.json")
|
|
78
|
+
return response.status_code == 200
|
|
79
|
+
except Exception as e:
|
|
80
|
+
logger.error(f"Twilio health check failed: {e}")
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
async def send_sms(
|
|
84
|
+
self,
|
|
85
|
+
to: str,
|
|
86
|
+
body: str,
|
|
87
|
+
from_: str | None = None,
|
|
88
|
+
) -> dict:
|
|
89
|
+
"""Send an SMS message."""
|
|
90
|
+
client = await self._get_client()
|
|
91
|
+
|
|
92
|
+
data = {
|
|
93
|
+
"To": to,
|
|
94
|
+
"Body": body,
|
|
95
|
+
"From": from_ or self.from_number,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
response = await client.post(
|
|
99
|
+
f"{self._base_url}/Messages.json",
|
|
100
|
+
data=data,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
if response.status_code not in (200, 201):
|
|
104
|
+
raise Exception(f"Twilio send_sms failed: {response.text}")
|
|
105
|
+
|
|
106
|
+
return response.json()
|
|
107
|
+
|
|
108
|
+
async def send_whatsapp(
|
|
109
|
+
self,
|
|
110
|
+
to: str,
|
|
111
|
+
body: str,
|
|
112
|
+
from_: str | None = None,
|
|
113
|
+
) -> dict:
|
|
114
|
+
"""Send a WhatsApp message via Twilio."""
|
|
115
|
+
client = await self._get_client()
|
|
116
|
+
|
|
117
|
+
# Add whatsapp: prefix if not present
|
|
118
|
+
if not to.startswith("whatsapp:"):
|
|
119
|
+
to = f"whatsapp:{to}"
|
|
120
|
+
|
|
121
|
+
from_num = from_ or self.from_number
|
|
122
|
+
if not from_num.startswith("whatsapp:"):
|
|
123
|
+
from_num = f"whatsapp:{from_num}"
|
|
124
|
+
|
|
125
|
+
data = {
|
|
126
|
+
"To": to,
|
|
127
|
+
"Body": body,
|
|
128
|
+
"From": from_num,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
response = await client.post(
|
|
132
|
+
f"{self._base_url}/Messages.json",
|
|
133
|
+
data=data,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if response.status_code not in (200, 201):
|
|
137
|
+
raise Exception(f"Twilio send_whatsapp failed: {response.text}")
|
|
138
|
+
|
|
139
|
+
return response.json()
|
|
140
|
+
|
|
141
|
+
async def make_call(
|
|
142
|
+
self,
|
|
143
|
+
to: str,
|
|
144
|
+
from_: str,
|
|
145
|
+
url: str,
|
|
146
|
+
) -> dict:
|
|
147
|
+
"""Make a voice call."""
|
|
148
|
+
client = await self._get_client()
|
|
149
|
+
|
|
150
|
+
data = {
|
|
151
|
+
"To": to,
|
|
152
|
+
"From": from_,
|
|
153
|
+
"Url": url,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
response = await client.post(
|
|
157
|
+
f"{self._base_url}/Calls.json",
|
|
158
|
+
data=data,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
if response.status_code not in (200, 201):
|
|
162
|
+
raise Exception(f"Twilio make_call failed: {response.text}")
|
|
163
|
+
|
|
164
|
+
return response.json()
|
|
165
|
+
|
|
166
|
+
async def send_mms(
|
|
167
|
+
self,
|
|
168
|
+
to: str,
|
|
169
|
+
body: str,
|
|
170
|
+
media_url: str,
|
|
171
|
+
from_: str | None = None,
|
|
172
|
+
) -> dict:
|
|
173
|
+
"""Send an MMS message with media."""
|
|
174
|
+
client = await self._get_client()
|
|
175
|
+
|
|
176
|
+
data = {
|
|
177
|
+
"To": to,
|
|
178
|
+
"Body": body,
|
|
179
|
+
"From": from_ or self.from_number,
|
|
180
|
+
"MediaUrl": media_url,
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
response = await client.post(
|
|
184
|
+
f"{self._base_url}/Messages.json",
|
|
185
|
+
data=data,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
if response.status_code not in (200, 201):
|
|
189
|
+
raise Exception(f"Twilio send_mms failed: {response.text}")
|
|
190
|
+
|
|
191
|
+
return response.json()
|
|
192
|
+
|
|
193
|
+
async def list_messages(self, limit: int = 20) -> dict:
|
|
194
|
+
"""List recent messages."""
|
|
195
|
+
client = await self._get_client()
|
|
196
|
+
|
|
197
|
+
response = await client.get(
|
|
198
|
+
f"{self._base_url}/Messages.json",
|
|
199
|
+
params={"PageSize": limit},
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
if response.status_code != 200:
|
|
203
|
+
raise Exception(f"Twilio list_messages failed: {response.text}")
|
|
204
|
+
|
|
205
|
+
return response.json()
|
|
206
|
+
|
|
207
|
+
async def get_message(self, message_sid: str) -> dict:
|
|
208
|
+
"""Get a specific message."""
|
|
209
|
+
client = await self._get_client()
|
|
210
|
+
|
|
211
|
+
response = await client.get(
|
|
212
|
+
f"{self._base_url}/Messages/{message_sid}.json"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
if response.status_code != 200:
|
|
216
|
+
raise Exception(f"Twilio get_message failed: {response.text}")
|
|
217
|
+
|
|
218
|
+
return response.json()
|
|
219
|
+
|
|
220
|
+
async def list_calls(self, limit: int = 20) -> dict:
|
|
221
|
+
"""List recent calls."""
|
|
222
|
+
client = await self._get_client()
|
|
223
|
+
|
|
224
|
+
response = await client.get(
|
|
225
|
+
f"{self._base_url}/Calls.json",
|
|
226
|
+
params={"PageSize": limit},
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
if response.status_code != 200:
|
|
230
|
+
raise Exception(f"Twilio list_calls failed: {response.text}")
|
|
231
|
+
|
|
232
|
+
return response.json()
|
|
233
|
+
|
|
234
|
+
async def _read_impl(self, resource_id: str, **kwargs) -> dict:
|
|
235
|
+
"""Read a message."""
|
|
236
|
+
return await self.get_message(resource_id)
|
|
237
|
+
|
|
238
|
+
async def _list_impl(
|
|
239
|
+
self, resource_type: str, filters: dict, **kwargs
|
|
240
|
+
) -> list[dict]:
|
|
241
|
+
"""List resources."""
|
|
242
|
+
limit = filters.get("limit", 20)
|
|
243
|
+
if resource_type == "messages":
|
|
244
|
+
result = await self.list_messages(limit)
|
|
245
|
+
return result.get("messages", [])
|
|
246
|
+
elif resource_type == "calls":
|
|
247
|
+
result = await self.list_calls(limit)
|
|
248
|
+
return result.get("calls", [])
|
|
249
|
+
return []
|
|
250
|
+
|
|
251
|
+
async def _action_impl(self, action_name: str, params: dict) -> dict:
|
|
252
|
+
"""Execute Twilio-specific actions."""
|
|
253
|
+
if action_name == "send_sms":
|
|
254
|
+
return await self.send_sms(
|
|
255
|
+
to=params.get("to"),
|
|
256
|
+
body=params.get("body", ""),
|
|
257
|
+
from_=params.get("from"),
|
|
258
|
+
)
|
|
259
|
+
elif action_name == "send_whatsapp":
|
|
260
|
+
return await self.send_whatsapp(
|
|
261
|
+
to=params.get("to"),
|
|
262
|
+
body=params.get("body", ""),
|
|
263
|
+
from_=params.get("from"),
|
|
264
|
+
)
|
|
265
|
+
elif action_name == "make_call":
|
|
266
|
+
return await self.make_call(
|
|
267
|
+
to=params.get("to"),
|
|
268
|
+
from_=params.get("from"),
|
|
269
|
+
url=params.get("url"),
|
|
270
|
+
)
|
|
271
|
+
elif action_name == "send_mms":
|
|
272
|
+
return await self.send_mms(
|
|
273
|
+
to=params.get("to"),
|
|
274
|
+
body=params.get("body", ""),
|
|
275
|
+
media_url=params.get("media_url"),
|
|
276
|
+
from_=params.get("from"),
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
raise ValueError(f"Unknown action: {action_name}")
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""ZoomConnector — Zoom video conferencing integration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from integrations.base import (
|
|
11
|
+
BaseConnector,
|
|
12
|
+
ConnectorCapability,
|
|
13
|
+
ConnectorInfo,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger("hyperclaw.integrations.zoom")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ZoomConnector(BaseConnector):
|
|
20
|
+
"""
|
|
21
|
+
Zoom video conferencing connector.
|
|
22
|
+
|
|
23
|
+
Required config:
|
|
24
|
+
- account_id: Zoom account ID
|
|
25
|
+
- client_id: OAuth2 client ID
|
|
26
|
+
- client_secret: OAuth2 client secret
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
BASE_URL = "https://api.zoom.us/v2"
|
|
30
|
+
OAUTH_URL = "https://zoom.us/oauth/token"
|
|
31
|
+
|
|
32
|
+
def __init__(self, config: dict) -> None:
|
|
33
|
+
self.config = config
|
|
34
|
+
self.account_id = config.get("account_id", "")
|
|
35
|
+
self.client_id = config.get("client_id", "")
|
|
36
|
+
self.client_secret = config.get("client_secret", "")
|
|
37
|
+
self._access_token: str | None = None
|
|
38
|
+
|
|
39
|
+
if config.get("enabled", False):
|
|
40
|
+
if not all([self.account_id, self.client_id, self.client_secret]):
|
|
41
|
+
raise ValueError(
|
|
42
|
+
"ZoomConnector requires account_id, client_id, and client_secret"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
self._client: httpx.AsyncClient | None = None
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def info(self) -> ConnectorInfo:
|
|
49
|
+
return ConnectorInfo(
|
|
50
|
+
connector_id="zoom",
|
|
51
|
+
platform="zoom",
|
|
52
|
+
category="communication",
|
|
53
|
+
capabilities=frozenset(
|
|
54
|
+
[
|
|
55
|
+
ConnectorCapability.READ_DATA,
|
|
56
|
+
ConnectorCapability.WRITE_DATA,
|
|
57
|
+
ConnectorCapability.LIST_DATA,
|
|
58
|
+
ConnectorCapability.ACTION,
|
|
59
|
+
ConnectorCapability.WEBHOOKS,
|
|
60
|
+
]
|
|
61
|
+
),
|
|
62
|
+
auth_type="oauth2",
|
|
63
|
+
rate_limit_per_minute=100,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
async def _get_access_token(self) -> str:
|
|
67
|
+
"""Get OAuth2 access token using Server-to-Server OAuth."""
|
|
68
|
+
if self._access_token:
|
|
69
|
+
return self._access_token
|
|
70
|
+
|
|
71
|
+
credentials = base64.b64encode(
|
|
72
|
+
f"{self.client_id}:{self.client_secret}".encode()
|
|
73
|
+
).decode()
|
|
74
|
+
|
|
75
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
76
|
+
response = await client.post(
|
|
77
|
+
self.OAUTH_URL,
|
|
78
|
+
headers={
|
|
79
|
+
"Authorization": f"Basic {credentials}",
|
|
80
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
81
|
+
},
|
|
82
|
+
data={
|
|
83
|
+
"grant_type": "account_credentials",
|
|
84
|
+
"account_id": self.account_id,
|
|
85
|
+
},
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
if response.status_code != 200:
|
|
89
|
+
raise Exception(f"Zoom OAuth failed: {response.text}")
|
|
90
|
+
|
|
91
|
+
data = response.json()
|
|
92
|
+
self._access_token = data.get("access_token", "")
|
|
93
|
+
return self._access_token
|
|
94
|
+
|
|
95
|
+
async def _get_client(self) -> httpx.AsyncClient:
|
|
96
|
+
if self._client is None:
|
|
97
|
+
token = await self._get_access_token()
|
|
98
|
+
self._client = httpx.AsyncClient(
|
|
99
|
+
timeout=30.0,
|
|
100
|
+
headers={
|
|
101
|
+
"Authorization": f"Bearer {token}",
|
|
102
|
+
"Content-Type": "application/json",
|
|
103
|
+
},
|
|
104
|
+
)
|
|
105
|
+
return self._client
|
|
106
|
+
|
|
107
|
+
async def health(self) -> bool:
|
|
108
|
+
"""Check if Zoom API is accessible."""
|
|
109
|
+
try:
|
|
110
|
+
client = await self._get_client()
|
|
111
|
+
response = await client.get(f"{self.BASE_URL}/users/me")
|
|
112
|
+
return response.status_code == 200
|
|
113
|
+
except Exception as e:
|
|
114
|
+
logger.error(f"Zoom health check failed: {e}")
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
async def list_meetings(self, user_id: str = "me") -> dict:
|
|
118
|
+
"""List meetings for a user."""
|
|
119
|
+
client = await self._get_client()
|
|
120
|
+
|
|
121
|
+
response = await client.get(f"{self.BASE_URL}/users/{user_id}/meetings")
|
|
122
|
+
|
|
123
|
+
if response.status_code != 200:
|
|
124
|
+
raise Exception(f"Zoom list_meetings failed: {response.text}")
|
|
125
|
+
|
|
126
|
+
return response.json()
|
|
127
|
+
|
|
128
|
+
async def create_meeting(
|
|
129
|
+
self,
|
|
130
|
+
topic: str,
|
|
131
|
+
start_time: str,
|
|
132
|
+
duration: int = 60,
|
|
133
|
+
agenda: str = "",
|
|
134
|
+
) -> dict:
|
|
135
|
+
"""Create a meeting."""
|
|
136
|
+
client = await self._get_client()
|
|
137
|
+
|
|
138
|
+
payload = {
|
|
139
|
+
"topic": topic,
|
|
140
|
+
"type": 2, # Scheduled meeting
|
|
141
|
+
"start_time": start_time,
|
|
142
|
+
"duration": duration,
|
|
143
|
+
"timezone": "UTC",
|
|
144
|
+
}
|
|
145
|
+
if agenda:
|
|
146
|
+
payload["agenda"] = agenda
|
|
147
|
+
|
|
148
|
+
response = await client.post(
|
|
149
|
+
f"{self.BASE_URL}/users/me/meetings",
|
|
150
|
+
json=payload,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
if response.status_code not in (200, 201):
|
|
154
|
+
raise Exception(f"Zoom create_meeting failed: {response.text}")
|
|
155
|
+
|
|
156
|
+
return response.json()
|
|
157
|
+
|
|
158
|
+
async def get_meeting(self, meeting_id: str) -> dict:
|
|
159
|
+
"""Get meeting details."""
|
|
160
|
+
client = await self._get_client()
|
|
161
|
+
|
|
162
|
+
response = await client.get(f"{self.BASE_URL}/meetings/{meeting_id}")
|
|
163
|
+
|
|
164
|
+
if response.status_code != 200:
|
|
165
|
+
raise Exception(f"Zoom get_meeting failed: {response.text}")
|
|
166
|
+
|
|
167
|
+
return response.json()
|
|
168
|
+
|
|
169
|
+
async def list_recordings(self, user_id: str = "me") -> dict:
|
|
170
|
+
"""List recordings for a user."""
|
|
171
|
+
client = await self._get_client()
|
|
172
|
+
|
|
173
|
+
response = await client.get(f"{self.BASE_URL}/users/{user_id}/recordings")
|
|
174
|
+
|
|
175
|
+
if response.status_code != 200:
|
|
176
|
+
raise Exception(f"Zoom list_recordings failed: {response.text}")
|
|
177
|
+
|
|
178
|
+
return response.json()
|
|
179
|
+
|
|
180
|
+
async def get_transcript(self, meeting_id: str) -> dict:
|
|
181
|
+
"""Get transcript for a meeting recording."""
|
|
182
|
+
client = await self._get_client()
|
|
183
|
+
|
|
184
|
+
# First get the recording details
|
|
185
|
+
response = await client.get(
|
|
186
|
+
f"{self.BASE_URL}/meetings/{meeting_id}/recordings"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
if response.status_code != 200:
|
|
190
|
+
raise Exception(f"Zoom get_transcript failed: {response.text}")
|
|
191
|
+
|
|
192
|
+
data = response.json()
|
|
193
|
+
|
|
194
|
+
# Find VTT transcript file
|
|
195
|
+
for file in data.get("recording_files", []):
|
|
196
|
+
if file.get("file_type") == "TRANSCRIPT":
|
|
197
|
+
return file
|
|
198
|
+
|
|
199
|
+
return data
|
|
200
|
+
|
|
201
|
+
async def list_webinars(self, user_id: str = "me") -> dict:
|
|
202
|
+
"""List webinars for a user."""
|
|
203
|
+
client = await self._get_client()
|
|
204
|
+
|
|
205
|
+
response = await client.get(f"{self.BASE_URL}/users/{user_id}/webinars")
|
|
206
|
+
|
|
207
|
+
if response.status_code != 200:
|
|
208
|
+
raise Exception(f"Zoom list_webinars failed: {response.text}")
|
|
209
|
+
|
|
210
|
+
return response.json()
|
|
211
|
+
|
|
212
|
+
async def send_chat_message(
|
|
213
|
+
self,
|
|
214
|
+
to_jid: str,
|
|
215
|
+
message: str,
|
|
216
|
+
channel_id: str | None = None,
|
|
217
|
+
) -> dict:
|
|
218
|
+
"""Send a chat message."""
|
|
219
|
+
client = await self._get_client()
|
|
220
|
+
|
|
221
|
+
payload = {"message": message}
|
|
222
|
+
if channel_id:
|
|
223
|
+
payload["to_channel"] = channel_id
|
|
224
|
+
else:
|
|
225
|
+
payload["to_jid"] = to_jid
|
|
226
|
+
|
|
227
|
+
response = await client.post(
|
|
228
|
+
f"{self.BASE_URL}/chat/users/me/messages",
|
|
229
|
+
json=payload,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
if response.status_code not in (200, 201):
|
|
233
|
+
raise Exception(f"Zoom send_chat_message failed: {response.text}")
|
|
234
|
+
|
|
235
|
+
return response.json()
|
|
236
|
+
|
|
237
|
+
async def _read_impl(self, resource_id: str, **kwargs) -> dict:
|
|
238
|
+
"""Read meeting details."""
|
|
239
|
+
return await self.get_meeting(resource_id)
|
|
240
|
+
|
|
241
|
+
async def _write_impl(self, resource_type: str, data: dict, **kwargs) -> dict:
|
|
242
|
+
"""Create a meeting."""
|
|
243
|
+
if resource_type == "meeting":
|
|
244
|
+
return await self.create_meeting(
|
|
245
|
+
topic=data.get("topic", "Meeting"),
|
|
246
|
+
start_time=data.get("start_time"),
|
|
247
|
+
duration=data.get("duration", 60),
|
|
248
|
+
agenda=data.get("agenda", ""),
|
|
249
|
+
)
|
|
250
|
+
elif resource_type == "chat_message":
|
|
251
|
+
return await self.send_chat_message(
|
|
252
|
+
to_jid=data.get("to_jid", ""),
|
|
253
|
+
message=data.get("message", ""),
|
|
254
|
+
channel_id=data.get("channel_id"),
|
|
255
|
+
)
|
|
256
|
+
raise ValueError(f"Unknown resource type: {resource_type}")
|
|
257
|
+
|
|
258
|
+
async def _list_impl(
|
|
259
|
+
self, resource_type: str, filters: dict, **kwargs
|
|
260
|
+
) -> list[dict]:
|
|
261
|
+
"""List resources."""
|
|
262
|
+
user_id = filters.get("user_id", "me")
|
|
263
|
+
if resource_type == "meetings":
|
|
264
|
+
result = await self.list_meetings(user_id)
|
|
265
|
+
return result.get("meetings", [])
|
|
266
|
+
elif resource_type == "recordings":
|
|
267
|
+
result = await self.list_recordings(user_id)
|
|
268
|
+
return result.get("meetings", [])
|
|
269
|
+
elif resource_type == "webinars":
|
|
270
|
+
result = await self.list_webinars(user_id)
|
|
271
|
+
return result.get("webinars", [])
|
|
272
|
+
return []
|
|
273
|
+
|
|
274
|
+
async def _action_impl(self, action_name: str, params: dict) -> dict:
|
|
275
|
+
"""Execute Zoom-specific actions."""
|
|
276
|
+
if action_name == "create_meeting":
|
|
277
|
+
return await self.create_meeting(
|
|
278
|
+
topic=params.get("topic", "Meeting"),
|
|
279
|
+
start_time=params.get("start_time"),
|
|
280
|
+
duration=params.get("duration", 60),
|
|
281
|
+
agenda=params.get("agenda", ""),
|
|
282
|
+
)
|
|
283
|
+
elif action_name == "send_chat":
|
|
284
|
+
return await self.send_chat_message(
|
|
285
|
+
to_jid=params.get("to_jid", ""),
|
|
286
|
+
message=params.get("message", ""),
|
|
287
|
+
channel_id=params.get("channel_id"),
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
raise ValueError(f"Unknown action: {action_name}")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Data integrations
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""GoogleAnalyticsConnector — Google Analytics Data API integration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from integrations.base import (
|
|
10
|
+
BaseConnector,
|
|
11
|
+
ConnectorCapability,
|
|
12
|
+
ConnectorInfo,
|
|
13
|
+
)
|
|
14
|
+
from integrations.productivity.google.base import GoogleOAuthBase
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger("hyperclaw.integrations.google_analytics")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class GoogleAnalyticsConnector(GoogleOAuthBase, BaseConnector):
|
|
20
|
+
"""
|
|
21
|
+
Google Analytics Data API connector.
|
|
22
|
+
|
|
23
|
+
Required config:
|
|
24
|
+
- service_account_file: Path to service account JSON
|
|
25
|
+
OR
|
|
26
|
+
- access_token: OAuth2 access token
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
DATA_API_URL = "https://analyticsdata.googleapis.com/v1beta"
|
|
30
|
+
ADMIN_API_URL = "https://analyticsadmin.googleapis.com/v1alpha"
|
|
31
|
+
|
|
32
|
+
def __init__(self, config: dict) -> None:
|
|
33
|
+
self.config = config
|
|
34
|
+
self._init_google_auth(config)
|
|
35
|
+
|
|
36
|
+
if config.get("enabled", False):
|
|
37
|
+
self._validate_google_config(config)
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def info(self) -> ConnectorInfo:
|
|
41
|
+
return ConnectorInfo(
|
|
42
|
+
connector_id="google_analytics",
|
|
43
|
+
platform="google_analytics",
|
|
44
|
+
category="data",
|
|
45
|
+
capabilities=frozenset(
|
|
46
|
+
[
|
|
47
|
+
ConnectorCapability.READ_DATA,
|
|
48
|
+
ConnectorCapability.LIST_DATA,
|
|
49
|
+
]
|
|
50
|
+
),
|
|
51
|
+
auth_type="oauth2",
|
|
52
|
+
rate_limit_per_minute=60,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
async def health(self) -> bool:
|
|
56
|
+
"""Check if Google Analytics API is accessible."""
|
|
57
|
+
return True
|
|
58
|
+
|
|
59
|
+
async def run_report(
|
|
60
|
+
self,
|
|
61
|
+
property_id: str,
|
|
62
|
+
dimensions: list[str],
|
|
63
|
+
metrics: list[str],
|
|
64
|
+
date_range: dict,
|
|
65
|
+
) -> dict:
|
|
66
|
+
"""Run a Google Analytics report."""
|
|
67
|
+
client = await self._get_google_client()
|
|
68
|
+
|
|
69
|
+
payload = {
|
|
70
|
+
"dimensions": [{"name": d} for d in dimensions],
|
|
71
|
+
"metrics": [{"name": m} for m in metrics],
|
|
72
|
+
"dateRanges": [date_range],
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
response = await client.post(
|
|
76
|
+
f"{self.DATA_API_URL}/properties/{property_id}:runReport",
|
|
77
|
+
json=payload,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if response.status_code != 200:
|
|
81
|
+
raise Exception(f"Google Analytics run_report failed: {response.text}")
|
|
82
|
+
|
|
83
|
+
return response.json()
|
|
84
|
+
|
|
85
|
+
async def get_realtime(self, property_id: str) -> dict:
|
|
86
|
+
"""Get realtime analytics data."""
|
|
87
|
+
client = await self._get_google_client()
|
|
88
|
+
|
|
89
|
+
payload = {
|
|
90
|
+
"dimensions": [{"name": "country"}],
|
|
91
|
+
"metrics": [{"name": "activeUsers"}],
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
response = await client.post(
|
|
95
|
+
f"{self.DATA_API_URL}/properties/{property_id}:runRealtimeReport",
|
|
96
|
+
json=payload,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if response.status_code != 200:
|
|
100
|
+
raise Exception(f"Google Analytics get_realtime failed: {response.text}")
|
|
101
|
+
|
|
102
|
+
return response.json()
|
|
103
|
+
|
|
104
|
+
async def list_properties(self) -> dict:
|
|
105
|
+
"""List all Google Analytics properties."""
|
|
106
|
+
client = await self._get_google_client()
|
|
107
|
+
|
|
108
|
+
response = await client.get(f"{self.ADMIN_API_URL}/accountSummaries")
|
|
109
|
+
|
|
110
|
+
if response.status_code != 200:
|
|
111
|
+
raise Exception(
|
|
112
|
+
f"Google Analytics list_properties failed: {response.text}"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return response.json()
|
|
116
|
+
|
|
117
|
+
async def _read_impl(self, resource_id: str, **kwargs) -> dict:
|
|
118
|
+
"""Read analytics data for a property."""
|
|
119
|
+
dimensions = kwargs.get("dimensions", ["date"])
|
|
120
|
+
metrics = kwargs.get("metrics", ["sessions"])
|
|
121
|
+
date_range = kwargs.get("date_range", {
|
|
122
|
+
"startDate": "30daysAgo",
|
|
123
|
+
"endDate": "today",
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
return await self.run_report(resource_id, dimensions, metrics, date_range)
|
|
127
|
+
|
|
128
|
+
async def _list_impl(
|
|
129
|
+
self, resource_type: str, filters: dict, **kwargs
|
|
130
|
+
) -> list[dict]:
|
|
131
|
+
"""List properties."""
|
|
132
|
+
if resource_type == "properties":
|
|
133
|
+
result = await self.list_properties()
|
|
134
|
+
return result.get("accountSummaries", [])
|
|
135
|
+
return []
|