autotouch-cli 0.2.27__tar.gz → 0.2.28__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.27 → autotouch_cli-0.2.28}/PKG-INFO +1 -1
- autotouch_cli-0.2.28/autotouch_cli/__init__.py +1 -0
- {autotouch_cli-0.2.27 → autotouch_cli-0.2.28}/autotouch_cli.egg-info/PKG-INFO +1 -1
- autotouch_cli-0.2.28/autotouch_cli.egg-info/SOURCES.txt +18 -0
- autotouch_cli-0.2.28/autotouch_cli.egg-info/entry_points.txt +3 -0
- autotouch_cli-0.2.28/autotouch_cli.egg-info/top_level.txt +1 -0
- {autotouch_cli-0.2.27 → autotouch_cli-0.2.28}/pyproject.toml +4 -4
- autotouch_cli-0.2.27/autotouch_cli.egg-info/SOURCES.txt +0 -63
- autotouch_cli-0.2.27/autotouch_cli.egg-info/entry_points.txt +0 -3
- autotouch_cli-0.2.27/autotouch_cli.egg-info/top_level.txt +0 -1
- autotouch_cli-0.2.27/scripts/__init__.py +0 -1
- autotouch_cli-0.2.27/scripts/add_column_unique_index.py +0 -112
- autotouch_cli-0.2.27/scripts/attach_csv_import_leads_to_research_table.py +0 -130
- autotouch_cli-0.2.27/scripts/bundle_sequences_backend.py +0 -250
- autotouch_cli-0.2.27/scripts/check_agent_traces.py +0 -125
- autotouch_cli-0.2.27/scripts/check_column_mode.py +0 -87
- autotouch_cli-0.2.27/scripts/exit_terminal_leads_from_sequences.py +0 -302
- autotouch_cli-0.2.27/scripts/fetch_lead.py +0 -12
- autotouch_cli-0.2.27/scripts/fix_lead_titles_from_csv.py +0 -217
- autotouch_cli-0.2.27/scripts/migrations/20250106_add_column_position.py +0 -66
- autotouch_cli-0.2.27/scripts/migrations/20250108_fix_legacy_column_fields.py +0 -91
- autotouch_cli-0.2.27/scripts/migrations/20250109_add_user_fields_to_tables.py +0 -84
- autotouch_cli-0.2.27/scripts/migrations/20250117_add_call_logs_webhook_indexes.py +0 -84
- autotouch_cli-0.2.27/scripts/migrations/20250117_rename_call_logs_collection.py +0 -91
- autotouch_cli-0.2.27/scripts/migrations/20250119_create_leads_unique_email_index.py +0 -263
- autotouch_cli-0.2.27/scripts/migrations/20250123_add_filter_indexes.py +0 -134
- autotouch_cli-0.2.27/scripts/migrations/20250123_add_llm_responses_collection.py +0 -108
- autotouch_cli-0.2.27/scripts/migrations/20250128_migrate_user_ids_to_objectid.py +0 -166
- autotouch_cli-0.2.27/scripts/migrations/20250208_backfill_task_research_values.py +0 -150
- autotouch_cli-0.2.27/scripts/migrations/20250604_add_origin_indexes.py +0 -52
- autotouch_cli-0.2.27/scripts/migrations/20250608_cleanup_agent_metadata.py +0 -85
- autotouch_cli-0.2.27/scripts/migrations/20250608_rename_agent_metadata_to_metadata.py +0 -103
- autotouch_cli-0.2.27/scripts/migrations/20250922_add_activity_indexes.py +0 -215
- autotouch_cli-0.2.27/scripts/migrations/20250926_migrate_single_to_arrays.py +0 -465
- autotouch_cli-0.2.27/scripts/migrations/20250928_add_missing_timestamp_fields.py +0 -133
- autotouch_cli-0.2.27/scripts/migrations/20250929_add_task_join_indexes.py +0 -245
- autotouch_cli-0.2.27/scripts/migrations/20250929_add_task_join_indexes_safe.py +0 -107
- autotouch_cli-0.2.27/scripts/migrations/20250929_create_shared_phone_cache.py +0 -213
- autotouch_cli-0.2.27/scripts/migrations/20251007_add_rows_position_id_index.py +0 -46
- autotouch_cli-0.2.27/scripts/migrations/20251109_add_ttl_for_llm_and_preview_traces.py +0 -99
- autotouch_cli-0.2.27/scripts/migrations/20260113_normalize_table_filter_operators.py +0 -113
- autotouch_cli-0.2.27/scripts/migrations/20260113_set_user_permissions_user_admin.py +0 -100
- autotouch_cli-0.2.27/scripts/migrations/20260204_sync_lead_owner_from_tasks.py +0 -269
- autotouch_cli-0.2.27/scripts/migrations/20260303_add_webhook_subscription_collections.py +0 -124
- autotouch_cli-0.2.27/scripts/migrations/20260305_force_formatter_autorun_on_source_update.py +0 -98
- autotouch_cli-0.2.27/scripts/migrations/20260306_migrate_lead_identity_v1.py +0 -538
- autotouch_cli-0.2.27/scripts/migrations/migrate_org_user_credits.py +0 -117
- autotouch_cli-0.2.27/scripts/migrations/set_default_lead_status.py +0 -49
- autotouch_cli-0.2.27/scripts/migrations/update_lead_owner_from_tasks.py +0 -152
- autotouch_cli-0.2.27/scripts/reassign_sequence_owner.py +0 -136
- autotouch_cli-0.2.27/scripts/run_sidecar_orchestrator_demo.py +0 -106
- autotouch_cli-0.2.27/scripts/test_crm_company_policy.py +0 -277
- autotouch_cli-0.2.27/scripts/test_sequences_instantly_e2e.py +0 -249
- autotouch_cli-0.2.27/scripts/test_sequences_personal_e2e.py +0 -215
- autotouch_cli-0.2.27/scripts/test_task_error_logger.py +0 -44
- autotouch_cli-0.2.27/scripts/verify_azurite_voicemail.py +0 -64
- /autotouch_cli-0.2.27/scripts/smart_table_cli.py → /autotouch_cli-0.2.28/autotouch_cli/cli.py +0 -0
- {autotouch_cli-0.2.27 → autotouch_cli-0.2.28}/autotouch_cli.egg-info/dependency_links.txt +0 -0
- {autotouch_cli-0.2.27 → autotouch_cli-0.2.28}/autotouch_cli.egg-info/requires.txt +0 -0
- {autotouch_cli-0.2.27 → autotouch_cli-0.2.28}/docs/research-table/reference/autotouch-cli.md +0 -0
- {autotouch_cli-0.2.27 → autotouch_cli-0.2.28}/setup.cfg +0 -0
- {autotouch_cli-0.2.27 → autotouch_cli-0.2.28}/tests/test_contactout_custom.py +0 -0
- {autotouch_cli-0.2.27 → autotouch_cli-0.2.28}/tests/test_contactout_integration.py +0 -0
- {autotouch_cli-0.2.27 → autotouch_cli-0.2.28}/tests/test_contactout_multi_titles.py +0 -0
- {autotouch_cli-0.2.27 → autotouch_cli-0.2.28}/tests/test_contactout_pipeline.py +0 -0
- {autotouch_cli-0.2.27 → autotouch_cli-0.2.28}/tests/test_contactout_simple.py +0 -0
- {autotouch_cli-0.2.27 → autotouch_cli-0.2.28}/tests/test_contactout_v2_bulk.py +0 -0
- {autotouch_cli-0.2.27 → autotouch_cli-0.2.28}/tests/test_lead_required_fields.py +0 -0
- {autotouch_cli-0.2.27 → autotouch_cli-0.2.28}/tests/test_phone_provider_pipeline.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Autotouch CLI package."""
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
pyproject.toml
|
|
2
|
+
autotouch_cli/__init__.py
|
|
3
|
+
autotouch_cli/cli.py
|
|
4
|
+
autotouch_cli.egg-info/PKG-INFO
|
|
5
|
+
autotouch_cli.egg-info/SOURCES.txt
|
|
6
|
+
autotouch_cli.egg-info/dependency_links.txt
|
|
7
|
+
autotouch_cli.egg-info/entry_points.txt
|
|
8
|
+
autotouch_cli.egg-info/requires.txt
|
|
9
|
+
autotouch_cli.egg-info/top_level.txt
|
|
10
|
+
docs/research-table/reference/autotouch-cli.md
|
|
11
|
+
tests/test_contactout_custom.py
|
|
12
|
+
tests/test_contactout_integration.py
|
|
13
|
+
tests/test_contactout_multi_titles.py
|
|
14
|
+
tests/test_contactout_pipeline.py
|
|
15
|
+
tests/test_contactout_simple.py
|
|
16
|
+
tests/test_contactout_v2_bulk.py
|
|
17
|
+
tests/test_lead_required_fields.py
|
|
18
|
+
tests/test_phone_provider_pipeline.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
autotouch_cli
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "autotouch-cli"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.28"
|
|
8
8
|
description = "Autotouch Smart Table CLI"
|
|
9
9
|
readme = "docs/research-table/reference/autotouch-cli.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -14,9 +14,9 @@ dependencies = [
|
|
|
14
14
|
]
|
|
15
15
|
|
|
16
16
|
[project.scripts]
|
|
17
|
-
autotouch = "
|
|
18
|
-
smart-table = "
|
|
17
|
+
autotouch = "autotouch_cli.cli:main"
|
|
18
|
+
smart-table = "autotouch_cli.cli:main"
|
|
19
19
|
|
|
20
20
|
[tool.setuptools.packages.find]
|
|
21
21
|
where = ["."]
|
|
22
|
-
include = ["
|
|
22
|
+
include = ["autotouch_cli*"]
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
pyproject.toml
|
|
2
|
-
autotouch_cli.egg-info/PKG-INFO
|
|
3
|
-
autotouch_cli.egg-info/SOURCES.txt
|
|
4
|
-
autotouch_cli.egg-info/dependency_links.txt
|
|
5
|
-
autotouch_cli.egg-info/entry_points.txt
|
|
6
|
-
autotouch_cli.egg-info/requires.txt
|
|
7
|
-
autotouch_cli.egg-info/top_level.txt
|
|
8
|
-
docs/research-table/reference/autotouch-cli.md
|
|
9
|
-
scripts/__init__.py
|
|
10
|
-
scripts/add_column_unique_index.py
|
|
11
|
-
scripts/attach_csv_import_leads_to_research_table.py
|
|
12
|
-
scripts/bundle_sequences_backend.py
|
|
13
|
-
scripts/check_agent_traces.py
|
|
14
|
-
scripts/check_column_mode.py
|
|
15
|
-
scripts/exit_terminal_leads_from_sequences.py
|
|
16
|
-
scripts/fetch_lead.py
|
|
17
|
-
scripts/fix_lead_titles_from_csv.py
|
|
18
|
-
scripts/reassign_sequence_owner.py
|
|
19
|
-
scripts/run_sidecar_orchestrator_demo.py
|
|
20
|
-
scripts/smart_table_cli.py
|
|
21
|
-
scripts/test_crm_company_policy.py
|
|
22
|
-
scripts/test_sequences_instantly_e2e.py
|
|
23
|
-
scripts/test_sequences_personal_e2e.py
|
|
24
|
-
scripts/test_task_error_logger.py
|
|
25
|
-
scripts/verify_azurite_voicemail.py
|
|
26
|
-
scripts/migrations/20250106_add_column_position.py
|
|
27
|
-
scripts/migrations/20250108_fix_legacy_column_fields.py
|
|
28
|
-
scripts/migrations/20250109_add_user_fields_to_tables.py
|
|
29
|
-
scripts/migrations/20250117_add_call_logs_webhook_indexes.py
|
|
30
|
-
scripts/migrations/20250117_rename_call_logs_collection.py
|
|
31
|
-
scripts/migrations/20250119_create_leads_unique_email_index.py
|
|
32
|
-
scripts/migrations/20250123_add_filter_indexes.py
|
|
33
|
-
scripts/migrations/20250123_add_llm_responses_collection.py
|
|
34
|
-
scripts/migrations/20250128_migrate_user_ids_to_objectid.py
|
|
35
|
-
scripts/migrations/20250208_backfill_task_research_values.py
|
|
36
|
-
scripts/migrations/20250604_add_origin_indexes.py
|
|
37
|
-
scripts/migrations/20250608_cleanup_agent_metadata.py
|
|
38
|
-
scripts/migrations/20250608_rename_agent_metadata_to_metadata.py
|
|
39
|
-
scripts/migrations/20250922_add_activity_indexes.py
|
|
40
|
-
scripts/migrations/20250926_migrate_single_to_arrays.py
|
|
41
|
-
scripts/migrations/20250928_add_missing_timestamp_fields.py
|
|
42
|
-
scripts/migrations/20250929_add_task_join_indexes.py
|
|
43
|
-
scripts/migrations/20250929_add_task_join_indexes_safe.py
|
|
44
|
-
scripts/migrations/20250929_create_shared_phone_cache.py
|
|
45
|
-
scripts/migrations/20251007_add_rows_position_id_index.py
|
|
46
|
-
scripts/migrations/20251109_add_ttl_for_llm_and_preview_traces.py
|
|
47
|
-
scripts/migrations/20260113_normalize_table_filter_operators.py
|
|
48
|
-
scripts/migrations/20260113_set_user_permissions_user_admin.py
|
|
49
|
-
scripts/migrations/20260204_sync_lead_owner_from_tasks.py
|
|
50
|
-
scripts/migrations/20260303_add_webhook_subscription_collections.py
|
|
51
|
-
scripts/migrations/20260305_force_formatter_autorun_on_source_update.py
|
|
52
|
-
scripts/migrations/20260306_migrate_lead_identity_v1.py
|
|
53
|
-
scripts/migrations/migrate_org_user_credits.py
|
|
54
|
-
scripts/migrations/set_default_lead_status.py
|
|
55
|
-
scripts/migrations/update_lead_owner_from_tasks.py
|
|
56
|
-
tests/test_contactout_custom.py
|
|
57
|
-
tests/test_contactout_integration.py
|
|
58
|
-
tests/test_contactout_multi_titles.py
|
|
59
|
-
tests/test_contactout_pipeline.py
|
|
60
|
-
tests/test_contactout_simple.py
|
|
61
|
-
tests/test_contactout_v2_bulk.py
|
|
62
|
-
tests/test_lead_required_fields.py
|
|
63
|
-
tests/test_phone_provider_pipeline.py
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
scripts
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
# CLI/script package marker for setuptools console entry points.
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Add unique index on (table_id, key) to columns collection.
|
|
4
|
-
This prevents duplicate column keys within the same table.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import asyncio
|
|
8
|
-
from motor.motor_asyncio import AsyncIOMotorClient
|
|
9
|
-
from pymongo import ASCENDING
|
|
10
|
-
from pymongo.errors import DuplicateKeyError
|
|
11
|
-
import os
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
async def add_unique_index(db):
|
|
15
|
-
"""Add unique compound index on (table_id, key) to prevent duplicates."""
|
|
16
|
-
|
|
17
|
-
print("Adding unique index on columns collection...")
|
|
18
|
-
|
|
19
|
-
try:
|
|
20
|
-
# List existing indexes
|
|
21
|
-
existing_indexes = await db.columns.list_indexes().to_list(None)
|
|
22
|
-
print(f"Existing indexes: {[idx['name'] for idx in existing_indexes]}")
|
|
23
|
-
|
|
24
|
-
# Check if index already exists
|
|
25
|
-
index_exists = any(
|
|
26
|
-
idx.get('name') == 'table_id_key_unique'
|
|
27
|
-
for idx in existing_indexes
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
if index_exists:
|
|
31
|
-
print("Index 'table_id_key_unique' already exists")
|
|
32
|
-
return True
|
|
33
|
-
|
|
34
|
-
# Create unique compound index
|
|
35
|
-
result = await db.columns.create_index(
|
|
36
|
-
[('table_id', ASCENDING), ('key', ASCENDING)],
|
|
37
|
-
unique=True,
|
|
38
|
-
name='table_id_key_unique'
|
|
39
|
-
)
|
|
40
|
-
print(f"Successfully created unique index: {result}")
|
|
41
|
-
return True
|
|
42
|
-
|
|
43
|
-
except DuplicateKeyError as e:
|
|
44
|
-
print(f"ERROR: Cannot create unique index due to duplicate keys!")
|
|
45
|
-
print(f"Error details: {e}")
|
|
46
|
-
|
|
47
|
-
# Find and report duplicates
|
|
48
|
-
pipeline = [
|
|
49
|
-
{'$group': {
|
|
50
|
-
'_id': {'table_id': '$table_id', 'key': '$key'},
|
|
51
|
-
'count': {'$sum': 1},
|
|
52
|
-
'ids': {'$push': '$_id'},
|
|
53
|
-
'names': {'$push': '$name'}
|
|
54
|
-
}},
|
|
55
|
-
{'$match': {'count': {'$gt': 1}}}
|
|
56
|
-
]
|
|
57
|
-
|
|
58
|
-
duplicates = await db.columns.aggregate(pipeline).to_list(None)
|
|
59
|
-
|
|
60
|
-
if duplicates:
|
|
61
|
-
print(f"\nFound {len(duplicates)} duplicate (table_id, key) pairs:")
|
|
62
|
-
for i, dup in enumerate(duplicates):
|
|
63
|
-
print(f"\n{i+1}. Table: {dup['_id']['table_id']}, Key: '{dup['_id']['key']}'")
|
|
64
|
-
print(f" Count: {dup['count']}")
|
|
65
|
-
print(f" IDs: {[str(id) for id in dup['ids']]}")
|
|
66
|
-
print(f" Names: {dup['names']}")
|
|
67
|
-
|
|
68
|
-
print("\nPlease run the fix_duplicate_columns.py script first to clean up duplicates.")
|
|
69
|
-
|
|
70
|
-
return False
|
|
71
|
-
|
|
72
|
-
except Exception as e:
|
|
73
|
-
print(f"ERROR: Failed to create index - {e}")
|
|
74
|
-
return False
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
async def main():
|
|
78
|
-
"""Run the index creation."""
|
|
79
|
-
|
|
80
|
-
# Get MongoDB URL from environment or use default
|
|
81
|
-
mongodb_url = os.getenv('MONGODB_URL', 'mongodb://localhost:27017')
|
|
82
|
-
db_name = os.getenv('MONGODB_DB', 'smart_table')
|
|
83
|
-
|
|
84
|
-
print(f"Connecting to MongoDB at {mongodb_url}/{db_name}")
|
|
85
|
-
|
|
86
|
-
# Connect to MongoDB
|
|
87
|
-
client = AsyncIOMotorClient(mongodb_url)
|
|
88
|
-
db = client[db_name]
|
|
89
|
-
|
|
90
|
-
try:
|
|
91
|
-
# Test connection
|
|
92
|
-
await db.command('ping')
|
|
93
|
-
print("Successfully connected to MongoDB\n")
|
|
94
|
-
|
|
95
|
-
# Add the unique index
|
|
96
|
-
success = await add_unique_index(db)
|
|
97
|
-
|
|
98
|
-
if success:
|
|
99
|
-
print("\nIndex creation completed successfully!")
|
|
100
|
-
print("Duplicate column keys are now prevented at the database level.")
|
|
101
|
-
else:
|
|
102
|
-
print("\nIndex creation failed. Please resolve issues and try again.")
|
|
103
|
-
|
|
104
|
-
except Exception as e:
|
|
105
|
-
print(f"\nERROR: {e}")
|
|
106
|
-
raise
|
|
107
|
-
finally:
|
|
108
|
-
client.close()
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if __name__ == '__main__':
|
|
112
|
-
asyncio.run(main())
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Attach (link) existing CRM leads to a research table by adding the table id to
|
|
4
|
-
Lead.research_table_ids[].
|
|
5
|
-
|
|
6
|
-
This is the lightweight way to make CSV-imported leads show up when filtering
|
|
7
|
-
Engage by a research table with source_table_scope=lead.
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
from __future__ import annotations
|
|
11
|
-
|
|
12
|
-
import argparse
|
|
13
|
-
import os
|
|
14
|
-
import sys
|
|
15
|
-
from typing import Any, Dict, List, Optional
|
|
16
|
-
|
|
17
|
-
import certifi
|
|
18
|
-
from bson import ObjectId
|
|
19
|
-
from pymongo import MongoClient
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def _coerce_oid(s: str) -> Optional[ObjectId]:
|
|
23
|
-
try:
|
|
24
|
-
return ObjectId(s)
|
|
25
|
-
except Exception:
|
|
26
|
-
return None
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def _mongo_db(uri: str, db_name: str):
|
|
30
|
-
# Atlas requires a CA bundle in some environments.
|
|
31
|
-
return MongoClient(uri, serverSelectionTimeoutMS=20_000, tlsCAFile=certifi.where())[db_name]
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def main(argv: Optional[List[str]] = None) -> int:
|
|
35
|
-
p = argparse.ArgumentParser(
|
|
36
|
-
description="Attach CSV-imported leads to a research table via research_table_ids[]",
|
|
37
|
-
)
|
|
38
|
-
p.add_argument("--org-id", required=True, help="Organization UUID (string)")
|
|
39
|
-
p.add_argument("--table-id", required=True, help="Research table _id (ObjectId string)")
|
|
40
|
-
p.add_argument(
|
|
41
|
-
"--source",
|
|
42
|
-
default="csv_import",
|
|
43
|
-
help="Match lead_source/source value (default: csv_import)",
|
|
44
|
-
)
|
|
45
|
-
p.add_argument("--uri", default=os.getenv("MONGODB_URI", ""), help="MongoDB connection string")
|
|
46
|
-
p.add_argument("--db-name", default=os.getenv("MONGODB_DB_NAME", "autotouch"))
|
|
47
|
-
p.add_argument("--dry-run", action="store_true", help="Print what would change without writing")
|
|
48
|
-
p.add_argument("--limit", type=int, default=0, help="Optional limit for update (0 = no limit)")
|
|
49
|
-
args = p.parse_args(argv)
|
|
50
|
-
|
|
51
|
-
uri = args.uri.strip() or os.getenv("MONGODB_PROD_CONNECTION_STRING", "").strip()
|
|
52
|
-
if not uri:
|
|
53
|
-
print("Missing MongoDB URI (pass --uri or set MONGODB_URI / MONGODB_PROD_CONNECTION_STRING).", file=sys.stderr)
|
|
54
|
-
return 2
|
|
55
|
-
|
|
56
|
-
org_id = args.org_id.strip()
|
|
57
|
-
table_id_str = args.table_id.strip()
|
|
58
|
-
source = args.source.strip()
|
|
59
|
-
|
|
60
|
-
db = _mongo_db(uri, args.db_name)
|
|
61
|
-
|
|
62
|
-
# Validate table exists and belongs to org (defensive safety check).
|
|
63
|
-
table_oid = _coerce_oid(table_id_str)
|
|
64
|
-
table = db.tables.find_one(
|
|
65
|
-
{"_id": table_oid or table_id_str, "organization_id": org_id},
|
|
66
|
-
{"name": 1, "organization_id": 1, "status": 1},
|
|
67
|
-
)
|
|
68
|
-
if not table:
|
|
69
|
-
print(f"Table not found for org_id={org_id} table_id={table_id_str}", file=sys.stderr)
|
|
70
|
-
return 1
|
|
71
|
-
if table.get("status") in {"deleting", "deleted"}:
|
|
72
|
-
print(f"Refusing to attach to table in status={table.get('status')}", file=sys.stderr)
|
|
73
|
-
return 1
|
|
74
|
-
|
|
75
|
-
variants: List[Any] = [table_id_str]
|
|
76
|
-
if table_oid is not None:
|
|
77
|
-
variants.append(table_oid)
|
|
78
|
-
|
|
79
|
-
base_org = {"$or": [{"organization_id": org_id}, {"org_id": org_id}]}
|
|
80
|
-
base_source = {"$or": [{"lead_source": source}, {"source": source}]}
|
|
81
|
-
query: Dict[str, Any] = {
|
|
82
|
-
"$and": [
|
|
83
|
-
base_org,
|
|
84
|
-
base_source,
|
|
85
|
-
{"research_table_ids": {"$nin": variants}},
|
|
86
|
-
]
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
total_candidates = db.leads.count_documents({"$and": [base_org, base_source]})
|
|
90
|
-
to_update = db.leads.count_documents(query)
|
|
91
|
-
|
|
92
|
-
table_name = table.get("name") or ""
|
|
93
|
-
print(f"Table: {table_id_str} {table_name!r}")
|
|
94
|
-
print(f"Org: {org_id}")
|
|
95
|
-
print(f"Match: lead_source/source == {source!r}")
|
|
96
|
-
print(f"Leads matching source in org: {total_candidates}")
|
|
97
|
-
print(f"Leads missing table link: {to_update}")
|
|
98
|
-
|
|
99
|
-
if to_update == 0:
|
|
100
|
-
return 0
|
|
101
|
-
|
|
102
|
-
sample = list(
|
|
103
|
-
db.leads.find(query, {"_id": 1, "company_domain": 1, "linkedin_url": 1, "lead_source": 1, "created_at": 1})
|
|
104
|
-
.sort("created_at", -1)
|
|
105
|
-
.limit(5)
|
|
106
|
-
)
|
|
107
|
-
if sample:
|
|
108
|
-
print("Sample leads to update (max 5):")
|
|
109
|
-
for d in sample:
|
|
110
|
-
print(f"- {str(d.get('_id'))}\t{d.get('company_domain','')}\t{d.get('linkedin_url','')}")
|
|
111
|
-
|
|
112
|
-
if args.dry_run:
|
|
113
|
-
print("Dry run: no writes performed.")
|
|
114
|
-
return 0
|
|
115
|
-
|
|
116
|
-
update = {"$addToSet": {"research_table_ids": table_id_str}}
|
|
117
|
-
if args.limit and args.limit > 0:
|
|
118
|
-
# Apply a limit by selecting ids first. (Mongo update_many has no limit.)
|
|
119
|
-
ids = [d["_id"] for d in db.leads.find(query, {"_id": 1}).limit(int(args.limit))]
|
|
120
|
-
res = db.leads.update_many({"_id": {"$in": ids}}, update)
|
|
121
|
-
else:
|
|
122
|
-
res = db.leads.update_many(query, update)
|
|
123
|
-
|
|
124
|
-
print(f"Updated: matched={res.matched_count} modified={res.modified_count}")
|
|
125
|
-
return 0
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if __name__ == "__main__":
|
|
129
|
-
raise SystemExit(main())
|
|
130
|
-
|
|
@@ -1,250 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Bundle relevant backend source files into a single labeled text file for LLM sharing.
|
|
4
|
-
|
|
5
|
-
Usage:
|
|
6
|
-
python scripts/bundle_sequences_backend.py \
|
|
7
|
-
--output docs/shares/sequences_backend_bundle.txt
|
|
8
|
-
|
|
9
|
-
Options:
|
|
10
|
-
--output PATH Output file (default: docs/shares/sequences_backend_bundle.txt)
|
|
11
|
-
--stdout Print to stdout instead of writing a file
|
|
12
|
-
--extra PATH ... Additional file paths to include (repeatable)
|
|
13
|
-
--auto Auto-discover relevant backend files (keywords scan)
|
|
14
|
-
--core Force core-only (DEFAULT_FILES) and ignore --auto
|
|
15
|
-
--max-lines-per-file N Truncate each file to the first N lines (annotation added)
|
|
16
|
-
--strip-comments Remove full-line comments and leading module docstrings; collapse blank lines
|
|
17
|
-
--root PATH Project root (default: cwd)
|
|
18
|
-
|
|
19
|
-
By default, it includes the core Sequences pipeline files:
|
|
20
|
-
- apps/api/routers/sequences.py
|
|
21
|
-
- apps/api/services/sequence_service.py
|
|
22
|
-
- apps/api/services/sequence_enrollment_service.py
|
|
23
|
-
- apps/api/services/research_snapshot_service.py
|
|
24
|
-
- apps/api/models/sequence.py
|
|
25
|
-
- apps/api/models/sequence_enrollment.py
|
|
26
|
-
- apps/api/models/collections.py
|
|
27
|
-
- apps/api/main.py
|
|
28
|
-
"""
|
|
29
|
-
|
|
30
|
-
from __future__ import annotations
|
|
31
|
-
|
|
32
|
-
import argparse
|
|
33
|
-
import os
|
|
34
|
-
from pathlib import Path
|
|
35
|
-
from datetime import datetime
|
|
36
|
-
from typing import Iterable, List, Set, Optional, Tuple
|
|
37
|
-
|
|
38
|
-
DEFAULT_FILES: List[str] = [
|
|
39
|
-
"apps/api/routers/sequences.py",
|
|
40
|
-
"apps/api/services/sequence_service.py",
|
|
41
|
-
"apps/api/services/sequence_enrollment_service.py",
|
|
42
|
-
"apps/api/services/research_snapshot_service.py",
|
|
43
|
-
"apps/api/models/sequence.py",
|
|
44
|
-
"apps/api/models/sequence_enrollment.py",
|
|
45
|
-
"apps/api/models/collections.py",
|
|
46
|
-
"apps/api/main.py",
|
|
47
|
-
]
|
|
48
|
-
|
|
49
|
-
SEPARATOR_LINE = "=" * 60
|
|
50
|
-
SUBSEP_LINE = "-" * 60
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def _strip_leading_docstring(text: str) -> Tuple[str, bool]:
|
|
54
|
-
"""Remove a top-level triple-quoted module docstring if present."""
|
|
55
|
-
import re
|
|
56
|
-
# Match at very start, optional encoding/comments/blank lines ignored is complicated;
|
|
57
|
-
# keep it simple: only strip if the very first non-whitespace chars are a triple quote.
|
|
58
|
-
s = text.lstrip()
|
|
59
|
-
leading_ws_len = len(text) - len(s)
|
|
60
|
-
if s.startswith('"""') or s.startswith("'''"):
|
|
61
|
-
quote = s[:3]
|
|
62
|
-
end = s.find(quote, 3)
|
|
63
|
-
if end != -1:
|
|
64
|
-
stripped = s[end + 3 :]
|
|
65
|
-
return ("\n" * (1 if leading_ws_len > 0 else 0)) + stripped.lstrip("\n"), True
|
|
66
|
-
return text, False
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def _strip_comments_and_collapse(text: str) -> str:
|
|
70
|
-
lines = text.splitlines()
|
|
71
|
-
out: List[str] = []
|
|
72
|
-
for ln in lines:
|
|
73
|
-
# Remove full-line comments; keep inline code comments
|
|
74
|
-
if ln.strip().startswith("#"):
|
|
75
|
-
continue
|
|
76
|
-
out.append(ln)
|
|
77
|
-
# collapse consecutive blank lines
|
|
78
|
-
collapsed: List[str] = []
|
|
79
|
-
blank = 0
|
|
80
|
-
for ln in out:
|
|
81
|
-
if ln.strip() == "":
|
|
82
|
-
blank += 1
|
|
83
|
-
if blank > 1:
|
|
84
|
-
continue
|
|
85
|
-
else:
|
|
86
|
-
blank = 0
|
|
87
|
-
collapsed.append(ln)
|
|
88
|
-
return "\n".join(collapsed)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def read_file(path: Path) -> str:
|
|
92
|
-
try:
|
|
93
|
-
return path.read_text(encoding="utf-8")
|
|
94
|
-
except Exception as e:
|
|
95
|
-
return f"<ERROR reading {path}: {e}>\n"
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def format_section(path: Path, content: str) -> str:
|
|
99
|
-
rel = str(path)
|
|
100
|
-
return (
|
|
101
|
-
f"{SEPARATOR_LINE}\n"
|
|
102
|
-
f"{rel}\n"
|
|
103
|
-
f"{SUBSEP_LINE}\n"
|
|
104
|
-
f"{content.rstrip()}\n\n"
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
def bundle(paths: Iterable[Path], *, max_lines_per_file: Optional[int] = None, strip_comments: bool = False) -> str:
|
|
109
|
-
ts = datetime.utcnow().isoformat(timespec="seconds") + "Z"
|
|
110
|
-
header = (
|
|
111
|
-
"SEQUENCES BACKEND BUNDLE — Source Snapshot\n"
|
|
112
|
-
f"Generated: {ts}\n"
|
|
113
|
-
"Repo root relative paths.\n\n"
|
|
114
|
-
)
|
|
115
|
-
sections: List[str] = [header]
|
|
116
|
-
for p in paths:
|
|
117
|
-
raw = read_file(p)
|
|
118
|
-
if strip_comments:
|
|
119
|
-
stripped, did_strip = _strip_leading_docstring(raw)
|
|
120
|
-
raw = _strip_comments_and_collapse(stripped)
|
|
121
|
-
if max_lines_per_file is not None and max_lines_per_file > 0:
|
|
122
|
-
lines = raw.splitlines()
|
|
123
|
-
if len(lines) > max_lines_per_file:
|
|
124
|
-
clipped = "\n".join(lines[:max_lines_per_file]) + f"\n\n# [truncated to first {max_lines_per_file} lines]\n"
|
|
125
|
-
sections.append(format_section(p, clipped))
|
|
126
|
-
continue
|
|
127
|
-
sections.append(format_section(p, raw))
|
|
128
|
-
return "".join(sections)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
KEYWORDS: List[str] = [
|
|
132
|
-
# Sequences + enrollment + schedule
|
|
133
|
-
"SEQUENCES_COLLECTION",
|
|
134
|
-
"SEQUENCE_ENROLLMENTS_COLLECTION",
|
|
135
|
-
"sequence_enrollment",
|
|
136
|
-
"sequence_enrollments",
|
|
137
|
-
"sequence_service",
|
|
138
|
-
"sequence_enrollment_service",
|
|
139
|
-
"emailDelivery",
|
|
140
|
-
"delivery-status",
|
|
141
|
-
"next_run_at",
|
|
142
|
-
"timeline",
|
|
143
|
-
# Research context
|
|
144
|
-
"research_snapshot_service",
|
|
145
|
-
"projection_resolver",
|
|
146
|
-
"research_context",
|
|
147
|
-
# Task queue hooks
|
|
148
|
-
"task_queue",
|
|
149
|
-
# Readiness dependencies
|
|
150
|
-
"mail_users",
|
|
151
|
-
"api_keys",
|
|
152
|
-
]
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
def scan_auto(root: Path, base_dir: str = "apps/api") -> List[Path]:
|
|
156
|
-
import re
|
|
157
|
-
matches: Set[Path] = set()
|
|
158
|
-
search_dir = root / base_dir
|
|
159
|
-
if not search_dir.exists():
|
|
160
|
-
return []
|
|
161
|
-
pattern = re.compile("|".join(re.escape(k) for k in KEYWORDS))
|
|
162
|
-
for p in search_dir.rglob("*.py"):
|
|
163
|
-
if "__pycache__" in p.parts:
|
|
164
|
-
continue
|
|
165
|
-
try:
|
|
166
|
-
text = p.read_text(encoding="utf-8", errors="ignore")
|
|
167
|
-
except Exception:
|
|
168
|
-
continue
|
|
169
|
-
if pattern.search(text):
|
|
170
|
-
matches.add(p)
|
|
171
|
-
# Always ensure main router + models included
|
|
172
|
-
for rel in DEFAULT_FILES:
|
|
173
|
-
q = root / rel
|
|
174
|
-
if q.exists():
|
|
175
|
-
matches.add(q)
|
|
176
|
-
# Sort and relativize
|
|
177
|
-
rels = []
|
|
178
|
-
for p in matches:
|
|
179
|
-
try:
|
|
180
|
-
rels.append(p.relative_to(root))
|
|
181
|
-
except Exception:
|
|
182
|
-
rels.append(p)
|
|
183
|
-
return sorted(rels, key=lambda x: str(x))
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
def main() -> None:
|
|
187
|
-
parser = argparse.ArgumentParser(description="Bundle backend files into a single text file")
|
|
188
|
-
parser.add_argument("--output", default="docs/shares/sequences_backend_bundle.txt", help="Output path")
|
|
189
|
-
parser.add_argument("--stdout", action="store_true", help="Print to stdout instead of writing a file")
|
|
190
|
-
parser.add_argument("--extra", action="append", default=[], help="Additional file path to include (repeatable)")
|
|
191
|
-
parser.add_argument("--auto", action="store_true", help="Auto-discover relevant backend files (keyword scan under apps/api)")
|
|
192
|
-
parser.add_argument("--core", action="store_true", help="Include only core DEFAULT_FILES and ignore --auto")
|
|
193
|
-
parser.add_argument("--max-lines-per-file", type=int, default=None, help="Truncate each file to the first N lines")
|
|
194
|
-
parser.add_argument("--strip-comments", action="store_true", help="Remove full-line comments and top module docstrings; collapse blank lines")
|
|
195
|
-
parser.add_argument("--root", default=None, help="Project root (default: cwd)")
|
|
196
|
-
args = parser.parse_args()
|
|
197
|
-
|
|
198
|
-
root = Path(args.root) if args.root else Path.cwd()
|
|
199
|
-
candidates = []
|
|
200
|
-
|
|
201
|
-
if args.core:
|
|
202
|
-
# Force core-only
|
|
203
|
-
for rel in DEFAULT_FILES:
|
|
204
|
-
p = root / rel
|
|
205
|
-
if p.exists():
|
|
206
|
-
candidates.append(p)
|
|
207
|
-
else:
|
|
208
|
-
print(f"[warn] missing core file: {rel}")
|
|
209
|
-
elif args.auto:
|
|
210
|
-
auto_paths = scan_auto(root)
|
|
211
|
-
candidates.extend(auto_paths)
|
|
212
|
-
else:
|
|
213
|
-
# Collect default files
|
|
214
|
-
for rel in DEFAULT_FILES:
|
|
215
|
-
p = root / rel
|
|
216
|
-
if p.exists():
|
|
217
|
-
candidates.append(p)
|
|
218
|
-
else:
|
|
219
|
-
print(f"[warn] missing default file: {rel}")
|
|
220
|
-
|
|
221
|
-
# Add extras
|
|
222
|
-
for rel in args.extra:
|
|
223
|
-
p = (root / rel).resolve()
|
|
224
|
-
if p.exists():
|
|
225
|
-
candidates.append(p.relative_to(root))
|
|
226
|
-
else:
|
|
227
|
-
print(f"[warn] extra file not found: {rel}")
|
|
228
|
-
|
|
229
|
-
# De-duplicate and sort
|
|
230
|
-
uniq_sorted = sorted({str(p) for p in candidates})
|
|
231
|
-
paths = [Path(s) for s in uniq_sorted]
|
|
232
|
-
|
|
233
|
-
output_text = bundle(
|
|
234
|
-
paths,
|
|
235
|
-
max_lines_per_file=args.max_lines_per_file,
|
|
236
|
-
strip_comments=args.strip_comments,
|
|
237
|
-
)
|
|
238
|
-
|
|
239
|
-
if args.stdout:
|
|
240
|
-
print(output_text)
|
|
241
|
-
return
|
|
242
|
-
|
|
243
|
-
out_path = root / args.output
|
|
244
|
-
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
245
|
-
out_path.write_text(output_text, encoding="utf-8")
|
|
246
|
-
print(f"[ok] Wrote {out_path} ({len(paths)} files)")
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
if __name__ == "__main__":
|
|
250
|
-
main()
|