autotouch-cli 0.2.59__tar.gz → 0.2.60__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.
Files changed (51) hide show
  1. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/PKG-INFO +1 -1
  2. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_cli/core/validation.py +62 -6
  3. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_cli/data/CLI_REFERENCE.md +1 -1
  4. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_cli/data/cli-manifest.json +1 -1
  5. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_cli/templates.py +6 -6
  6. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_cli.egg-info/PKG-INFO +1 -1
  7. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_shared/provider_registry.py +36 -11
  8. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/pyproject.toml +1 -1
  9. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/MANIFEST.in +0 -0
  10. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/README.md +0 -0
  11. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_cli/__init__.py +0 -0
  12. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_cli/cli.py +0 -0
  13. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_cli/cli_contracts.py +0 -0
  14. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_cli/commands/__init__.py +0 -0
  15. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_cli/commands/auth.py +0 -0
  16. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_cli/commands/cells.py +0 -0
  17. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_cli/commands/columns.py +0 -0
  18. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_cli/commands/jobs.py +0 -0
  19. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_cli/commands/leads.py +0 -0
  20. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_cli/commands/linkedin.py +0 -0
  21. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_cli/commands/prompts.py +0 -0
  22. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_cli/commands/rows.py +0 -0
  23. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_cli/commands/search.py +0 -0
  24. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_cli/commands/sequences.py +0 -0
  25. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_cli/commands/tables.py +0 -0
  26. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_cli/commands/tasks.py +0 -0
  27. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_cli/commands/webhooks.py +0 -0
  28. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_cli/commands/workspace_secrets.py +0 -0
  29. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_cli/core/__init__.py +0 -0
  30. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_cli/core/auth.py +0 -0
  31. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_cli/core/config.py +0 -0
  32. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_cli/core/csv_import.py +0 -0
  33. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_cli/core/http.py +0 -0
  34. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_cli/core/io.py +0 -0
  35. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_cli/core/output.py +0 -0
  36. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_cli/core/polling.py +0 -0
  37. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_cli/core/run.py +0 -0
  38. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_cli/exceptions.py +0 -0
  39. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_cli/mongo_status.py +0 -0
  40. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_cli/parser.py +0 -0
  41. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_cli/parser_groups.py +0 -0
  42. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_cli/sequence_support.py +0 -0
  43. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_cli.egg-info/SOURCES.txt +0 -0
  44. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_cli.egg-info/dependency_links.txt +0 -0
  45. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_cli.egg-info/entry_points.txt +0 -0
  46. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_cli.egg-info/requires.txt +0 -0
  47. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_cli.egg-info/top_level.txt +0 -0
  48. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_shared/__init__.py +0 -0
  49. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_shared/linkedin_contract.py +0 -0
  50. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/autotouch_shared/search_contract.py +0 -0
  51. {autotouch_cli-0.2.59 → autotouch_cli-0.2.60}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: autotouch-cli
3
- Version: 0.2.59
3
+ Version: 0.2.60
4
4
  Summary: Autotouch Smart Table CLI
5
5
  Project-URL: Homepage, https://app.autotouch.ai
6
6
  Project-URL: Documentation, https://github.com/nicolonic/autotouch_main/tree/main/docs/research-table/reference
@@ -55,8 +55,18 @@ def extract_add_to_crm_field_mappings(payload: Any) -> Optional[Dict[str, Any]]:
55
55
  return {}
56
56
 
57
57
 
58
- def inspect_add_to_crm_field_mappings(field_mappings: Dict[str, Any]) -> Dict[str, Any]:
59
- mode = "multi" if str(field_mappings.get("mode") or "").strip().lower() == "multi" else "single"
58
+ def inspect_add_to_crm_field_mappings(
59
+ field_mappings: Dict[str, Any],
60
+ *,
61
+ require_company_domain: bool = True,
62
+ ) -> Dict[str, Any]:
63
+ raw_mode = str(field_mappings.get("mode") or "").strip().lower()
64
+ if raw_mode == "array":
65
+ mode = "array"
66
+ elif raw_mode == "multi":
67
+ mode = "multi"
68
+ else:
69
+ mode = "single"
60
70
  required_missing: List[str] = []
61
71
  optional_missing: List[str] = []
62
72
 
@@ -75,12 +85,20 @@ def inspect_add_to_crm_field_mappings(field_mappings: Dict[str, Any]) -> Dict[st
75
85
  return True
76
86
  return False
77
87
 
88
+ if not require_company_domain and mode != "single":
89
+ required_missing.append("singleModeOnlyWhenCompanyDomainOptional")
90
+ return {
91
+ "mode": mode,
92
+ "required_missing": required_missing,
93
+ "optional_missing": optional_missing,
94
+ }
95
+
78
96
  if mode == "multi":
79
97
  common = field_mappings.get("common") if isinstance(field_mappings.get("common"), dict) else {}
80
98
  leads = field_mappings.get("leads") if isinstance(field_mappings.get("leads"), list) else []
81
99
 
82
100
  common_domain = str((common or {}).get("companyDomain") or field_mappings.get("companyDomain") or "").strip()
83
- if not common_domain:
101
+ if require_company_domain and not common_domain:
84
102
  required_missing.append("common.companyDomain")
85
103
 
86
104
  has_lead_identity = False
@@ -95,10 +113,24 @@ def inspect_add_to_crm_field_mappings(field_mappings: Dict[str, Any]) -> Dict[st
95
113
  optional_missing.append(f"leads[{idx}].{slot}")
96
114
  if not has_lead_identity:
97
115
  required_missing.append("leads[].identityFields")
116
+ elif mode == "array":
117
+ common = field_mappings.get("common") if isinstance(field_mappings.get("common"), dict) else {}
118
+ template = field_mappings.get("template") if isinstance(field_mappings.get("template"), dict) else {}
119
+ if require_company_domain and not str((common or {}).get("companyDomain") or "").strip():
120
+ required_missing.append("common.companyDomain")
121
+ if not str(field_mappings.get("sourceColumn") or "").strip():
122
+ required_missing.append("sourceColumn")
123
+ if not _has_identity_mapping(template):
124
+ required_missing.append("template.identityFields")
98
125
  else:
99
126
  company_domain_key = str(field_mappings.get("companyDomain") or "").strip()
100
- if not company_domain_key:
127
+ if require_company_domain and not company_domain_key:
101
128
  required_missing.append("companyDomain")
129
+ if require_company_domain:
130
+ if not _has_identity_mapping(field_mappings):
131
+ required_missing.append("identityFields")
132
+ elif not str(field_mappings.get("linkedinUrl") or "").strip():
133
+ required_missing.append("linkedinUrl")
102
134
  if not _has_identity_mapping(field_mappings):
103
135
  required_missing.append("identityFields")
104
136
  for slot in ["firstName", "lastName", "title", "companyName"]:
@@ -116,6 +148,7 @@ def validate_add_to_crm_create_payload(payload: Dict[str, Any]) -> None:
116
148
  field_mappings = extract_add_to_crm_field_mappings(payload)
117
149
  if field_mappings is None:
118
150
  return
151
+ config = payload.get("config") if isinstance(payload.get("config"), dict) else {}
119
152
 
120
153
  kind_value = str(payload.get("kind") or "").strip().lower()
121
154
  if kind_value != "enrichment":
@@ -130,10 +163,24 @@ def validate_add_to_crm_create_payload(payload: Dict[str, Any]) -> None:
130
163
  "Tip: run `autotouch columns recipe --type add_to_crm`."
131
164
  )
132
165
 
133
- issues = inspect_add_to_crm_field_mappings(field_mappings)
166
+ require_company_domain = config.get("requireCompanyDomain", True)
167
+ if not isinstance(require_company_domain, bool):
168
+ raise AutotouchInputError(
169
+ "add_to_crm config.requireCompanyDomain must be a boolean when provided."
170
+ )
171
+
172
+ issues = inspect_add_to_crm_field_mappings(
173
+ field_mappings,
174
+ require_company_domain=require_company_domain,
175
+ )
134
176
  required_missing = issues.get("required_missing") or []
135
177
  optional_missing = issues.get("optional_missing") or []
136
178
  if required_missing:
179
+ if "singleModeOnlyWhenCompanyDomainOptional" in required_missing:
180
+ raise AutotouchInputError(
181
+ "add_to_crm with config.requireCompanyDomain=false only supports single-mode fieldMappings. "
182
+ "Tip: map config.fieldMappings.linkedinUrl and keep mode='single' for LinkedIn-First Sync to Leads."
183
+ )
137
184
  missing = ", ".join(str(v) for v in required_missing)
138
185
  raise AutotouchInputError(
139
186
  f"add_to_crm fieldMappings missing required keys: {missing}. "
@@ -301,11 +348,20 @@ def run_precheck_for_add_to_crm(
301
348
  if field_mappings is None:
302
349
  return
303
350
 
304
- issues = inspect_add_to_crm_field_mappings(field_mappings)
351
+ config = column.get("config") if isinstance(column.get("config"), dict) else {}
352
+ require_company_domain = config.get("requireCompanyDomain", True)
353
+ issues = inspect_add_to_crm_field_mappings(
354
+ field_mappings,
355
+ require_company_domain=bool(require_company_domain) if isinstance(require_company_domain, bool) else True,
356
+ )
305
357
  required_missing = issues.get("required_missing") or []
306
358
  optional_missing = issues.get("optional_missing") or []
307
359
 
308
360
  if required_missing:
361
+ if "singleModeOnlyWhenCompanyDomainOptional" in required_missing:
362
+ raise AutotouchInputError(
363
+ "add_to_crm column is misconfigured: config.requireCompanyDomain=false only supports single-mode fieldMappings for LinkedIn-First Sync to Leads."
364
+ )
309
365
  missing = ", ".join(str(v) for v in required_missing)
310
366
  raise AutotouchInputError(
311
367
  f"add_to_crm column is misconfigured (missing required mappings: {missing}). "
@@ -1,6 +1,6 @@
1
1
  # Autotouch CLI Reference
2
2
 
3
- Generated from the installed parser for `autotouch-cli` `0.2.59`.
3
+ Generated from the installed parser for `autotouch-cli` `0.2.60`.
4
4
  Manifest schema version: `2`.
5
5
 
6
6
  ## Output Modes
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.2.59",
2
+ "version": "0.2.60",
3
3
  "manifest_schema_version": 2,
4
4
  "entry_points": {
5
5
  "autotouch": "autotouch_cli.cli:main",
@@ -35,11 +35,11 @@ COLUMN_RECIPE_BASE_TYPES = tuple(recipe_types())
35
35
  COLUMN_RECIPE_ALIASES: Dict[str, Dict[str, Any]] = {
36
36
  "add_to_leads": {
37
37
  "canonical": "add_to_crm",
38
- "key": "add_to_leads",
39
- "label": "Add to Leads",
38
+ "key": "sync_to_leads",
39
+ "label": "Sync to Leads",
40
40
  "notes": [
41
- "CLI alias for the canonical add_to_crm provider recipe.",
42
- "Use this when the operator language is 'Add to Leads'. Internal provider/config names remain add_to_crm.",
41
+ "Legacy CLI alias for the canonical add_to_crm provider recipe.",
42
+ "Emits the standard Sync to Leads payload. Internal provider/config names remain add_to_crm.",
43
43
  ],
44
44
  },
45
45
  "sync_to_leads": {
@@ -47,8 +47,8 @@ COLUMN_RECIPE_ALIASES: Dict[str, Dict[str, Any]] = {
47
47
  "key": "sync_to_leads",
48
48
  "label": "Sync to Leads",
49
49
  "notes": [
50
- "CLI alias for the canonical add_to_crm provider recipe.",
51
- "Use this when the operator language is 'Sync to Leads'. Internal provider/config names remain add_to_crm.",
50
+ "Preferred CLI alias for the canonical add_to_crm provider recipe.",
51
+ "Emits the standard Sync to Leads payload. Internal provider/config names remain add_to_crm.",
52
52
  ],
53
53
  },
54
54
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: autotouch-cli
3
- Version: 0.2.59
3
+ Version: 0.2.60
4
4
  Summary: Autotouch Smart Table CLI
5
5
  Project-URL: Homepage, https://app.autotouch.ai
6
6
  Project-URL: Documentation, https://github.com/nicolonic/autotouch_main/tree/main/docs/research-table/reference
@@ -746,20 +746,21 @@ _PROVIDER_CONTRACTS: Tuple[ResearchTableProviderContract, ...] = (
746
746
  ),
747
747
  ResearchTableProviderContract(
748
748
  key="add_to_crm",
749
- display_name="Add to Leads",
749
+ display_name="Sync to Leads",
750
750
  kind="enrichment",
751
751
  behavior_class="action_enrichment",
752
752
  provider_aliases=("add_to_crm",),
753
753
  recipe_type="add_to_crm",
754
754
  recipe_payload={
755
- "key": "add_to_leads",
756
- "label": "Add to Leads",
755
+ "key": "sync_to_leads",
756
+ "label": "Sync to Leads",
757
757
  "kind": "enrichment",
758
758
  "dataType": "json",
759
759
  "origin": "manual",
760
760
  "autoRun": "onSourceUpdate",
761
761
  "config": {
762
762
  "provider": "add_to_crm",
763
+ "requireCompanyDomain": True,
763
764
  "leadSource": "research_table_export",
764
765
  "fieldMappings": {
765
766
  "mode": "single",
@@ -786,18 +787,19 @@ _PROVIDER_CONTRACTS: Tuple[ResearchTableProviderContract, ...] = (
786
787
  },
787
788
  recipe_notes=(
788
789
  "companyDomain is required; LinkedIn is optional.",
790
+ "Set `config.requireCompanyDomain=false` only for single-mode LinkedIn-first syncs. In that relaxed mode, `linkedinUrl` becomes required and multi/array mode stays unsupported.",
789
791
  "Include at least one usable hard-identity mapping such as LinkedIn, email, or phone.",
790
792
  "If companyDomain is missing in the table, derive or enrich that domain column first.",
791
793
  "Primitive source columns map directly. If a mapped source column stores structured JSON, send an object with column + path, for example {\"column\": \"work_email\", \"path\": \"response\"} or {\"column\": \"mobile_phone\", \"path\": \"mobile_number\"}.",
792
794
  "For firstName and lastName, prefer authoritative structured source fields when available, for example {\"column\": \"linkedin_profile_lookup_raw\", \"path\": \"first_name\"} and {\"column\": \"linkedin_profile_lookup_raw\", \"path\": \"last_name\"}. Avoid mapping a combined full-name column directly unless that is an intentional fallback and you accept imperfect CRM data.",
793
- "Add to CRM is optional and non-billable.",
794
- "Creating add_to_crm after upstream email/phone/lead columns already finished does not replay those older source updates; run add_to_crm explicitly or rerun the upstream source column.",
795
+ "Sync to Leads is non-billable.",
796
+ "Creating Sync to Leads (`add_to_crm`) after upstream email/phone/lead columns already finished does not replay those older source updates; run Sync to Leads explicitly or rerun the upstream source column.",
795
797
  ),
796
798
  column_type={
797
799
  "type": "add_to_crm",
798
800
  "kind": "enrichment",
799
801
  "runnable": True,
800
- "description": "Add eligible mapped rows into Leads CRM from research-table data.",
802
+ "description": "Sync eligible mapped rows into Leads from research-table data.",
801
803
  "requirements": {
802
804
  "config_provider": "add_to_crm",
803
805
  "field_mappings": "required",
@@ -806,7 +808,7 @@ _PROVIDER_CONTRACTS: Tuple[ResearchTableProviderContract, ...] = (
806
808
  "billing": {
807
809
  "billable": False,
808
810
  "credits": 0,
809
- "notes": "Add to Leads export does not consume credits.",
811
+ "notes": "Sync to Leads does not consume credits.",
810
812
  },
811
813
  },
812
814
  setup_contract={
@@ -971,14 +973,14 @@ _PROVIDER_CONTRACTS: Tuple[ResearchTableProviderContract, ...] = (
971
973
  "config": {
972
974
  "provider": "add_to_sequence",
973
975
  "sequenceId": "<SEQUENCE_ID>",
974
- "sourceLeadColumn": "add_to_leads",
976
+ "sourceLeadColumn": "sync_to_leads",
975
977
  },
976
978
  },
977
979
  recipe_notes=(
978
- "sourceLeadColumn should reference the source column key that stores lead IDs (for example add_to_leads), not the provider name add_to_crm.",
980
+ "sourceLeadColumn should reference the source column key that stores lead IDs (for example sync_to_leads), not the provider name add_to_crm.",
979
981
  "Set sequenceId to the target workflow sequence ID.",
980
982
  "The target sequence must already be ACTIVE for real enrollment.",
981
- "Common tail order after contact rows or lead IDs exist is email_finder -> add_to_crm -> create/activate sequence -> add_to_sequence.",
983
+ "Common tail order after contact rows or lead IDs exist is email_finder -> Sync to Leads (`add_to_crm`) -> create/activate sequence -> add_to_sequence.",
982
984
  "Creating add_to_sequence after lead IDs already exist does not replay those older updates; run add_to_sequence explicitly or rerun the lead-id source column.",
983
985
  ),
984
986
  column_type={
@@ -1366,7 +1368,7 @@ def validate_column_provider_contract(
1366
1368
  raise ProviderContractValidationError(
1367
1369
  "missing_required_field",
1368
1370
  "add_to_sequence requires config.sourceLeadColumn.",
1369
- hint="Set sourceLeadColumn to the column key that stores lead IDs, usually add_to_leads.",
1371
+ hint="Set sourceLeadColumn to the column key that stores lead IDs, usually sync_to_leads.",
1370
1372
  )
1371
1373
  return contract
1372
1374
 
@@ -1378,6 +1380,29 @@ def validate_column_provider_contract(
1378
1380
  "add_to_crm requires config.fieldMappings.",
1379
1381
  hint="Use `autotouch columns recipe --type add_to_crm`.",
1380
1382
  )
1383
+ require_company_domain = cfg.get("requireCompanyDomain", True)
1384
+ if not isinstance(require_company_domain, bool):
1385
+ raise ProviderContractValidationError(
1386
+ "invalid_field",
1387
+ "add_to_crm config.requireCompanyDomain must be a boolean when provided.",
1388
+ hint="Use true for the default strict mode or false for single-mode LinkedIn-first sync.",
1389
+ detail={"requireCompanyDomain": require_company_domain},
1390
+ )
1391
+ if require_company_domain is False:
1392
+ mode = _normalize(field_mappings.get("mode")) or "single"
1393
+ if mode in {"multi", "array"}:
1394
+ raise ProviderContractValidationError(
1395
+ "invalid_field",
1396
+ "add_to_crm config.requireCompanyDomain=false only supports single-mode fieldMappings.",
1397
+ hint="Use single-mode sync_to_leads with config.fieldMappings.linkedinUrl when companyDomain is optional.",
1398
+ detail={"mode": field_mappings.get("mode")},
1399
+ )
1400
+ if not _mapping_text(field_mappings.get("linkedinUrl")):
1401
+ raise ProviderContractValidationError(
1402
+ "missing_required_field",
1403
+ "add_to_crm with config.requireCompanyDomain=false requires config.fieldMappings.linkedinUrl.",
1404
+ hint="Map a LinkedIn profile URL column when using LinkedIn-first sync_to_leads.",
1405
+ )
1381
1406
  return contract
1382
1407
 
1383
1408
  if contract.key == "sync_to_table":
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "autotouch-cli"
7
- version = "0.2.59"
7
+ version = "0.2.60"
8
8
  description = "Autotouch Smart Table CLI"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
File without changes
File without changes