@cloudstreamsoftware/claude-tools 1.0.0 → 1.2.0
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.
- package/README.md +152 -37
- package/agents/INDEX.md +183 -0
- package/agents/architect.md +247 -0
- package/agents/build-error-resolver.md +555 -0
- package/agents/catalyst-deployer.md +132 -0
- package/agents/code-reviewer.md +121 -0
- package/agents/compliance-auditor.md +148 -0
- package/agents/creator-architect.md +395 -0
- package/agents/deluge-reviewer.md +98 -0
- package/agents/doc-updater.md +471 -0
- package/agents/e2e-runner.md +711 -0
- package/agents/planner.md +122 -0
- package/agents/refactor-cleaner.md +309 -0
- package/agents/security-reviewer.md +582 -0
- package/agents/tdd-guide.md +302 -0
- package/bin/cloudstream-setup.js +16 -6
- package/config/versions.json +63 -0
- package/dist/hooks/hooks.json +209 -0
- package/dist/index.js +47 -0
- package/dist/lib/asset-value.js +609 -0
- package/dist/lib/client-manager.js +300 -0
- package/dist/lib/command-matcher.js +242 -0
- package/dist/lib/cross-session-patterns.js +754 -0
- package/dist/lib/intent-classifier.js +1075 -0
- package/dist/lib/package-manager.js +374 -0
- package/dist/lib/recommendation-engine.js +597 -0
- package/dist/lib/session-memory.js +489 -0
- package/dist/lib/skill-effectiveness.js +486 -0
- package/dist/lib/skill-matcher.js +595 -0
- package/dist/lib/tutorial-metrics.js +242 -0
- package/dist/lib/tutorial-progress.js +209 -0
- package/dist/lib/tutorial-renderer.js +431 -0
- package/dist/lib/utils.js +380 -0
- package/dist/lib/verify-formatter.js +143 -0
- package/dist/lib/workflow-state.js +249 -0
- package/hooks/hooks.json +209 -0
- package/package.json +5 -1
- package/scripts/aggregate-sessions.js +290 -0
- package/scripts/branch-name-validator.js +291 -0
- package/scripts/build.js +101 -0
- package/scripts/commands/client-switch.js +231 -0
- package/scripts/deprecate-skill.js +610 -0
- package/scripts/diagnose.js +324 -0
- package/scripts/doc-freshness.js +168 -0
- package/scripts/generate-weekly-digest.js +393 -0
- package/scripts/health-check.js +270 -0
- package/scripts/hooks/credential-check.js +101 -0
- package/scripts/hooks/evaluate-session.js +81 -0
- package/scripts/hooks/pre-compact.js +66 -0
- package/scripts/hooks/prompt-analyzer.js +276 -0
- package/scripts/hooks/prompt-router.js +422 -0
- package/scripts/hooks/quality-gate-enforcer.js +371 -0
- package/scripts/hooks/session-end.js +156 -0
- package/scripts/hooks/session-start.js +195 -0
- package/scripts/hooks/skill-injector.js +333 -0
- package/scripts/hooks/suggest-compact.js +58 -0
- package/scripts/lib/asset-value.js +609 -0
- package/scripts/lib/client-manager.js +300 -0
- package/scripts/lib/command-matcher.js +242 -0
- package/scripts/lib/cross-session-patterns.js +754 -0
- package/scripts/lib/intent-classifier.js +1075 -0
- package/scripts/lib/package-manager.js +374 -0
- package/scripts/lib/recommendation-engine.js +597 -0
- package/scripts/lib/session-memory.js +489 -0
- package/scripts/lib/skill-effectiveness.js +486 -0
- package/scripts/lib/skill-matcher.js +595 -0
- package/scripts/lib/tutorial-metrics.js +242 -0
- package/scripts/lib/tutorial-progress.js +209 -0
- package/scripts/lib/tutorial-renderer.js +431 -0
- package/scripts/lib/utils.js +380 -0
- package/scripts/lib/verify-formatter.js +143 -0
- package/scripts/lib/workflow-state.js +249 -0
- package/scripts/onboard.js +363 -0
- package/scripts/quarterly-report.js +692 -0
- package/scripts/setup-package-manager.js +204 -0
- package/scripts/sync-upstream.js +391 -0
- package/scripts/test.js +108 -0
- package/scripts/tutorial-runner.js +351 -0
- package/scripts/validate-all.js +201 -0
- package/scripts/verifiers/agents.js +245 -0
- package/scripts/verifiers/config.js +186 -0
- package/scripts/verifiers/environment.js +123 -0
- package/scripts/verifiers/hooks.js +188 -0
- package/scripts/verifiers/index.js +38 -0
- package/scripts/verifiers/persistence.js +140 -0
- package/scripts/verifiers/plugin.js +215 -0
- package/scripts/verifiers/skills.js +209 -0
- package/scripts/verify-setup.js +164 -0
- package/skills/INDEX.md +157 -0
- package/skills/backend-patterns/SKILL.md +586 -0
- package/skills/backend-patterns/catalyst-patterns.md +128 -0
- package/skills/bigquery-patterns/SKILL.md +27 -0
- package/skills/bigquery-patterns/performance-optimization.md +518 -0
- package/skills/bigquery-patterns/query-patterns.md +372 -0
- package/skills/bigquery-patterns/schema-design.md +78 -0
- package/skills/cloudstream-project-template/SKILL.md +20 -0
- package/skills/cloudstream-project-template/structure.md +65 -0
- package/skills/coding-standards/SKILL.md +524 -0
- package/skills/coding-standards/deluge-standards.md +83 -0
- package/skills/compliance-patterns/SKILL.md +28 -0
- package/skills/compliance-patterns/hipaa/audit-requirements.md +251 -0
- package/skills/compliance-patterns/hipaa/baa-process.md +298 -0
- package/skills/compliance-patterns/hipaa/data-archival-strategy.md +387 -0
- package/skills/compliance-patterns/hipaa/phi-handling.md +52 -0
- package/skills/compliance-patterns/pci-dss/saq-a-requirements.md +307 -0
- package/skills/compliance-patterns/pci-dss/tokenization-patterns.md +382 -0
- package/skills/compliance-patterns/pci-dss/zoho-checkout-patterns.md +56 -0
- package/skills/compliance-patterns/soc2/access-controls.md +344 -0
- package/skills/compliance-patterns/soc2/audit-logging.md +458 -0
- package/skills/compliance-patterns/soc2/change-management.md +403 -0
- package/skills/compliance-patterns/soc2/deluge-execution-logging.md +407 -0
- package/skills/consultancy-workflows/SKILL.md +19 -0
- package/skills/consultancy-workflows/client-isolation.md +21 -0
- package/skills/consultancy-workflows/documentation-automation.md +454 -0
- package/skills/consultancy-workflows/handoff-procedures.md +257 -0
- package/skills/consultancy-workflows/knowledge-capture.md +513 -0
- package/skills/consultancy-workflows/time-tracking.md +26 -0
- package/skills/continuous-learning/SKILL.md +84 -0
- package/skills/continuous-learning/config.json +18 -0
- package/skills/continuous-learning/evaluate-session.sh +60 -0
- package/skills/continuous-learning-v2/SKILL.md +126 -0
- package/skills/continuous-learning-v2/config.json +61 -0
- package/skills/frontend-patterns/SKILL.md +635 -0
- package/skills/frontend-patterns/zoho-widget-patterns.md +103 -0
- package/skills/gcp-data-engineering/SKILL.md +36 -0
- package/skills/gcp-data-engineering/bigquery/performance-optimization.md +337 -0
- package/skills/gcp-data-engineering/dataflow/error-handling.md +496 -0
- package/skills/gcp-data-engineering/dataflow/pipeline-patterns.md +444 -0
- package/skills/gcp-data-engineering/dbt/model-organization.md +63 -0
- package/skills/gcp-data-engineering/dbt/testing-patterns.md +503 -0
- package/skills/gcp-data-engineering/medallion-architecture/bronze-layer.md +60 -0
- package/skills/gcp-data-engineering/medallion-architecture/gold-layer.md +311 -0
- package/skills/gcp-data-engineering/medallion-architecture/layer-transitions.md +517 -0
- package/skills/gcp-data-engineering/medallion-architecture/silver-layer.md +305 -0
- package/skills/gcp-data-engineering/zoho-to-gcp/data-extraction.md +543 -0
- package/skills/gcp-data-engineering/zoho-to-gcp/real-time-vs-batch.md +337 -0
- package/skills/security-review/SKILL.md +498 -0
- package/skills/security-review/compliance-checklist.md +53 -0
- package/skills/strategic-compact/SKILL.md +67 -0
- package/skills/tdd-workflow/SKILL.md +413 -0
- package/skills/tdd-workflow/zoho-testing.md +124 -0
- package/skills/tutorial/SKILL.md +249 -0
- package/skills/tutorial/docs/ACCESSIBILITY.md +169 -0
- package/skills/tutorial/lessons/00-philosophy-and-workflow.md +198 -0
- package/skills/tutorial/lessons/01-basics.md +81 -0
- package/skills/tutorial/lessons/02-training.md +86 -0
- package/skills/tutorial/lessons/03-commands.md +109 -0
- package/skills/tutorial/lessons/04-workflows.md +115 -0
- package/skills/tutorial/lessons/05-compliance.md +116 -0
- package/skills/tutorial/lessons/06-zoho.md +121 -0
- package/skills/tutorial/lessons/07-hooks-system.md +277 -0
- package/skills/tutorial/lessons/08-mcp-servers.md +316 -0
- package/skills/tutorial/lessons/09-client-management.md +215 -0
- package/skills/tutorial/lessons/10-testing-e2e.md +260 -0
- package/skills/tutorial/lessons/11-skills-deep-dive.md +272 -0
- package/skills/tutorial/lessons/12-rules-system.md +326 -0
- package/skills/tutorial/lessons/13-golden-standard-graduation.md +213 -0
- package/skills/tutorial/lessons/14-fork-setup-and-sync.md +312 -0
- package/skills/tutorial/lessons/15-living-examples-system.md +221 -0
- package/skills/tutorial/tracks/accelerated/README.md +134 -0
- package/skills/tutorial/tracks/accelerated/assessment/checkpoint-1.md +161 -0
- package/skills/tutorial/tracks/accelerated/assessment/checkpoint-2.md +175 -0
- package/skills/tutorial/tracks/accelerated/day-1-core-concepts.md +234 -0
- package/skills/tutorial/tracks/accelerated/day-2-essential-commands.md +270 -0
- package/skills/tutorial/tracks/accelerated/day-3-workflow-mastery.md +305 -0
- package/skills/tutorial/tracks/accelerated/day-4-compliance-zoho.md +304 -0
- package/skills/tutorial/tracks/accelerated/day-5-hooks-skills.md +344 -0
- package/skills/tutorial/tracks/accelerated/day-6-client-testing.md +386 -0
- package/skills/tutorial/tracks/accelerated/day-7-graduation.md +369 -0
- package/skills/zoho-patterns/CHANGELOG.md +108 -0
- package/skills/zoho-patterns/SKILL.md +446 -0
- package/skills/zoho-patterns/analytics/dashboard-patterns.md +352 -0
- package/skills/zoho-patterns/analytics/zoho-to-bigquery-pipeline.md +427 -0
- package/skills/zoho-patterns/catalyst/appsail-deployment.md +349 -0
- package/skills/zoho-patterns/catalyst/context-close-patterns.md +354 -0
- package/skills/zoho-patterns/catalyst/cron-batch-processing.md +374 -0
- package/skills/zoho-patterns/catalyst/function-patterns.md +439 -0
- package/skills/zoho-patterns/creator/form-design.md +304 -0
- package/skills/zoho-patterns/creator/publish-api-patterns.md +313 -0
- package/skills/zoho-patterns/creator/widget-integration.md +306 -0
- package/skills/zoho-patterns/creator/workflow-automation.md +253 -0
- package/skills/zoho-patterns/deluge/api-patterns.md +468 -0
- package/skills/zoho-patterns/deluge/batch-processing.md +403 -0
- package/skills/zoho-patterns/deluge/cross-app-integration.md +356 -0
- package/skills/zoho-patterns/deluge/error-handling.md +423 -0
- package/skills/zoho-patterns/deluge/syntax-reference.md +65 -0
- package/skills/zoho-patterns/integration/cors-proxy-architecture.md +426 -0
- package/skills/zoho-patterns/integration/crm-books-native-sync.md +277 -0
- package/skills/zoho-patterns/integration/oauth-token-management.md +461 -0
- package/skills/zoho-patterns/integration/zoho-flow-patterns.md +334 -0
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
# Zoho to GCP Data Extraction
|
|
2
|
+
|
|
3
|
+
> Patterns for extracting data from Zoho CRM, Books, Creator, and Analytics into GCP using CData Sync, Catalyst Cron, webhooks, and APIs.
|
|
4
|
+
|
|
5
|
+
## Extraction Method Overview
|
|
6
|
+
|
|
7
|
+
| Method | Latency | Complexity | Best For |
|
|
8
|
+
|--------|---------|------------|----------|
|
|
9
|
+
| CData Sync | 15min-daily | Low | Bulk initial loads, daily syncs |
|
|
10
|
+
| Catalyst Cron | 5min-hourly | Medium | Scheduled incremental pulls |
|
|
11
|
+
| Zoho Webhooks → PubSub | Real-time | High | Critical business events |
|
|
12
|
+
| Zoho Analytics Export | Daily | Low | Pre-built reports/datasets |
|
|
13
|
+
| Bulk API | Hourly-daily | Medium | Large record sets (>10K) |
|
|
14
|
+
|
|
15
|
+
## CData Sync Configuration
|
|
16
|
+
|
|
17
|
+
CData Sync provides a managed connector from Zoho to BigQuery with minimal code.
|
|
18
|
+
|
|
19
|
+
### Connection Setup
|
|
20
|
+
|
|
21
|
+
```yaml
|
|
22
|
+
# cdata_sync_config.yml (conceptual - configured via CData UI)
|
|
23
|
+
connections:
|
|
24
|
+
zoho_crm:
|
|
25
|
+
driver: ZohoCRM
|
|
26
|
+
auth_type: OAuth
|
|
27
|
+
client_id: "${ZOHO_CLIENT_ID}"
|
|
28
|
+
client_secret: "${ZOHO_CLIENT_SECRET}"
|
|
29
|
+
refresh_token: "${ZOHO_REFRESH_TOKEN}"
|
|
30
|
+
api_domain: "https://www.zohoapis.com"
|
|
31
|
+
accounts_server: "https://accounts.zoho.com"
|
|
32
|
+
|
|
33
|
+
bigquery_target:
|
|
34
|
+
driver: BigQuery
|
|
35
|
+
project: cloudstream-prod
|
|
36
|
+
dataset: bronze
|
|
37
|
+
service_account_key: "${GCP_SA_KEY_PATH}"
|
|
38
|
+
location: us-central1
|
|
39
|
+
|
|
40
|
+
sync_jobs:
|
|
41
|
+
- name: zoho_deals_daily
|
|
42
|
+
source: zoho_crm
|
|
43
|
+
target: bigquery_target
|
|
44
|
+
table: Deals
|
|
45
|
+
destination_table: zoho_deals
|
|
46
|
+
schedule: "0 2 * * *" # 2 AM daily
|
|
47
|
+
mode: incremental
|
|
48
|
+
incremental_column: Modified_Time
|
|
49
|
+
batch_size: 5000
|
|
50
|
+
|
|
51
|
+
- name: zoho_contacts_daily
|
|
52
|
+
source: zoho_crm
|
|
53
|
+
target: bigquery_target
|
|
54
|
+
table: Contacts
|
|
55
|
+
destination_table: zoho_contacts
|
|
56
|
+
schedule: "0 2 * * *"
|
|
57
|
+
mode: incremental
|
|
58
|
+
incremental_column: Modified_Time
|
|
59
|
+
|
|
60
|
+
- name: zoho_invoices_daily
|
|
61
|
+
source: zoho_crm # Or Zoho Books connection
|
|
62
|
+
target: bigquery_target
|
|
63
|
+
table: Invoices
|
|
64
|
+
destination_table: zoho_invoices
|
|
65
|
+
schedule: "0 3 * * *" # After deals sync
|
|
66
|
+
mode: incremental
|
|
67
|
+
incremental_column: Modified_Time
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### CData Sync Metadata Columns
|
|
71
|
+
|
|
72
|
+
CData automatically adds these columns - preserve them in bronze:
|
|
73
|
+
|
|
74
|
+
| Column | Description |
|
|
75
|
+
|--------|-------------|
|
|
76
|
+
| `_cdata_sync_id` | Unique sync operation ID |
|
|
77
|
+
| `_cdata_sync_timestamp` | When the record was synced |
|
|
78
|
+
| `_cdata_sync_operation` | INSERT, UPDATE, or DELETE |
|
|
79
|
+
|
|
80
|
+
## Catalyst Cron Function API Pulls
|
|
81
|
+
|
|
82
|
+
### Scheduled Extraction (Every 5 Minutes)
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
# catalyst_cron/zoho_crm_sync/main.py
|
|
86
|
+
"""Catalyst Cron function: Pull modified Zoho CRM records every 5 minutes."""
|
|
87
|
+
|
|
88
|
+
import zcatalyst_sdk
|
|
89
|
+
from google.cloud import bigquery, pubsub_v1
|
|
90
|
+
from datetime import datetime, timedelta
|
|
91
|
+
import requests
|
|
92
|
+
import json
|
|
93
|
+
|
|
94
|
+
# Zoho API configuration
|
|
95
|
+
ZOHO_API_BASE = "https://www.zohoapis.com/crm/v5"
|
|
96
|
+
MODULES = ['Deals', 'Contacts', 'Accounts', 'Tasks']
|
|
97
|
+
|
|
98
|
+
def handler(context, cronDetails):
|
|
99
|
+
"""Catalyst Cron entry point - runs every 5 minutes."""
|
|
100
|
+
app = zcatalyst_sdk.initialize()
|
|
101
|
+
cache = app.cache()
|
|
102
|
+
|
|
103
|
+
for module in MODULES:
|
|
104
|
+
try:
|
|
105
|
+
sync_module(app, cache, module)
|
|
106
|
+
except Exception as e:
|
|
107
|
+
print(f"Error syncing {module}: {e}")
|
|
108
|
+
# Continue with other modules even if one fails
|
|
109
|
+
|
|
110
|
+
def sync_module(app, cache, module_name):
|
|
111
|
+
"""Sync a single Zoho module incrementally."""
|
|
112
|
+
# Get last sync timestamp from Catalyst cache
|
|
113
|
+
segment = cache.get_segment('sync_timestamps')
|
|
114
|
+
last_sync = segment.get(f'{module_name}_last_sync')
|
|
115
|
+
|
|
116
|
+
if last_sync:
|
|
117
|
+
last_sync_time = last_sync['value']
|
|
118
|
+
else:
|
|
119
|
+
# First run: get last 24 hours
|
|
120
|
+
last_sync_time = (datetime.utcnow() - timedelta(hours=24)).strftime('%Y-%m-%dT%H:%M:%S+00:00')
|
|
121
|
+
|
|
122
|
+
# Fetch modified records from Zoho
|
|
123
|
+
access_token = get_zoho_token(app)
|
|
124
|
+
records = fetch_modified_records(access_token, module_name, last_sync_time)
|
|
125
|
+
|
|
126
|
+
if records:
|
|
127
|
+
# Publish to PubSub for GCP ingestion
|
|
128
|
+
publish_to_pubsub(records, module_name)
|
|
129
|
+
|
|
130
|
+
# Update last sync timestamp
|
|
131
|
+
segment.put(f'{module_name}_last_sync', {
|
|
132
|
+
'value': datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S+00:00')
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
print(f"Synced {len(records)} {module_name} records")
|
|
136
|
+
|
|
137
|
+
def fetch_modified_records(token, module, since_time):
|
|
138
|
+
"""Fetch records modified since last sync."""
|
|
139
|
+
headers = {"Authorization": f"Zoho-oauthtoken {token}"}
|
|
140
|
+
records = []
|
|
141
|
+
page = 1
|
|
142
|
+
has_more = True
|
|
143
|
+
|
|
144
|
+
while has_more:
|
|
145
|
+
params = {
|
|
146
|
+
'modified_since': since_time,
|
|
147
|
+
'page': page,
|
|
148
|
+
'per_page': 200, # Max per page
|
|
149
|
+
'sort_by': 'Modified_Time',
|
|
150
|
+
'sort_order': 'asc'
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
response = requests.get(
|
|
154
|
+
f"{ZOHO_API_BASE}/{module}",
|
|
155
|
+
headers=headers,
|
|
156
|
+
params=params
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
if response.status_code == 200:
|
|
160
|
+
data = response.json()
|
|
161
|
+
records.extend(data.get('data', []))
|
|
162
|
+
has_more = data.get('info', {}).get('more_records', False)
|
|
163
|
+
page += 1
|
|
164
|
+
elif response.status_code == 204:
|
|
165
|
+
# No records modified
|
|
166
|
+
has_more = False
|
|
167
|
+
else:
|
|
168
|
+
raise Exception(f"Zoho API error: {response.status_code} - {response.text}")
|
|
169
|
+
|
|
170
|
+
return records
|
|
171
|
+
|
|
172
|
+
def publish_to_pubsub(records, module_name):
|
|
173
|
+
"""Publish records to PubSub for downstream processing."""
|
|
174
|
+
publisher = pubsub_v1.PublisherClient()
|
|
175
|
+
topic_path = publisher.topic_path('cloudstream-prod', 'zoho-ingestion')
|
|
176
|
+
|
|
177
|
+
for record in records:
|
|
178
|
+
message = json.dumps({
|
|
179
|
+
'module': module_name,
|
|
180
|
+
'record': record,
|
|
181
|
+
'extracted_at': datetime.utcnow().isoformat(),
|
|
182
|
+
'source': 'catalyst_cron'
|
|
183
|
+
}).encode('utf-8')
|
|
184
|
+
|
|
185
|
+
publisher.publish(
|
|
186
|
+
topic_path,
|
|
187
|
+
message,
|
|
188
|
+
module=module_name,
|
|
189
|
+
record_id=str(record.get('id', ''))
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
def get_zoho_token(app):
|
|
193
|
+
"""Get fresh OAuth token using refresh token from Catalyst vault."""
|
|
194
|
+
# Store refresh token in Catalyst Vault (not in code)
|
|
195
|
+
connector = app.connection()
|
|
196
|
+
token = connector.get_connector('zoho_crm').execute_request(
|
|
197
|
+
method='POST',
|
|
198
|
+
url='https://accounts.zoho.com/oauth/v2/token',
|
|
199
|
+
params={
|
|
200
|
+
'grant_type': 'refresh_token',
|
|
201
|
+
'refresh_token': app.vault().get_secret('zoho_refresh_token'),
|
|
202
|
+
'client_id': app.vault().get_secret('zoho_client_id'),
|
|
203
|
+
'client_secret': app.vault().get_secret('zoho_client_secret')
|
|
204
|
+
}
|
|
205
|
+
)
|
|
206
|
+
return token['access_token']
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Webhook-Triggered Ingestion
|
|
210
|
+
|
|
211
|
+
### Zoho Flow to PubSub
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
# cloud_function/zoho_webhook_receiver/main.py
|
|
215
|
+
"""Cloud Function: Receive Zoho webhooks and publish to PubSub."""
|
|
216
|
+
|
|
217
|
+
import functions_framework
|
|
218
|
+
from google.cloud import pubsub_v1
|
|
219
|
+
import json
|
|
220
|
+
import hashlib
|
|
221
|
+
import hmac
|
|
222
|
+
from datetime import datetime
|
|
223
|
+
|
|
224
|
+
WEBHOOK_SECRET = "your-webhook-secret" # Store in Secret Manager
|
|
225
|
+
PROJECT_ID = "cloudstream-prod"
|
|
226
|
+
TOPIC_ID = "zoho-webhooks"
|
|
227
|
+
|
|
228
|
+
@functions_framework.http
|
|
229
|
+
def receive_webhook(request):
|
|
230
|
+
"""HTTP endpoint for Zoho workflow/Flow webhooks."""
|
|
231
|
+
# Verify webhook signature
|
|
232
|
+
if not verify_signature(request):
|
|
233
|
+
return ('Unauthorized', 401)
|
|
234
|
+
|
|
235
|
+
payload = request.get_json(force=True)
|
|
236
|
+
|
|
237
|
+
# Extract event metadata
|
|
238
|
+
event = {
|
|
239
|
+
'module': payload.get('module', 'unknown'),
|
|
240
|
+
'event_type': payload.get('event', 'unknown'), # create, update, delete
|
|
241
|
+
'record_id': payload.get('id', ''),
|
|
242
|
+
'record_data': payload.get('data', {}),
|
|
243
|
+
'triggered_at': datetime.utcnow().isoformat(),
|
|
244
|
+
'source': 'zoho_webhook'
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
# Publish to PubSub
|
|
248
|
+
publisher = pubsub_v1.PublisherClient()
|
|
249
|
+
topic_path = publisher.topic_path(PROJECT_ID, TOPIC_ID)
|
|
250
|
+
|
|
251
|
+
future = publisher.publish(
|
|
252
|
+
topic_path,
|
|
253
|
+
json.dumps(event).encode('utf-8'),
|
|
254
|
+
module=event['module'],
|
|
255
|
+
event_type=event['event_type'],
|
|
256
|
+
record_id=event['record_id']
|
|
257
|
+
)
|
|
258
|
+
message_id = future.result()
|
|
259
|
+
|
|
260
|
+
return (json.dumps({'status': 'ok', 'message_id': message_id}), 200)
|
|
261
|
+
|
|
262
|
+
def verify_signature(request):
|
|
263
|
+
"""Verify Zoho webhook HMAC signature."""
|
|
264
|
+
signature = request.headers.get('X-Zoho-Webhook-Signature', '')
|
|
265
|
+
body = request.get_data()
|
|
266
|
+
expected = hmac.new(
|
|
267
|
+
WEBHOOK_SECRET.encode(), body, hashlib.sha256
|
|
268
|
+
).hexdigest()
|
|
269
|
+
return hmac.compare_digest(signature, expected)
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Zoho Flow Configuration
|
|
273
|
+
|
|
274
|
+
```
|
|
275
|
+
Zoho Flow Trigger:
|
|
276
|
+
Module: Deals
|
|
277
|
+
Events: Create, Update, Delete
|
|
278
|
+
Action: Webhook POST to:
|
|
279
|
+
URL: https://us-central1-cloudstream-prod.cloudfunctions.net/zoho-webhook-receiver
|
|
280
|
+
Headers:
|
|
281
|
+
Content-Type: application/json
|
|
282
|
+
X-Zoho-Webhook-Signature: ${HMAC_SHA256(payload, secret)}
|
|
283
|
+
Body: ${record_data}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Zoho Analytics Export API
|
|
287
|
+
|
|
288
|
+
```python
|
|
289
|
+
# scripts/zoho_analytics_export.py
|
|
290
|
+
"""Export datasets from Zoho Analytics for comparison/validation."""
|
|
291
|
+
|
|
292
|
+
import requests
|
|
293
|
+
from google.cloud import storage
|
|
294
|
+
import json
|
|
295
|
+
|
|
296
|
+
ZOHO_ANALYTICS_API = "https://analyticsapi.zoho.com/api"
|
|
297
|
+
ORG_ID = "your-org-id"
|
|
298
|
+
WORKSPACE = "your-workspace"
|
|
299
|
+
|
|
300
|
+
def export_zoho_analytics_table(table_name, format='json'):
|
|
301
|
+
"""Export a Zoho Analytics table/view to GCS."""
|
|
302
|
+
token = get_analytics_token()
|
|
303
|
+
|
|
304
|
+
response = requests.get(
|
|
305
|
+
f"{ZOHO_ANALYTICS_API}/{ORG_ID}/{WORKSPACE}/{table_name}",
|
|
306
|
+
headers={"Authorization": f"Zoho-oauthtoken {token}"},
|
|
307
|
+
params={
|
|
308
|
+
'ZOHO_ACTION': 'EXPORT',
|
|
309
|
+
'ZOHO_OUTPUT_FORMAT': format.upper(),
|
|
310
|
+
'ZOHO_ERROR_FORMAT': 'JSON',
|
|
311
|
+
'ZOHO_API_VERSION': '1.0'
|
|
312
|
+
}
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
if response.status_code == 200:
|
|
316
|
+
# Upload to GCS
|
|
317
|
+
client = storage.Client()
|
|
318
|
+
bucket = client.bucket('cloudstream-landing')
|
|
319
|
+
blob = bucket.blob(
|
|
320
|
+
f'zoho-analytics/{table_name}/{datetime.now().strftime("%Y/%m/%d")}/export.{format}'
|
|
321
|
+
)
|
|
322
|
+
blob.upload_from_string(response.content)
|
|
323
|
+
print(f"Exported {table_name} to GCS")
|
|
324
|
+
else:
|
|
325
|
+
raise Exception(f"Export failed: {response.status_code}")
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
## Bulk API vs Single Record
|
|
329
|
+
|
|
330
|
+
### Decision Matrix
|
|
331
|
+
|
|
332
|
+
| Factor | Bulk API | Single Record API |
|
|
333
|
+
|--------|----------|-------------------|
|
|
334
|
+
| Records per call | Up to 200 (insert), 100 (read) | 1 |
|
|
335
|
+
| Rate limit impact | 1 API call = 1 credit | 1 API call = 1 credit |
|
|
336
|
+
| Latency | Higher (batching overhead) | Lower |
|
|
337
|
+
| Error handling | Partial success possible | All or nothing |
|
|
338
|
+
| Best for | Initial load, daily sync | Webhooks, real-time |
|
|
339
|
+
| Max daily calls | Based on plan | Based on plan |
|
|
340
|
+
|
|
341
|
+
### Bulk Read Example
|
|
342
|
+
|
|
343
|
+
```python
|
|
344
|
+
def bulk_read_zoho_module(module, criteria=None, page=1):
|
|
345
|
+
"""Read up to 200 records per page using bulk read."""
|
|
346
|
+
headers = {"Authorization": f"Zoho-oauthtoken {get_token()}"}
|
|
347
|
+
|
|
348
|
+
# Submit bulk read job
|
|
349
|
+
body = {
|
|
350
|
+
"query": {
|
|
351
|
+
"module": {"api_name": module},
|
|
352
|
+
"page": page,
|
|
353
|
+
"per_page": 200
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if criteria:
|
|
357
|
+
body["query"]["criteria"] = criteria
|
|
358
|
+
|
|
359
|
+
response = requests.post(
|
|
360
|
+
f"{ZOHO_API_BASE}/bulk/read",
|
|
361
|
+
headers=headers,
|
|
362
|
+
json=body
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
job_id = response.json()['data'][0]['details']['id']
|
|
366
|
+
|
|
367
|
+
# Poll for completion
|
|
368
|
+
while True:
|
|
369
|
+
status = requests.get(
|
|
370
|
+
f"{ZOHO_API_BASE}/bulk/read/{job_id}",
|
|
371
|
+
headers=headers
|
|
372
|
+
).json()
|
|
373
|
+
|
|
374
|
+
state = status['data'][0]['state']
|
|
375
|
+
if state == 'COMPLETED':
|
|
376
|
+
# Download result file
|
|
377
|
+
download_url = status['data'][0]['result']['download_url']
|
|
378
|
+
return requests.get(download_url, headers=headers).content
|
|
379
|
+
elif state == 'FAILED':
|
|
380
|
+
raise Exception(f"Bulk read failed: {status}")
|
|
381
|
+
time.sleep(5)
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
## Rate Limiting Awareness
|
|
385
|
+
|
|
386
|
+
> **WARNING**: Zoho API rate limits vary significantly by plan. Exceeding limits returns HTTP 429 and may temporarily block API access.
|
|
387
|
+
|
|
388
|
+
| Zoho Plan | API Credits/Day | Credits/Minute | Concurrent Requests |
|
|
389
|
+
|-----------|----------------|----------------|---------------------|
|
|
390
|
+
| Free | 5,000 | 100 | 5 |
|
|
391
|
+
| Standard | 10,000 | 150 | 10 |
|
|
392
|
+
| Professional | 15,000 | 200 | 15 |
|
|
393
|
+
| Enterprise | 25,000 | 250 | 25 |
|
|
394
|
+
| Ultimate | 50,000 | 500 | 25 |
|
|
395
|
+
|
|
396
|
+
```python
|
|
397
|
+
# Rate limiting implementation
|
|
398
|
+
import time
|
|
399
|
+
from functools import wraps
|
|
400
|
+
|
|
401
|
+
class ZohoRateLimiter:
|
|
402
|
+
"""Rate limiter for Zoho API calls."""
|
|
403
|
+
|
|
404
|
+
def __init__(self, calls_per_minute=100, daily_limit=10000):
|
|
405
|
+
self.calls_per_minute = calls_per_minute
|
|
406
|
+
self.daily_limit = daily_limit
|
|
407
|
+
self.minute_calls = []
|
|
408
|
+
self.daily_count = 0
|
|
409
|
+
|
|
410
|
+
def wait_if_needed(self):
|
|
411
|
+
"""Block if rate limit would be exceeded."""
|
|
412
|
+
now = time.time()
|
|
413
|
+
|
|
414
|
+
# Clean old entries
|
|
415
|
+
self.minute_calls = [t for t in self.minute_calls if now - t < 60]
|
|
416
|
+
|
|
417
|
+
if len(self.minute_calls) >= self.calls_per_minute:
|
|
418
|
+
sleep_time = 60 - (now - self.minute_calls[0])
|
|
419
|
+
print(f"Rate limit approaching, sleeping {sleep_time:.1f}s")
|
|
420
|
+
time.sleep(sleep_time)
|
|
421
|
+
|
|
422
|
+
if self.daily_count >= self.daily_limit * 0.9: # 90% threshold warning
|
|
423
|
+
print(f"WARNING: {self.daily_count}/{self.daily_limit} daily API credits used")
|
|
424
|
+
|
|
425
|
+
self.minute_calls.append(now)
|
|
426
|
+
self.daily_count += 1
|
|
427
|
+
|
|
428
|
+
rate_limiter = ZohoRateLimiter(calls_per_minute=80, daily_limit=10000) # 80% of limit
|
|
429
|
+
|
|
430
|
+
def rate_limited_request(url, headers, params=None):
|
|
431
|
+
"""Make a rate-limited request to Zoho API."""
|
|
432
|
+
rate_limiter.wait_if_needed()
|
|
433
|
+
response = requests.get(url, headers=headers, params=params)
|
|
434
|
+
|
|
435
|
+
if response.status_code == 429:
|
|
436
|
+
# Hit rate limit - back off significantly
|
|
437
|
+
retry_after = int(response.headers.get('Retry-After', 60))
|
|
438
|
+
print(f"Rate limited! Waiting {retry_after}s")
|
|
439
|
+
time.sleep(retry_after)
|
|
440
|
+
return rate_limited_request(url, headers, params)
|
|
441
|
+
|
|
442
|
+
return response
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
## Incremental Extraction (modified_time Filter)
|
|
446
|
+
|
|
447
|
+
```python
|
|
448
|
+
def incremental_extract(module, last_modified_time):
|
|
449
|
+
"""Extract only records modified since last sync."""
|
|
450
|
+
headers = {"Authorization": f"Zoho-oauthtoken {get_token()}"}
|
|
451
|
+
|
|
452
|
+
all_records = []
|
|
453
|
+
page = 1
|
|
454
|
+
has_more = True
|
|
455
|
+
|
|
456
|
+
while has_more:
|
|
457
|
+
response = rate_limited_request(
|
|
458
|
+
f"{ZOHO_API_BASE}/{module}",
|
|
459
|
+
headers=headers,
|
|
460
|
+
params={
|
|
461
|
+
'modified_since': last_modified_time, # ISO 8601 format
|
|
462
|
+
'page': page,
|
|
463
|
+
'per_page': 200,
|
|
464
|
+
'sort_by': 'Modified_Time',
|
|
465
|
+
'sort_order': 'asc',
|
|
466
|
+
'fields': 'id,Modified_Time,Created_Time,Deal_Name,Amount,Stage,Owner'
|
|
467
|
+
}
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
if response.status_code == 200:
|
|
471
|
+
data = response.json()
|
|
472
|
+
all_records.extend(data.get('data', []))
|
|
473
|
+
has_more = data.get('info', {}).get('more_records', False)
|
|
474
|
+
page += 1
|
|
475
|
+
elif response.status_code == 204:
|
|
476
|
+
has_more = False # No modified records
|
|
477
|
+
else:
|
|
478
|
+
raise Exception(f"API error: {response.status_code}")
|
|
479
|
+
|
|
480
|
+
return all_records
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
## Full Refresh Patterns
|
|
484
|
+
|
|
485
|
+
```python
|
|
486
|
+
def full_refresh(module, destination_table):
|
|
487
|
+
"""Full table replacement - use sparingly due to API credit cost."""
|
|
488
|
+
print(f"Starting full refresh for {module}")
|
|
489
|
+
|
|
490
|
+
all_records = []
|
|
491
|
+
page = 1
|
|
492
|
+
has_more = True
|
|
493
|
+
|
|
494
|
+
while has_more:
|
|
495
|
+
response = rate_limited_request(
|
|
496
|
+
f"{ZOHO_API_BASE}/{module}",
|
|
497
|
+
headers={"Authorization": f"Zoho-oauthtoken {get_token()}"},
|
|
498
|
+
params={'page': page, 'per_page': 200}
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
if response.status_code == 200:
|
|
502
|
+
data = response.json()
|
|
503
|
+
all_records.extend(data.get('data', []))
|
|
504
|
+
has_more = data.get('info', {}).get('more_records', False)
|
|
505
|
+
page += 1
|
|
506
|
+
else:
|
|
507
|
+
has_more = False
|
|
508
|
+
|
|
509
|
+
# Load to BigQuery with WRITE_TRUNCATE (full replace)
|
|
510
|
+
client = bigquery.Client()
|
|
511
|
+
job_config = bigquery.LoadJobConfig(
|
|
512
|
+
write_disposition=bigquery.WriteDisposition.WRITE_TRUNCATE,
|
|
513
|
+
source_format=bigquery.SourceFormat.NEWLINE_DELIMITED_JSON
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
# Write to temp file, then load
|
|
517
|
+
import tempfile
|
|
518
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.jsonl') as f:
|
|
519
|
+
for record in all_records:
|
|
520
|
+
f.write(json.dumps(record) + '\n')
|
|
521
|
+
f.flush()
|
|
522
|
+
|
|
523
|
+
with open(f.name, 'rb') as source:
|
|
524
|
+
job = client.load_table_from_file(
|
|
525
|
+
source, destination_table, job_config=job_config
|
|
526
|
+
)
|
|
527
|
+
job.result()
|
|
528
|
+
|
|
529
|
+
print(f"Full refresh complete: {len(all_records)} records loaded to {destination_table}")
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
> **COST TIP**: A full refresh of 50,000 records costs 250 API credits (200 records/page). Do full refreshes weekly at most; use incremental extraction for daily syncs.
|
|
533
|
+
|
|
534
|
+
## Best Practices
|
|
535
|
+
|
|
536
|
+
1. **Always use incremental extraction** for daily operations - saves API credits
|
|
537
|
+
2. **Store refresh tokens in Secret Manager** - never in code or environment variables
|
|
538
|
+
3. **Implement rate limiting client-side** - do not rely on 429 responses
|
|
539
|
+
4. **Use PubSub as buffer** - decouples extraction speed from load speed
|
|
540
|
+
5. **Schedule full refreshes weekly** - catches any missed incremental updates
|
|
541
|
+
6. **Monitor API credit usage** - set alerts at 70% daily consumption
|
|
542
|
+
7. **Use bulk API for initial loads** - more efficient than single-record pagination
|
|
543
|
+
8. **Preserve all Zoho metadata** - `Modified_Time`, `Created_Time`, `Owner` are critical for silver layer
|