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.
Files changed (69) hide show
  1. {autotouch_cli-0.2.27 → autotouch_cli-0.2.28}/PKG-INFO +1 -1
  2. autotouch_cli-0.2.28/autotouch_cli/__init__.py +1 -0
  3. {autotouch_cli-0.2.27 → autotouch_cli-0.2.28}/autotouch_cli.egg-info/PKG-INFO +1 -1
  4. autotouch_cli-0.2.28/autotouch_cli.egg-info/SOURCES.txt +18 -0
  5. autotouch_cli-0.2.28/autotouch_cli.egg-info/entry_points.txt +3 -0
  6. autotouch_cli-0.2.28/autotouch_cli.egg-info/top_level.txt +1 -0
  7. {autotouch_cli-0.2.27 → autotouch_cli-0.2.28}/pyproject.toml +4 -4
  8. autotouch_cli-0.2.27/autotouch_cli.egg-info/SOURCES.txt +0 -63
  9. autotouch_cli-0.2.27/autotouch_cli.egg-info/entry_points.txt +0 -3
  10. autotouch_cli-0.2.27/autotouch_cli.egg-info/top_level.txt +0 -1
  11. autotouch_cli-0.2.27/scripts/__init__.py +0 -1
  12. autotouch_cli-0.2.27/scripts/add_column_unique_index.py +0 -112
  13. autotouch_cli-0.2.27/scripts/attach_csv_import_leads_to_research_table.py +0 -130
  14. autotouch_cli-0.2.27/scripts/bundle_sequences_backend.py +0 -250
  15. autotouch_cli-0.2.27/scripts/check_agent_traces.py +0 -125
  16. autotouch_cli-0.2.27/scripts/check_column_mode.py +0 -87
  17. autotouch_cli-0.2.27/scripts/exit_terminal_leads_from_sequences.py +0 -302
  18. autotouch_cli-0.2.27/scripts/fetch_lead.py +0 -12
  19. autotouch_cli-0.2.27/scripts/fix_lead_titles_from_csv.py +0 -217
  20. autotouch_cli-0.2.27/scripts/migrations/20250106_add_column_position.py +0 -66
  21. autotouch_cli-0.2.27/scripts/migrations/20250108_fix_legacy_column_fields.py +0 -91
  22. autotouch_cli-0.2.27/scripts/migrations/20250109_add_user_fields_to_tables.py +0 -84
  23. autotouch_cli-0.2.27/scripts/migrations/20250117_add_call_logs_webhook_indexes.py +0 -84
  24. autotouch_cli-0.2.27/scripts/migrations/20250117_rename_call_logs_collection.py +0 -91
  25. autotouch_cli-0.2.27/scripts/migrations/20250119_create_leads_unique_email_index.py +0 -263
  26. autotouch_cli-0.2.27/scripts/migrations/20250123_add_filter_indexes.py +0 -134
  27. autotouch_cli-0.2.27/scripts/migrations/20250123_add_llm_responses_collection.py +0 -108
  28. autotouch_cli-0.2.27/scripts/migrations/20250128_migrate_user_ids_to_objectid.py +0 -166
  29. autotouch_cli-0.2.27/scripts/migrations/20250208_backfill_task_research_values.py +0 -150
  30. autotouch_cli-0.2.27/scripts/migrations/20250604_add_origin_indexes.py +0 -52
  31. autotouch_cli-0.2.27/scripts/migrations/20250608_cleanup_agent_metadata.py +0 -85
  32. autotouch_cli-0.2.27/scripts/migrations/20250608_rename_agent_metadata_to_metadata.py +0 -103
  33. autotouch_cli-0.2.27/scripts/migrations/20250922_add_activity_indexes.py +0 -215
  34. autotouch_cli-0.2.27/scripts/migrations/20250926_migrate_single_to_arrays.py +0 -465
  35. autotouch_cli-0.2.27/scripts/migrations/20250928_add_missing_timestamp_fields.py +0 -133
  36. autotouch_cli-0.2.27/scripts/migrations/20250929_add_task_join_indexes.py +0 -245
  37. autotouch_cli-0.2.27/scripts/migrations/20250929_add_task_join_indexes_safe.py +0 -107
  38. autotouch_cli-0.2.27/scripts/migrations/20250929_create_shared_phone_cache.py +0 -213
  39. autotouch_cli-0.2.27/scripts/migrations/20251007_add_rows_position_id_index.py +0 -46
  40. autotouch_cli-0.2.27/scripts/migrations/20251109_add_ttl_for_llm_and_preview_traces.py +0 -99
  41. autotouch_cli-0.2.27/scripts/migrations/20260113_normalize_table_filter_operators.py +0 -113
  42. autotouch_cli-0.2.27/scripts/migrations/20260113_set_user_permissions_user_admin.py +0 -100
  43. autotouch_cli-0.2.27/scripts/migrations/20260204_sync_lead_owner_from_tasks.py +0 -269
  44. autotouch_cli-0.2.27/scripts/migrations/20260303_add_webhook_subscription_collections.py +0 -124
  45. autotouch_cli-0.2.27/scripts/migrations/20260305_force_formatter_autorun_on_source_update.py +0 -98
  46. autotouch_cli-0.2.27/scripts/migrations/20260306_migrate_lead_identity_v1.py +0 -538
  47. autotouch_cli-0.2.27/scripts/migrations/migrate_org_user_credits.py +0 -117
  48. autotouch_cli-0.2.27/scripts/migrations/set_default_lead_status.py +0 -49
  49. autotouch_cli-0.2.27/scripts/migrations/update_lead_owner_from_tasks.py +0 -152
  50. autotouch_cli-0.2.27/scripts/reassign_sequence_owner.py +0 -136
  51. autotouch_cli-0.2.27/scripts/run_sidecar_orchestrator_demo.py +0 -106
  52. autotouch_cli-0.2.27/scripts/test_crm_company_policy.py +0 -277
  53. autotouch_cli-0.2.27/scripts/test_sequences_instantly_e2e.py +0 -249
  54. autotouch_cli-0.2.27/scripts/test_sequences_personal_e2e.py +0 -215
  55. autotouch_cli-0.2.27/scripts/test_task_error_logger.py +0 -44
  56. autotouch_cli-0.2.27/scripts/verify_azurite_voicemail.py +0 -64
  57. /autotouch_cli-0.2.27/scripts/smart_table_cli.py → /autotouch_cli-0.2.28/autotouch_cli/cli.py +0 -0
  58. {autotouch_cli-0.2.27 → autotouch_cli-0.2.28}/autotouch_cli.egg-info/dependency_links.txt +0 -0
  59. {autotouch_cli-0.2.27 → autotouch_cli-0.2.28}/autotouch_cli.egg-info/requires.txt +0 -0
  60. {autotouch_cli-0.2.27 → autotouch_cli-0.2.28}/docs/research-table/reference/autotouch-cli.md +0 -0
  61. {autotouch_cli-0.2.27 → autotouch_cli-0.2.28}/setup.cfg +0 -0
  62. {autotouch_cli-0.2.27 → autotouch_cli-0.2.28}/tests/test_contactout_custom.py +0 -0
  63. {autotouch_cli-0.2.27 → autotouch_cli-0.2.28}/tests/test_contactout_integration.py +0 -0
  64. {autotouch_cli-0.2.27 → autotouch_cli-0.2.28}/tests/test_contactout_multi_titles.py +0 -0
  65. {autotouch_cli-0.2.27 → autotouch_cli-0.2.28}/tests/test_contactout_pipeline.py +0 -0
  66. {autotouch_cli-0.2.27 → autotouch_cli-0.2.28}/tests/test_contactout_simple.py +0 -0
  67. {autotouch_cli-0.2.27 → autotouch_cli-0.2.28}/tests/test_contactout_v2_bulk.py +0 -0
  68. {autotouch_cli-0.2.27 → autotouch_cli-0.2.28}/tests/test_lead_required_fields.py +0 -0
  69. {autotouch_cli-0.2.27 → autotouch_cli-0.2.28}/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.27
3
+ Version: 0.2.28
4
4
  Summary: Autotouch Smart Table CLI
5
5
  Requires-Python: >=3.9
6
6
  Description-Content-Type: text/markdown
@@ -0,0 +1 @@
1
+ """Autotouch CLI package."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: autotouch-cli
3
- Version: 0.2.27
3
+ Version: 0.2.28
4
4
  Summary: Autotouch Smart Table CLI
5
5
  Requires-Python: >=3.9
6
6
  Description-Content-Type: text/markdown
@@ -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,3 @@
1
+ [console_scripts]
2
+ autotouch = autotouch_cli.cli:main
3
+ smart-table = autotouch_cli.cli:main
@@ -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.27"
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 = "scripts.smart_table_cli:main"
18
- smart-table = "scripts.smart_table_cli:main"
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 = ["scripts*"]
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,3 +0,0 @@
1
- [console_scripts]
2
- autotouch = scripts.smart_table_cli:main
3
- smart-table = scripts.smart_table_cli:main
@@ -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()