autotouch-cli 0.2.21__tar.gz → 0.2.22__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/PKG-INFO +57 -1
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/autotouch_cli.egg-info/PKG-INFO +57 -1
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/autotouch_cli.egg-info/SOURCES.txt +1 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/docs/research-table/reference/autotouch-cli-pypi.md +56 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/pyproject.toml +1 -1
- autotouch_cli-0.2.22/scripts/migrations/20260305_force_formatter_autorun_on_source_update.py +98 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/smart_table_cli.py +417 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/autotouch_cli.egg-info/dependency_links.txt +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/autotouch_cli.egg-info/entry_points.txt +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/autotouch_cli.egg-info/requires.txt +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/autotouch_cli.egg-info/top_level.txt +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/__init__.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/add_column_unique_index.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/attach_csv_import_leads_to_research_table.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/bundle_sequences_backend.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/check_agent_traces.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/check_column_mode.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/exit_terminal_leads_from_sequences.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/fetch_lead.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/fix_lead_titles_from_csv.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/migrations/20250106_add_column_position.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/migrations/20250108_fix_legacy_column_fields.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/migrations/20250109_add_user_fields_to_tables.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/migrations/20250117_add_call_logs_webhook_indexes.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/migrations/20250117_rename_call_logs_collection.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/migrations/20250119_create_leads_unique_email_index.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/migrations/20250123_add_filter_indexes.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/migrations/20250123_add_llm_responses_collection.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/migrations/20250128_migrate_user_ids_to_objectid.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/migrations/20250208_backfill_task_research_values.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/migrations/20250604_add_origin_indexes.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/migrations/20250608_cleanup_agent_metadata.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/migrations/20250608_rename_agent_metadata_to_metadata.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/migrations/20250922_add_activity_indexes.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/migrations/20250926_migrate_single_to_arrays.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/migrations/20250928_add_missing_timestamp_fields.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/migrations/20250929_add_task_join_indexes.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/migrations/20250929_add_task_join_indexes_safe.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/migrations/20250929_create_shared_phone_cache.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/migrations/20251007_add_rows_position_id_index.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/migrations/20251109_add_ttl_for_llm_and_preview_traces.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/migrations/20260113_normalize_table_filter_operators.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/migrations/20260113_set_user_permissions_user_admin.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/migrations/20260204_sync_lead_owner_from_tasks.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/migrations/20260303_add_webhook_subscription_collections.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/migrations/migrate_org_user_credits.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/migrations/set_default_lead_status.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/migrations/update_lead_owner_from_tasks.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/reassign_sequence_owner.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/run_sidecar_orchestrator_demo.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/test_crm_company_policy.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/test_sequences_instantly_e2e.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/test_sequences_personal_e2e.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/test_task_error_logger.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/verify_azurite_voicemail.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/setup.cfg +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/tests/test_contactout_custom.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/tests/test_contactout_integration.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/tests/test_contactout_multi_titles.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/tests/test_contactout_pipeline.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/tests/test_contactout_simple.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/tests/test_contactout_v2_bulk.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/tests/test_lead_required_fields.py +0 -0
- {autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/tests/test_phone_provider_pipeline.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: autotouch-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.22
|
|
4
4
|
Summary: Autotouch Smart Table CLI
|
|
5
5
|
Requires-Python: >=3.9
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -102,6 +102,11 @@ Notes:
|
|
|
102
102
|
- `--wait` polls import status to terminal state.
|
|
103
103
|
- `--checkpoint-file` stores state and blocks accidental duplicate re-imports.
|
|
104
104
|
- `--validate-only` validates CSV parse/shape on server without writing rows.
|
|
105
|
+
- Blacklist controls on optimized import:
|
|
106
|
+
- company-domain filtering is ON by default
|
|
107
|
+
- email filtering is OFF by default
|
|
108
|
+
- disable company filtering with `--no-check-company-blacklist`
|
|
109
|
+
- enable email filtering with `--check-email-blacklist`
|
|
105
110
|
- Safety assertions:
|
|
106
111
|
- `--expected-rows <N>`
|
|
107
112
|
- `--require-columns col_a,col_b`
|
|
@@ -109,6 +114,43 @@ Notes:
|
|
|
109
114
|
- `--require-non-empty key:ratio`
|
|
110
115
|
- For async imports with `--wait`, assertions are rechecked postflight via task manifest verification.
|
|
111
116
|
- Use `--allow-reimport` if you intentionally want to run the same file again.
|
|
117
|
+
- Import responses include `blacklist_summary` with company/email `skipped_count` and `enforced` flags.
|
|
118
|
+
|
|
119
|
+
Example enabling both company and email enforcement:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
autotouch rows import-csv \
|
|
123
|
+
--table-id <TABLE_ID> \
|
|
124
|
+
--confirm-table-id <TABLE_ID> \
|
|
125
|
+
--file contacts.csv \
|
|
126
|
+
--checkpoint-file .autotouch-import.json \
|
|
127
|
+
--check-email-blacklist \
|
|
128
|
+
--wait \
|
|
129
|
+
--output json
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Manage blacklist entries (native CLI, admin identity required):
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
# List current entries
|
|
136
|
+
autotouch blacklist list --type-filter all --limit 100
|
|
137
|
+
|
|
138
|
+
# Add entries
|
|
139
|
+
autotouch blacklist add --type domain --value competitor.com --reason "Do not contact"
|
|
140
|
+
autotouch blacklist add --type email --value blocked@example.com --reason "Unsubscribed"
|
|
141
|
+
|
|
142
|
+
# Bulk-import entries from CSV/TXT
|
|
143
|
+
autotouch blacklist import --file blacklist.csv
|
|
144
|
+
|
|
145
|
+
# Check/filter email sets (auto-chunked for large lists)
|
|
146
|
+
autotouch blacklist check --emails-file recipients.csv --emails-column email --summary-only
|
|
147
|
+
autotouch blacklist filter --emails-file recipients.csv --emails-column email --summary-only
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Recommended timing:
|
|
151
|
+
- Add suppressions early (unsubscribed addresses, existing customers, competitors, and clear non-ICP domains).
|
|
152
|
+
- Before billable enrichments (`llm_enrichment`, `email_finder`, `phone_finder`), run blacklist filtering and enrich only clean candidates.
|
|
153
|
+
- Run one final blacklist check/filter before outreach execution.
|
|
112
154
|
|
|
113
155
|
## Import modes
|
|
114
156
|
|
|
@@ -140,6 +182,7 @@ Auto-run is configured per column (`autoRun`), not per table.
|
|
|
140
182
|
Important:
|
|
141
183
|
- Insert events do not run `onSourceUpdate` columns.
|
|
142
184
|
- Imports may queue auto-run dispatch evaluation, but only columns whose `autoRun` policy matches the event are queued.
|
|
185
|
+
- Formatter columns are normalized server-side to `onSourceUpdate` (attempted `never`/`onInsert` values are overridden).
|
|
143
186
|
|
|
144
187
|
## Create a column
|
|
145
188
|
|
|
@@ -164,6 +207,7 @@ Notes:
|
|
|
164
207
|
- `add_to_crm` is optional and non-billable.
|
|
165
208
|
- Email/phone enrichment does not require creating/running `add_to_crm`.
|
|
166
209
|
- For `add_to_crm`, required mapping keys are `linkedinUrl` and `companyDomain`.
|
|
210
|
+
- Formatter formulas must use row references (`row['first_name']`, `row.last_name`), not bare template placeholders like ``${first_name}``.
|
|
167
211
|
- `sync_to_table` supports both:
|
|
168
212
|
- single destination: `destinationTableId` + `columnMappings`
|
|
169
213
|
- router mode: `routes[]` (first matching route wins)
|
|
@@ -171,6 +215,7 @@ Notes:
|
|
|
171
215
|
- `sequenceId`
|
|
172
216
|
- `sourceLeadColumn` pointing to a lead-id producing column (`add_to_crm` or `lead_finder` output)
|
|
173
217
|
- auto-attaches research context defaults during enrollment (`source_table_id`, plus optional table name); favorite fields resolve from current starred columns when explicit `fieldIds` are not provided
|
|
218
|
+
- star/favorite high-signal fields so call-sidecar context stays quick to scan and AI draft context stays focused
|
|
174
219
|
|
|
175
220
|
If you create custom `llm_enrichment` schemas:
|
|
176
221
|
- Use strict field-map schema shape (no root `type/properties` wrapper).
|
|
@@ -227,6 +272,8 @@ Design guidance:
|
|
|
227
272
|
- default behavior for agents: write full context/content, not summaries or truncation
|
|
228
273
|
- only truncate/summarize when there is a hard limit (storage/model/provider/payload), and explicitly mark truncation
|
|
229
274
|
- keep intent separate from raw context so downstream logic can change without losing source data
|
|
275
|
+
- for calling workflows, favorite/star key fields so users can access the most relevant context quickly in sidecar views
|
|
276
|
+
- for AI-generated emails/copy, include both intent and full context in imports so drafts are grounded, not generic
|
|
230
277
|
- keep one entity per row and dedupe on a stable identity key
|
|
231
278
|
- keep snippets/summaries optional and derived from raw context, never a replacement for it
|
|
232
279
|
- normalize intent labels if you need deterministic automation
|
|
@@ -516,6 +563,15 @@ autotouch columns run-next \
|
|
|
516
563
|
--wait
|
|
517
564
|
```
|
|
518
565
|
|
|
566
|
+
Blacklist gate (recommended before billable runs):
|
|
567
|
+
|
|
568
|
+
```bash
|
|
569
|
+
autotouch blacklist filter \
|
|
570
|
+
--emails-file candidates.csv \
|
|
571
|
+
--emails-column work_email \
|
|
572
|
+
--output json
|
|
573
|
+
```
|
|
574
|
+
|
|
519
575
|
## Job truth contract (agent-safe)
|
|
520
576
|
|
|
521
577
|
- Treat a run as started only when you receive `job_id`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: autotouch-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.22
|
|
4
4
|
Summary: Autotouch Smart Table CLI
|
|
5
5
|
Requires-Python: >=3.9
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -102,6 +102,11 @@ Notes:
|
|
|
102
102
|
- `--wait` polls import status to terminal state.
|
|
103
103
|
- `--checkpoint-file` stores state and blocks accidental duplicate re-imports.
|
|
104
104
|
- `--validate-only` validates CSV parse/shape on server without writing rows.
|
|
105
|
+
- Blacklist controls on optimized import:
|
|
106
|
+
- company-domain filtering is ON by default
|
|
107
|
+
- email filtering is OFF by default
|
|
108
|
+
- disable company filtering with `--no-check-company-blacklist`
|
|
109
|
+
- enable email filtering with `--check-email-blacklist`
|
|
105
110
|
- Safety assertions:
|
|
106
111
|
- `--expected-rows <N>`
|
|
107
112
|
- `--require-columns col_a,col_b`
|
|
@@ -109,6 +114,43 @@ Notes:
|
|
|
109
114
|
- `--require-non-empty key:ratio`
|
|
110
115
|
- For async imports with `--wait`, assertions are rechecked postflight via task manifest verification.
|
|
111
116
|
- Use `--allow-reimport` if you intentionally want to run the same file again.
|
|
117
|
+
- Import responses include `blacklist_summary` with company/email `skipped_count` and `enforced` flags.
|
|
118
|
+
|
|
119
|
+
Example enabling both company and email enforcement:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
autotouch rows import-csv \
|
|
123
|
+
--table-id <TABLE_ID> \
|
|
124
|
+
--confirm-table-id <TABLE_ID> \
|
|
125
|
+
--file contacts.csv \
|
|
126
|
+
--checkpoint-file .autotouch-import.json \
|
|
127
|
+
--check-email-blacklist \
|
|
128
|
+
--wait \
|
|
129
|
+
--output json
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Manage blacklist entries (native CLI, admin identity required):
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
# List current entries
|
|
136
|
+
autotouch blacklist list --type-filter all --limit 100
|
|
137
|
+
|
|
138
|
+
# Add entries
|
|
139
|
+
autotouch blacklist add --type domain --value competitor.com --reason "Do not contact"
|
|
140
|
+
autotouch blacklist add --type email --value blocked@example.com --reason "Unsubscribed"
|
|
141
|
+
|
|
142
|
+
# Bulk-import entries from CSV/TXT
|
|
143
|
+
autotouch blacklist import --file blacklist.csv
|
|
144
|
+
|
|
145
|
+
# Check/filter email sets (auto-chunked for large lists)
|
|
146
|
+
autotouch blacklist check --emails-file recipients.csv --emails-column email --summary-only
|
|
147
|
+
autotouch blacklist filter --emails-file recipients.csv --emails-column email --summary-only
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Recommended timing:
|
|
151
|
+
- Add suppressions early (unsubscribed addresses, existing customers, competitors, and clear non-ICP domains).
|
|
152
|
+
- Before billable enrichments (`llm_enrichment`, `email_finder`, `phone_finder`), run blacklist filtering and enrich only clean candidates.
|
|
153
|
+
- Run one final blacklist check/filter before outreach execution.
|
|
112
154
|
|
|
113
155
|
## Import modes
|
|
114
156
|
|
|
@@ -140,6 +182,7 @@ Auto-run is configured per column (`autoRun`), not per table.
|
|
|
140
182
|
Important:
|
|
141
183
|
- Insert events do not run `onSourceUpdate` columns.
|
|
142
184
|
- Imports may queue auto-run dispatch evaluation, but only columns whose `autoRun` policy matches the event are queued.
|
|
185
|
+
- Formatter columns are normalized server-side to `onSourceUpdate` (attempted `never`/`onInsert` values are overridden).
|
|
143
186
|
|
|
144
187
|
## Create a column
|
|
145
188
|
|
|
@@ -164,6 +207,7 @@ Notes:
|
|
|
164
207
|
- `add_to_crm` is optional and non-billable.
|
|
165
208
|
- Email/phone enrichment does not require creating/running `add_to_crm`.
|
|
166
209
|
- For `add_to_crm`, required mapping keys are `linkedinUrl` and `companyDomain`.
|
|
210
|
+
- Formatter formulas must use row references (`row['first_name']`, `row.last_name`), not bare template placeholders like ``${first_name}``.
|
|
167
211
|
- `sync_to_table` supports both:
|
|
168
212
|
- single destination: `destinationTableId` + `columnMappings`
|
|
169
213
|
- router mode: `routes[]` (first matching route wins)
|
|
@@ -171,6 +215,7 @@ Notes:
|
|
|
171
215
|
- `sequenceId`
|
|
172
216
|
- `sourceLeadColumn` pointing to a lead-id producing column (`add_to_crm` or `lead_finder` output)
|
|
173
217
|
- auto-attaches research context defaults during enrollment (`source_table_id`, plus optional table name); favorite fields resolve from current starred columns when explicit `fieldIds` are not provided
|
|
218
|
+
- star/favorite high-signal fields so call-sidecar context stays quick to scan and AI draft context stays focused
|
|
174
219
|
|
|
175
220
|
If you create custom `llm_enrichment` schemas:
|
|
176
221
|
- Use strict field-map schema shape (no root `type/properties` wrapper).
|
|
@@ -227,6 +272,8 @@ Design guidance:
|
|
|
227
272
|
- default behavior for agents: write full context/content, not summaries or truncation
|
|
228
273
|
- only truncate/summarize when there is a hard limit (storage/model/provider/payload), and explicitly mark truncation
|
|
229
274
|
- keep intent separate from raw context so downstream logic can change without losing source data
|
|
275
|
+
- for calling workflows, favorite/star key fields so users can access the most relevant context quickly in sidecar views
|
|
276
|
+
- for AI-generated emails/copy, include both intent and full context in imports so drafts are grounded, not generic
|
|
230
277
|
- keep one entity per row and dedupe on a stable identity key
|
|
231
278
|
- keep snippets/summaries optional and derived from raw context, never a replacement for it
|
|
232
279
|
- normalize intent labels if you need deterministic automation
|
|
@@ -516,6 +563,15 @@ autotouch columns run-next \
|
|
|
516
563
|
--wait
|
|
517
564
|
```
|
|
518
565
|
|
|
566
|
+
Blacklist gate (recommended before billable runs):
|
|
567
|
+
|
|
568
|
+
```bash
|
|
569
|
+
autotouch blacklist filter \
|
|
570
|
+
--emails-file candidates.csv \
|
|
571
|
+
--emails-column work_email \
|
|
572
|
+
--output json
|
|
573
|
+
```
|
|
574
|
+
|
|
519
575
|
## Job truth contract (agent-safe)
|
|
520
576
|
|
|
521
577
|
- Treat a run as started only when you receive `job_id`.
|
|
@@ -48,6 +48,7 @@ scripts/migrations/20260113_normalize_table_filter_operators.py
|
|
|
48
48
|
scripts/migrations/20260113_set_user_permissions_user_admin.py
|
|
49
49
|
scripts/migrations/20260204_sync_lead_owner_from_tasks.py
|
|
50
50
|
scripts/migrations/20260303_add_webhook_subscription_collections.py
|
|
51
|
+
scripts/migrations/20260305_force_formatter_autorun_on_source_update.py
|
|
51
52
|
scripts/migrations/migrate_org_user_credits.py
|
|
52
53
|
scripts/migrations/set_default_lead_status.py
|
|
53
54
|
scripts/migrations/update_lead_owner_from_tasks.py
|
{autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/docs/research-table/reference/autotouch-cli-pypi.md
RENAMED
|
@@ -93,6 +93,11 @@ Notes:
|
|
|
93
93
|
- `--wait` polls import status to terminal state.
|
|
94
94
|
- `--checkpoint-file` stores state and blocks accidental duplicate re-imports.
|
|
95
95
|
- `--validate-only` validates CSV parse/shape on server without writing rows.
|
|
96
|
+
- Blacklist controls on optimized import:
|
|
97
|
+
- company-domain filtering is ON by default
|
|
98
|
+
- email filtering is OFF by default
|
|
99
|
+
- disable company filtering with `--no-check-company-blacklist`
|
|
100
|
+
- enable email filtering with `--check-email-blacklist`
|
|
96
101
|
- Safety assertions:
|
|
97
102
|
- `--expected-rows <N>`
|
|
98
103
|
- `--require-columns col_a,col_b`
|
|
@@ -100,6 +105,43 @@ Notes:
|
|
|
100
105
|
- `--require-non-empty key:ratio`
|
|
101
106
|
- For async imports with `--wait`, assertions are rechecked postflight via task manifest verification.
|
|
102
107
|
- Use `--allow-reimport` if you intentionally want to run the same file again.
|
|
108
|
+
- Import responses include `blacklist_summary` with company/email `skipped_count` and `enforced` flags.
|
|
109
|
+
|
|
110
|
+
Example enabling both company and email enforcement:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
autotouch rows import-csv \
|
|
114
|
+
--table-id <TABLE_ID> \
|
|
115
|
+
--confirm-table-id <TABLE_ID> \
|
|
116
|
+
--file contacts.csv \
|
|
117
|
+
--checkpoint-file .autotouch-import.json \
|
|
118
|
+
--check-email-blacklist \
|
|
119
|
+
--wait \
|
|
120
|
+
--output json
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Manage blacklist entries (native CLI, admin identity required):
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
# List current entries
|
|
127
|
+
autotouch blacklist list --type-filter all --limit 100
|
|
128
|
+
|
|
129
|
+
# Add entries
|
|
130
|
+
autotouch blacklist add --type domain --value competitor.com --reason "Do not contact"
|
|
131
|
+
autotouch blacklist add --type email --value blocked@example.com --reason "Unsubscribed"
|
|
132
|
+
|
|
133
|
+
# Bulk-import entries from CSV/TXT
|
|
134
|
+
autotouch blacklist import --file blacklist.csv
|
|
135
|
+
|
|
136
|
+
# Check/filter email sets (auto-chunked for large lists)
|
|
137
|
+
autotouch blacklist check --emails-file recipients.csv --emails-column email --summary-only
|
|
138
|
+
autotouch blacklist filter --emails-file recipients.csv --emails-column email --summary-only
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Recommended timing:
|
|
142
|
+
- Add suppressions early (unsubscribed addresses, existing customers, competitors, and clear non-ICP domains).
|
|
143
|
+
- Before billable enrichments (`llm_enrichment`, `email_finder`, `phone_finder`), run blacklist filtering and enrich only clean candidates.
|
|
144
|
+
- Run one final blacklist check/filter before outreach execution.
|
|
103
145
|
|
|
104
146
|
## Import modes
|
|
105
147
|
|
|
@@ -131,6 +173,7 @@ Auto-run is configured per column (`autoRun`), not per table.
|
|
|
131
173
|
Important:
|
|
132
174
|
- Insert events do not run `onSourceUpdate` columns.
|
|
133
175
|
- Imports may queue auto-run dispatch evaluation, but only columns whose `autoRun` policy matches the event are queued.
|
|
176
|
+
- Formatter columns are normalized server-side to `onSourceUpdate` (attempted `never`/`onInsert` values are overridden).
|
|
134
177
|
|
|
135
178
|
## Create a column
|
|
136
179
|
|
|
@@ -155,6 +198,7 @@ Notes:
|
|
|
155
198
|
- `add_to_crm` is optional and non-billable.
|
|
156
199
|
- Email/phone enrichment does not require creating/running `add_to_crm`.
|
|
157
200
|
- For `add_to_crm`, required mapping keys are `linkedinUrl` and `companyDomain`.
|
|
201
|
+
- Formatter formulas must use row references (`row['first_name']`, `row.last_name`), not bare template placeholders like ``${first_name}``.
|
|
158
202
|
- `sync_to_table` supports both:
|
|
159
203
|
- single destination: `destinationTableId` + `columnMappings`
|
|
160
204
|
- router mode: `routes[]` (first matching route wins)
|
|
@@ -162,6 +206,7 @@ Notes:
|
|
|
162
206
|
- `sequenceId`
|
|
163
207
|
- `sourceLeadColumn` pointing to a lead-id producing column (`add_to_crm` or `lead_finder` output)
|
|
164
208
|
- auto-attaches research context defaults during enrollment (`source_table_id`, plus optional table name); favorite fields resolve from current starred columns when explicit `fieldIds` are not provided
|
|
209
|
+
- star/favorite high-signal fields so call-sidecar context stays quick to scan and AI draft context stays focused
|
|
165
210
|
|
|
166
211
|
If you create custom `llm_enrichment` schemas:
|
|
167
212
|
- Use strict field-map schema shape (no root `type/properties` wrapper).
|
|
@@ -218,6 +263,8 @@ Design guidance:
|
|
|
218
263
|
- default behavior for agents: write full context/content, not summaries or truncation
|
|
219
264
|
- only truncate/summarize when there is a hard limit (storage/model/provider/payload), and explicitly mark truncation
|
|
220
265
|
- keep intent separate from raw context so downstream logic can change without losing source data
|
|
266
|
+
- for calling workflows, favorite/star key fields so users can access the most relevant context quickly in sidecar views
|
|
267
|
+
- for AI-generated emails/copy, include both intent and full context in imports so drafts are grounded, not generic
|
|
221
268
|
- keep one entity per row and dedupe on a stable identity key
|
|
222
269
|
- keep snippets/summaries optional and derived from raw context, never a replacement for it
|
|
223
270
|
- normalize intent labels if you need deterministic automation
|
|
@@ -507,6 +554,15 @@ autotouch columns run-next \
|
|
|
507
554
|
--wait
|
|
508
555
|
```
|
|
509
556
|
|
|
557
|
+
Blacklist gate (recommended before billable runs):
|
|
558
|
+
|
|
559
|
+
```bash
|
|
560
|
+
autotouch blacklist filter \
|
|
561
|
+
--emails-file candidates.csv \
|
|
562
|
+
--emails-column work_email \
|
|
563
|
+
--output json
|
|
564
|
+
```
|
|
565
|
+
|
|
510
566
|
## Job truth contract (agent-safe)
|
|
511
567
|
|
|
512
568
|
- Treat a run as started only when you receive `job_id`.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Normalize formatter columns to auto-run on source updates.
|
|
4
|
+
|
|
5
|
+
Why:
|
|
6
|
+
- Formatter columns should be reactive transforms.
|
|
7
|
+
- Legacy records may still have auto_run=never/onInsert or a conflicting legacy autoRun field.
|
|
8
|
+
|
|
9
|
+
What this migration does:
|
|
10
|
+
1) Finds formatter columns with missing/legacy auto-run values.
|
|
11
|
+
2) Sets `auto_run` to `onSourceUpdate`.
|
|
12
|
+
3) Removes legacy `autoRun` to avoid precedence conflicts in payload serialization.
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
python scripts/migrations/20260305_force_formatter_autorun_on_source_update.py
|
|
16
|
+
|
|
17
|
+
Environment variables:
|
|
18
|
+
MONGODB_URI (default: mongodb://localhost:27017)
|
|
19
|
+
MONGODB_DB_NAME (default: autotouch)
|
|
20
|
+
DRY_RUN (default: false)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import asyncio
|
|
24
|
+
import os
|
|
25
|
+
from datetime import datetime
|
|
26
|
+
from typing import Any, Dict
|
|
27
|
+
|
|
28
|
+
from motor.motor_asyncio import AsyncIOMotorClient
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
MONGODB_URI = os.getenv("MONGODB_URI", "mongodb://localhost:27017")
|
|
32
|
+
MONGODB_DB_NAME = os.getenv("MONGODB_DB_NAME", "autotouch")
|
|
33
|
+
DRY_RUN = os.getenv("DRY_RUN", "false").strip().lower() == "true"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _formatter_query() -> Dict[str, Any]:
|
|
37
|
+
return {
|
|
38
|
+
"kind": "formatter",
|
|
39
|
+
"$or": [
|
|
40
|
+
{"auto_run": {"$exists": False}},
|
|
41
|
+
{"auto_run": {"$in": [None, "", "never", "onInsert"]}},
|
|
42
|
+
{"autoRun": {"$exists": True}},
|
|
43
|
+
],
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async def run() -> None:
|
|
48
|
+
client = AsyncIOMotorClient(MONGODB_URI)
|
|
49
|
+
db = client[MONGODB_DB_NAME]
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
print(f"Starting formatter auto-run migration on db={MONGODB_DB_NAME}")
|
|
53
|
+
print(f"Timestamp: {datetime.utcnow().isoformat()}Z")
|
|
54
|
+
print(f"DRY_RUN={DRY_RUN}")
|
|
55
|
+
|
|
56
|
+
query = _formatter_query()
|
|
57
|
+
candidates = await db.columns.count_documents(query)
|
|
58
|
+
print(f"Formatter columns to normalize: {candidates}")
|
|
59
|
+
|
|
60
|
+
if candidates == 0:
|
|
61
|
+
print("No formatter columns require migration.")
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
sample = await db.columns.find(
|
|
65
|
+
query,
|
|
66
|
+
{"_id": 1, "key": 1, "auto_run": 1, "autoRun": 1},
|
|
67
|
+
).limit(5).to_list(length=5)
|
|
68
|
+
print("Sample candidates:")
|
|
69
|
+
for col in sample:
|
|
70
|
+
print(
|
|
71
|
+
f"- id={col.get('_id')} key={col.get('key')} "
|
|
72
|
+
f"auto_run={col.get('auto_run')} autoRun={col.get('autoRun')}"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if DRY_RUN:
|
|
76
|
+
print("DRY_RUN enabled; no updates applied.")
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
now = datetime.utcnow()
|
|
80
|
+
result = await db.columns.update_many(
|
|
81
|
+
query,
|
|
82
|
+
{
|
|
83
|
+
"$set": {
|
|
84
|
+
"auto_run": "onSourceUpdate",
|
|
85
|
+
"updated_at": now,
|
|
86
|
+
},
|
|
87
|
+
"$unset": {"autoRun": ""},
|
|
88
|
+
},
|
|
89
|
+
)
|
|
90
|
+
print(f"Matched: {result.matched_count}")
|
|
91
|
+
print(f"Modified: {result.modified_count}")
|
|
92
|
+
print("Migration complete.")
|
|
93
|
+
finally:
|
|
94
|
+
client.close()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
if __name__ == "__main__":
|
|
98
|
+
asyncio.run(run())
|
|
@@ -659,6 +659,150 @@ def _load_json_input(
|
|
|
659
659
|
return default
|
|
660
660
|
|
|
661
661
|
|
|
662
|
+
def _normalize_email_value(value: Any) -> Optional[str]:
|
|
663
|
+
text = str(value or "").strip().lower()
|
|
664
|
+
return text or None
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
def _split_cli_email_values(raw_values: Optional[List[str]]) -> List[str]:
|
|
668
|
+
emails: List[str] = []
|
|
669
|
+
for raw in raw_values or []:
|
|
670
|
+
for piece in str(raw).replace("\n", ",").split(","):
|
|
671
|
+
normalized = _normalize_email_value(piece)
|
|
672
|
+
if normalized:
|
|
673
|
+
emails.append(normalized)
|
|
674
|
+
return emails
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def _parse_email_payload(payload: Any, *, context: str) -> List[str]:
|
|
678
|
+
raw_values: List[Any]
|
|
679
|
+
if isinstance(payload, list):
|
|
680
|
+
raw_values = payload
|
|
681
|
+
elif isinstance(payload, dict) and isinstance(payload.get("emails"), list):
|
|
682
|
+
raw_values = payload.get("emails") or []
|
|
683
|
+
else:
|
|
684
|
+
print(
|
|
685
|
+
f"ERROR: {context} must be a JSON array or an object with an 'emails' array",
|
|
686
|
+
file=sys.stderr,
|
|
687
|
+
)
|
|
688
|
+
sys.exit(2)
|
|
689
|
+
|
|
690
|
+
emails: List[str] = []
|
|
691
|
+
for item in raw_values:
|
|
692
|
+
normalized = _normalize_email_value(item)
|
|
693
|
+
if normalized:
|
|
694
|
+
emails.append(normalized)
|
|
695
|
+
return emails
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def _load_emails_from_file(file_path: str, *, csv_column: Optional[str]) -> List[str]:
|
|
699
|
+
resolved_path = os.path.abspath(os.path.expanduser(str(file_path)))
|
|
700
|
+
suffix = Path(resolved_path).suffix.lower()
|
|
701
|
+
|
|
702
|
+
try:
|
|
703
|
+
if suffix == ".json":
|
|
704
|
+
with open(resolved_path, "r", encoding="utf-8") as f:
|
|
705
|
+
payload = json.load(f)
|
|
706
|
+
return _parse_email_payload(payload, context=f"emails file '{resolved_path}'")
|
|
707
|
+
|
|
708
|
+
if suffix == ".csv":
|
|
709
|
+
with open(resolved_path, "r", encoding="utf-8", newline="") as f:
|
|
710
|
+
reader = csv.DictReader(f)
|
|
711
|
+
rows = list(reader)
|
|
712
|
+
fieldnames = [name for name in (reader.fieldnames or []) if name]
|
|
713
|
+
|
|
714
|
+
if not fieldnames:
|
|
715
|
+
return []
|
|
716
|
+
|
|
717
|
+
preferred = str(csv_column or "").strip().lower()
|
|
718
|
+
selected_column: Optional[str] = None
|
|
719
|
+
if preferred:
|
|
720
|
+
for field_name in fieldnames:
|
|
721
|
+
if str(field_name).strip().lower() == preferred:
|
|
722
|
+
selected_column = field_name
|
|
723
|
+
break
|
|
724
|
+
|
|
725
|
+
if selected_column is None:
|
|
726
|
+
normalized_fields = {
|
|
727
|
+
str(field_name).strip().lower(): field_name for field_name in fieldnames
|
|
728
|
+
}
|
|
729
|
+
for candidate in ("email", "work_email", "personal_email", "primary_email"):
|
|
730
|
+
if candidate in normalized_fields:
|
|
731
|
+
selected_column = normalized_fields[candidate]
|
|
732
|
+
break
|
|
733
|
+
|
|
734
|
+
if selected_column is None:
|
|
735
|
+
selected_column = fieldnames[0]
|
|
736
|
+
|
|
737
|
+
emails: List[str] = []
|
|
738
|
+
for row in rows:
|
|
739
|
+
normalized = _normalize_email_value((row or {}).get(selected_column))
|
|
740
|
+
if normalized:
|
|
741
|
+
emails.append(normalized)
|
|
742
|
+
return emails
|
|
743
|
+
|
|
744
|
+
emails: List[str] = []
|
|
745
|
+
with open(resolved_path, "r", encoding="utf-8") as f:
|
|
746
|
+
for line in f:
|
|
747
|
+
for piece in str(line).replace("\n", "").split(","):
|
|
748
|
+
normalized = _normalize_email_value(piece)
|
|
749
|
+
if normalized:
|
|
750
|
+
emails.append(normalized)
|
|
751
|
+
return emails
|
|
752
|
+
|
|
753
|
+
except FileNotFoundError:
|
|
754
|
+
print(f"ERROR: emails file not found: {resolved_path}", file=sys.stderr)
|
|
755
|
+
sys.exit(2)
|
|
756
|
+
except Exception as exc:
|
|
757
|
+
print(f"ERROR: failed to read emails file '{resolved_path}': {exc}", file=sys.stderr)
|
|
758
|
+
sys.exit(2)
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
def _dedupe_keep_order(values: List[str]) -> List[str]:
|
|
762
|
+
seen: set[str] = set()
|
|
763
|
+
deduped: List[str] = []
|
|
764
|
+
for value in values:
|
|
765
|
+
if value in seen:
|
|
766
|
+
continue
|
|
767
|
+
seen.add(value)
|
|
768
|
+
deduped.append(value)
|
|
769
|
+
return deduped
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
def _collect_blacklist_emails(args: argparse.Namespace) -> List[str]:
|
|
773
|
+
emails: List[str] = []
|
|
774
|
+
emails.extend(_split_cli_email_values(getattr(args, "email", None)))
|
|
775
|
+
|
|
776
|
+
raw_emails_json = getattr(args, "emails_json", None)
|
|
777
|
+
if raw_emails_json:
|
|
778
|
+
payload = _parse_json_string(str(raw_emails_json), "emails")
|
|
779
|
+
emails.extend(_parse_email_payload(payload, context="--emails-json"))
|
|
780
|
+
|
|
781
|
+
emails_file = getattr(args, "emails_file", None)
|
|
782
|
+
if emails_file:
|
|
783
|
+
emails.extend(
|
|
784
|
+
_load_emails_from_file(
|
|
785
|
+
str(emails_file),
|
|
786
|
+
csv_column=getattr(args, "emails_column", None),
|
|
787
|
+
)
|
|
788
|
+
)
|
|
789
|
+
|
|
790
|
+
deduped = _dedupe_keep_order([email for email in emails if email])
|
|
791
|
+
if not deduped:
|
|
792
|
+
print(
|
|
793
|
+
"ERROR: provide emails via --email, --emails-json, or --emails-file",
|
|
794
|
+
file=sys.stderr,
|
|
795
|
+
)
|
|
796
|
+
sys.exit(2)
|
|
797
|
+
return deduped
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
def _chunk_list(values: List[str], size: int) -> List[List[str]]:
|
|
801
|
+
if size <= 0:
|
|
802
|
+
return [values]
|
|
803
|
+
return [values[idx: idx + size] for idx in range(0, len(values), size)]
|
|
804
|
+
|
|
805
|
+
|
|
662
806
|
def _request_api(
|
|
663
807
|
method: str,
|
|
664
808
|
path: str,
|
|
@@ -2239,6 +2383,211 @@ def cmd_tables_create(args: argparse.Namespace) -> None:
|
|
|
2239
2383
|
_print_json(data, compact=args.compact)
|
|
2240
2384
|
|
|
2241
2385
|
|
|
2386
|
+
def cmd_blacklist_list(args: argparse.Namespace) -> None:
|
|
2387
|
+
token = _resolve_token(args.token, required=True)
|
|
2388
|
+
params: Dict[str, Any] = {
|
|
2389
|
+
"type_filter": str(getattr(args, "type_filter", "all") or "all"),
|
|
2390
|
+
"page": max(1, int(getattr(args, "page", 1) or 1)),
|
|
2391
|
+
"limit": max(1, int(getattr(args, "limit", 100) or 100)),
|
|
2392
|
+
}
|
|
2393
|
+
search = str(getattr(args, "search", "") or "").strip()
|
|
2394
|
+
if search:
|
|
2395
|
+
params["search"] = search
|
|
2396
|
+
|
|
2397
|
+
data = _request_api(
|
|
2398
|
+
"GET",
|
|
2399
|
+
"/api/blacklist",
|
|
2400
|
+
base_url=args.base_url,
|
|
2401
|
+
token=token,
|
|
2402
|
+
use_x_api_key=args.use_x_api_key,
|
|
2403
|
+
params=params,
|
|
2404
|
+
timeout=args.timeout,
|
|
2405
|
+
verbose=args.verbose,
|
|
2406
|
+
)
|
|
2407
|
+
_print_json(data, compact=args.compact)
|
|
2408
|
+
|
|
2409
|
+
|
|
2410
|
+
def cmd_blacklist_add(args: argparse.Namespace) -> None:
|
|
2411
|
+
token = _resolve_token(args.token, required=True)
|
|
2412
|
+
entry_type = str(args.type).strip().lower()
|
|
2413
|
+
value = str(args.value or "").strip().lower()
|
|
2414
|
+
|
|
2415
|
+
if entry_type == "domain":
|
|
2416
|
+
value = value.replace("https://", "").replace("http://", "")
|
|
2417
|
+
value = value.lstrip("@")
|
|
2418
|
+
value = value.split("/", 1)[0]
|
|
2419
|
+
|
|
2420
|
+
payload = {
|
|
2421
|
+
"type": entry_type,
|
|
2422
|
+
"value": value,
|
|
2423
|
+
"reason": str(getattr(args, "reason", "") or ""),
|
|
2424
|
+
"notes": str(getattr(args, "notes", "") or ""),
|
|
2425
|
+
}
|
|
2426
|
+
data = _request_api(
|
|
2427
|
+
"POST",
|
|
2428
|
+
"/api/blacklist",
|
|
2429
|
+
base_url=args.base_url,
|
|
2430
|
+
token=token,
|
|
2431
|
+
use_x_api_key=args.use_x_api_key,
|
|
2432
|
+
payload=payload,
|
|
2433
|
+
timeout=args.timeout,
|
|
2434
|
+
verbose=args.verbose,
|
|
2435
|
+
)
|
|
2436
|
+
_print_json(data, compact=args.compact)
|
|
2437
|
+
|
|
2438
|
+
|
|
2439
|
+
def cmd_blacklist_remove(args: argparse.Namespace) -> None:
|
|
2440
|
+
token = _resolve_token(args.token, required=True)
|
|
2441
|
+
data = _request_api(
|
|
2442
|
+
"DELETE",
|
|
2443
|
+
f"/api/blacklist/{args.entry_id}",
|
|
2444
|
+
base_url=args.base_url,
|
|
2445
|
+
token=token,
|
|
2446
|
+
use_x_api_key=args.use_x_api_key,
|
|
2447
|
+
timeout=args.timeout,
|
|
2448
|
+
verbose=args.verbose,
|
|
2449
|
+
)
|
|
2450
|
+
_print_json(data, compact=args.compact)
|
|
2451
|
+
|
|
2452
|
+
|
|
2453
|
+
def cmd_blacklist_import(args: argparse.Namespace) -> None:
|
|
2454
|
+
token = _resolve_token(args.token, required=True)
|
|
2455
|
+
source_path = os.path.abspath(os.path.expanduser(str(args.file)))
|
|
2456
|
+
data = _request_multipart_api(
|
|
2457
|
+
"POST",
|
|
2458
|
+
"/api/blacklist/import",
|
|
2459
|
+
base_url=args.base_url,
|
|
2460
|
+
token=token,
|
|
2461
|
+
use_x_api_key=args.use_x_api_key,
|
|
2462
|
+
file_path=source_path,
|
|
2463
|
+
file_field="file",
|
|
2464
|
+
timeout=args.timeout,
|
|
2465
|
+
verbose=args.verbose,
|
|
2466
|
+
)
|
|
2467
|
+
if isinstance(data, dict):
|
|
2468
|
+
data.setdefault("source", source_path)
|
|
2469
|
+
_print_json(data, compact=args.compact)
|
|
2470
|
+
|
|
2471
|
+
|
|
2472
|
+
def cmd_blacklist_check(args: argparse.Namespace) -> None:
|
|
2473
|
+
token = _resolve_token(args.token, required=True)
|
|
2474
|
+
emails = _collect_blacklist_emails(args)
|
|
2475
|
+
chunk_size = max(1, min(int(getattr(args, "chunk_size", 1000) or 1000), 1000))
|
|
2476
|
+
|
|
2477
|
+
all_results: List[Dict[str, Any]] = []
|
|
2478
|
+
chunk_summaries: List[Dict[str, Any]] = []
|
|
2479
|
+
for chunk_index, chunk in enumerate(_chunk_list(emails, chunk_size), start=1):
|
|
2480
|
+
response = _request_api(
|
|
2481
|
+
"POST",
|
|
2482
|
+
"/api/blacklist/check",
|
|
2483
|
+
base_url=args.base_url,
|
|
2484
|
+
token=token,
|
|
2485
|
+
use_x_api_key=args.use_x_api_key,
|
|
2486
|
+
payload={"emails": chunk},
|
|
2487
|
+
timeout=args.timeout,
|
|
2488
|
+
verbose=args.verbose,
|
|
2489
|
+
)
|
|
2490
|
+
if not isinstance(response, dict):
|
|
2491
|
+
print("ERROR: unexpected response from /api/blacklist/check", file=sys.stderr)
|
|
2492
|
+
sys.exit(1)
|
|
2493
|
+
|
|
2494
|
+
chunk_results = response.get("results") if isinstance(response.get("results"), list) else []
|
|
2495
|
+
chunk_summary = response.get("summary") if isinstance(response.get("summary"), dict) else {}
|
|
2496
|
+
all_results.extend([result for result in chunk_results if isinstance(result, dict)])
|
|
2497
|
+
chunk_summaries.append(
|
|
2498
|
+
{
|
|
2499
|
+
"chunk": chunk_index,
|
|
2500
|
+
"size": len(chunk),
|
|
2501
|
+
"summary": chunk_summary,
|
|
2502
|
+
}
|
|
2503
|
+
)
|
|
2504
|
+
|
|
2505
|
+
total_blacklisted = sum(
|
|
2506
|
+
1
|
|
2507
|
+
for result in all_results
|
|
2508
|
+
if bool(result.get("is_blacklisted"))
|
|
2509
|
+
)
|
|
2510
|
+
output: Dict[str, Any] = {
|
|
2511
|
+
"input_count": len(emails),
|
|
2512
|
+
"chunks": len(chunk_summaries),
|
|
2513
|
+
"chunk_size": chunk_size,
|
|
2514
|
+
"summary": {
|
|
2515
|
+
"total_checked": len(all_results),
|
|
2516
|
+
"total_blacklisted": total_blacklisted,
|
|
2517
|
+
"total_clean": max(0, len(all_results) - total_blacklisted),
|
|
2518
|
+
},
|
|
2519
|
+
}
|
|
2520
|
+
if not bool(getattr(args, "summary_only", False)):
|
|
2521
|
+
output["results"] = all_results
|
|
2522
|
+
if len(chunk_summaries) > 1:
|
|
2523
|
+
output["chunk_summaries"] = chunk_summaries
|
|
2524
|
+
_print_json(output, compact=args.compact)
|
|
2525
|
+
|
|
2526
|
+
|
|
2527
|
+
def cmd_blacklist_filter(args: argparse.Namespace) -> None:
|
|
2528
|
+
token = _resolve_token(args.token, required=True)
|
|
2529
|
+
emails = _collect_blacklist_emails(args)
|
|
2530
|
+
chunk_size = max(1, min(int(getattr(args, "chunk_size", 10000) or 10000), 10000))
|
|
2531
|
+
|
|
2532
|
+
clean_emails: List[str] = []
|
|
2533
|
+
blacklisted_emails: List[Dict[str, Any]] = []
|
|
2534
|
+
chunk_summaries: List[Dict[str, Any]] = []
|
|
2535
|
+
for chunk_index, chunk in enumerate(_chunk_list(emails, chunk_size), start=1):
|
|
2536
|
+
response = _request_api(
|
|
2537
|
+
"POST",
|
|
2538
|
+
"/api/blacklist/filter",
|
|
2539
|
+
base_url=args.base_url,
|
|
2540
|
+
token=token,
|
|
2541
|
+
use_x_api_key=args.use_x_api_key,
|
|
2542
|
+
payload={"emails": chunk},
|
|
2543
|
+
timeout=args.timeout,
|
|
2544
|
+
verbose=args.verbose,
|
|
2545
|
+
)
|
|
2546
|
+
if not isinstance(response, dict):
|
|
2547
|
+
print("ERROR: unexpected response from /api/blacklist/filter", file=sys.stderr)
|
|
2548
|
+
sys.exit(1)
|
|
2549
|
+
|
|
2550
|
+
chunk_clean = response.get("clean_emails") if isinstance(response.get("clean_emails"), list) else []
|
|
2551
|
+
chunk_blocked = (
|
|
2552
|
+
response.get("blacklisted_emails")
|
|
2553
|
+
if isinstance(response.get("blacklisted_emails"), list)
|
|
2554
|
+
else []
|
|
2555
|
+
)
|
|
2556
|
+
chunk_summary = response.get("summary") if isinstance(response.get("summary"), dict) else {}
|
|
2557
|
+
clean_emails.extend(
|
|
2558
|
+
[
|
|
2559
|
+
normalized
|
|
2560
|
+
for normalized in (_normalize_email_value(item) for item in chunk_clean)
|
|
2561
|
+
if normalized
|
|
2562
|
+
]
|
|
2563
|
+
)
|
|
2564
|
+
blacklisted_emails.extend([item for item in chunk_blocked if isinstance(item, dict)])
|
|
2565
|
+
chunk_summaries.append(
|
|
2566
|
+
{
|
|
2567
|
+
"chunk": chunk_index,
|
|
2568
|
+
"size": len(chunk),
|
|
2569
|
+
"summary": chunk_summary,
|
|
2570
|
+
}
|
|
2571
|
+
)
|
|
2572
|
+
|
|
2573
|
+
output: Dict[str, Any] = {
|
|
2574
|
+
"input_count": len(emails),
|
|
2575
|
+
"chunks": len(chunk_summaries),
|
|
2576
|
+
"chunk_size": chunk_size,
|
|
2577
|
+
"summary": {
|
|
2578
|
+
"total_provided": len(emails),
|
|
2579
|
+
"total_clean": len(clean_emails),
|
|
2580
|
+
"total_blacklisted": len(blacklisted_emails),
|
|
2581
|
+
},
|
|
2582
|
+
}
|
|
2583
|
+
if not bool(getattr(args, "summary_only", False)):
|
|
2584
|
+
output["clean_emails"] = clean_emails
|
|
2585
|
+
output["blacklisted_emails"] = blacklisted_emails
|
|
2586
|
+
if len(chunk_summaries) > 1:
|
|
2587
|
+
output["chunk_summaries"] = chunk_summaries
|
|
2588
|
+
_print_json(output, compact=args.compact)
|
|
2589
|
+
|
|
2590
|
+
|
|
2242
2591
|
def cmd_rows_add(args: argparse.Namespace) -> None:
|
|
2243
2592
|
token = _resolve_token(args.token, required=True)
|
|
2244
2593
|
records_payload = _load_json_input(
|
|
@@ -3616,6 +3965,74 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
3616
3965
|
_add_api_common_arguments(ptc)
|
|
3617
3966
|
ptc.set_defaults(func=cmd_tables_create)
|
|
3618
3967
|
|
|
3968
|
+
# blacklist
|
|
3969
|
+
pbl = sub.add_parser("blacklist", help="Organization blacklist operations (admin)")
|
|
3970
|
+
blacklist_sub = pbl.add_subparsers(dest="blacklist_cmd", required=True)
|
|
3971
|
+
|
|
3972
|
+
pbll = blacklist_sub.add_parser("list", help="List blacklist entries")
|
|
3973
|
+
pbll.add_argument("--type-filter", choices=["all", "email", "domain"], default="all")
|
|
3974
|
+
pbll.add_argument("--search", help="Filter by value/reason/notes")
|
|
3975
|
+
pbll.add_argument("--page", type=int, default=1, help="Page number (default: 1)")
|
|
3976
|
+
pbll.add_argument("--limit", type=int, default=100, help="Entries per page (default: 100)")
|
|
3977
|
+
_add_api_common_arguments(pbll)
|
|
3978
|
+
pbll.set_defaults(func=cmd_blacklist_list)
|
|
3979
|
+
|
|
3980
|
+
pbla = blacklist_sub.add_parser("add", help="Add a blacklist entry")
|
|
3981
|
+
pbla.add_argument("--type", choices=["email", "domain"], required=True, help="Entry type")
|
|
3982
|
+
pbla.add_argument("--value", required=True, help="Email or domain value")
|
|
3983
|
+
pbla.add_argument("--reason", default="", help="Optional reason")
|
|
3984
|
+
pbla.add_argument("--notes", default="", help="Optional notes")
|
|
3985
|
+
_add_api_common_arguments(pbla)
|
|
3986
|
+
pbla.set_defaults(func=cmd_blacklist_add)
|
|
3987
|
+
|
|
3988
|
+
pblr = blacklist_sub.add_parser("remove", help="Remove a blacklist entry by id")
|
|
3989
|
+
pblr.add_argument("--entry-id", required=True)
|
|
3990
|
+
_add_api_common_arguments(pblr)
|
|
3991
|
+
pblr.set_defaults(func=cmd_blacklist_remove)
|
|
3992
|
+
|
|
3993
|
+
pbli = blacklist_sub.add_parser("import", help="Import blacklist entries from CSV/TXT")
|
|
3994
|
+
pbli.add_argument("--file", required=True, help="Path to CSV/TXT file")
|
|
3995
|
+
_add_api_common_arguments(pbli)
|
|
3996
|
+
pbli.set_defaults(func=cmd_blacklist_import)
|
|
3997
|
+
|
|
3998
|
+
pblc = blacklist_sub.add_parser("check", help="Check whether emails are blacklisted")
|
|
3999
|
+
pblc.add_argument(
|
|
4000
|
+
"--email",
|
|
4001
|
+
action="append",
|
|
4002
|
+
help="Email value (repeatable; comma-separated also supported)",
|
|
4003
|
+
)
|
|
4004
|
+
pblc.add_argument("--emails-json", help="JSON array or object with emails[]")
|
|
4005
|
+
pblc.add_argument("--emails-file", help="Path to .txt/.csv/.json email list")
|
|
4006
|
+
pblc.add_argument("--emails-column", default="email", help="CSV column for emails (default: email)")
|
|
4007
|
+
pblc.add_argument(
|
|
4008
|
+
"--chunk-size",
|
|
4009
|
+
type=int,
|
|
4010
|
+
default=1000,
|
|
4011
|
+
help="Request chunk size (max 1000, default: 1000)",
|
|
4012
|
+
)
|
|
4013
|
+
pblc.add_argument("--summary-only", action="store_true", help="Omit per-email results")
|
|
4014
|
+
_add_api_common_arguments(pblc)
|
|
4015
|
+
pblc.set_defaults(func=cmd_blacklist_check)
|
|
4016
|
+
|
|
4017
|
+
pblf = blacklist_sub.add_parser("filter", help="Filter blacklisted emails from a list")
|
|
4018
|
+
pblf.add_argument(
|
|
4019
|
+
"--email",
|
|
4020
|
+
action="append",
|
|
4021
|
+
help="Email value (repeatable; comma-separated also supported)",
|
|
4022
|
+
)
|
|
4023
|
+
pblf.add_argument("--emails-json", help="JSON array or object with emails[]")
|
|
4024
|
+
pblf.add_argument("--emails-file", help="Path to .txt/.csv/.json email list")
|
|
4025
|
+
pblf.add_argument("--emails-column", default="email", help="CSV column for emails (default: email)")
|
|
4026
|
+
pblf.add_argument(
|
|
4027
|
+
"--chunk-size",
|
|
4028
|
+
type=int,
|
|
4029
|
+
default=10000,
|
|
4030
|
+
help="Request chunk size (max 10000, default: 10000)",
|
|
4031
|
+
)
|
|
4032
|
+
pblf.add_argument("--summary-only", action="store_true", help="Omit full clean/blocked lists")
|
|
4033
|
+
_add_api_common_arguments(pblf)
|
|
4034
|
+
pblf.set_defaults(func=cmd_blacklist_filter)
|
|
4035
|
+
|
|
3619
4036
|
# rows
|
|
3620
4037
|
pr = sub.add_parser("rows", help="Row operations")
|
|
3621
4038
|
rows_sub = pr.add_subparsers(dest="rows_cmd", required=True)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/attach_csv_import_leads_to_research_table.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/migrations/20250106_add_column_position.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/migrations/20250123_add_filter_indexes.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/migrations/20250604_add_origin_indexes.py
RENAMED
|
File without changes
|
{autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/migrations/20250608_cleanup_agent_metadata.py
RENAMED
|
File without changes
|
|
File without changes
|
{autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/migrations/20250922_add_activity_indexes.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/migrations/20250929_add_task_join_indexes.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/migrations/migrate_org_user_credits.py
RENAMED
|
File without changes
|
|
File without changes
|
{autotouch_cli-0.2.21 → autotouch_cli-0.2.22}/scripts/migrations/update_lead_owner_from_tasks.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|