Includes: API (Express/TypeScript), Astro site, Python workers, document generators, FCC compliance tools, Canada CRTC formation, Ansible infrastructure, and deployment scripts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
175 lines
5.5 KiB
Python
175 lines
5.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Test name search automation for any state.
|
|
|
|
Usage:
|
|
python -m scripts.formation.test_name_search WY "Test Business LLC"
|
|
python -m scripts.formation.test_name_search CO "Acme Holdings"
|
|
python -m scripts.formation.test_name_search DE "First State Corp"
|
|
python -m scripts.formation.test_name_search --all "Test Business LLC"
|
|
|
|
Reports:
|
|
- Whether name search succeeded
|
|
- Whether the portal was reachable
|
|
- What selectors worked
|
|
- What barriers were encountered (CAPTCHA, login, WAF, etc.)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
import logging
|
|
import sys
|
|
import time
|
|
from datetime import datetime
|
|
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format="%(asctime)s [%(name)s] %(levelname)s %(message)s",
|
|
)
|
|
LOG = logging.getLogger("test_name_search")
|
|
|
|
|
|
async def test_state(state_code: str, name: str) -> dict:
|
|
"""Test name search for a single state."""
|
|
from scripts.formation.states import get_adapter, get_config, STATES
|
|
|
|
state_code = state_code.upper()
|
|
if state_code not in STATES:
|
|
return {"state": state_code, "status": "ERROR", "error": f"Unknown state: {state_code}"}
|
|
|
|
config = get_config(state_code)
|
|
state_name = STATES[state_code]["name"]
|
|
search_method = config.get("search_method", "playwright")
|
|
|
|
LOG.info("Testing %s (%s) — method: %s, name: '%s'", state_code, state_name, search_method, name)
|
|
start = time.time()
|
|
|
|
result = {
|
|
"state": state_code,
|
|
"state_name": state_name,
|
|
"search_method": search_method,
|
|
"portal_url": config.get("name_search_url", ""),
|
|
"test_name": name,
|
|
"started_at": datetime.utcnow().isoformat(),
|
|
}
|
|
|
|
try:
|
|
adapter = get_adapter(state_code)
|
|
search_result = await adapter.search_name(name)
|
|
|
|
result["status"] = "SUCCESS" if search_result.searched_name else "PARTIAL"
|
|
result["available"] = search_result.available
|
|
result["exact_match"] = search_result.exact_match
|
|
result["similar_names"] = search_result.similar_names[:5]
|
|
result["raw_response_length"] = len(search_result.raw_response)
|
|
|
|
if "not yet configured" in search_result.raw_response.lower():
|
|
result["status"] = "NOT_CONFIGURED"
|
|
result["barrier"] = "Selectors not configured in config.py"
|
|
elif "captcha" in search_result.raw_response.lower():
|
|
result["status"] = "BLOCKED_CAPTCHA"
|
|
result["barrier"] = "CAPTCHA detected"
|
|
elif "error" in search_result.raw_response.lower() and not search_result.available:
|
|
result["status"] = "ERROR"
|
|
result["error"] = search_result.raw_response[:200]
|
|
|
|
except Exception as e:
|
|
result["status"] = "ERROR"
|
|
result["error"] = str(e)
|
|
LOG.error("Test failed for %s: %s", state_code, e)
|
|
|
|
elapsed = time.time() - start
|
|
result["elapsed_seconds"] = round(elapsed, 2)
|
|
result["finished_at"] = datetime.utcnow().isoformat()
|
|
|
|
# Status emoji for readability
|
|
status_icon = {
|
|
"SUCCESS": "✓",
|
|
"PARTIAL": "~",
|
|
"NOT_CONFIGURED": "○",
|
|
"BLOCKED_CAPTCHA": "⚠",
|
|
"BLOCKED_LOGIN": "🔒",
|
|
"ERROR": "✗",
|
|
}.get(result["status"], "?")
|
|
|
|
LOG.info(
|
|
"%s %s (%s): %s — available=%s, elapsed=%.1fs",
|
|
status_icon, state_code, state_name, result["status"],
|
|
result.get("available", "?"), elapsed,
|
|
)
|
|
|
|
return result
|
|
|
|
|
|
async def test_all_states(name: str) -> list[dict]:
|
|
"""Test name search for all 51 states sequentially."""
|
|
from scripts.formation.states import STATES
|
|
|
|
results = []
|
|
for code in sorted(STATES.keys()):
|
|
result = await test_state(code, name)
|
|
results.append(result)
|
|
|
|
# Brief pause between states
|
|
if result["status"] not in ("NOT_CONFIGURED", "ERROR"):
|
|
await asyncio.sleep(2)
|
|
|
|
return results
|
|
|
|
|
|
def print_summary(results: list[dict]):
|
|
"""Print a summary table of test results."""
|
|
print("\n" + "=" * 80)
|
|
print("STATE NAME SEARCH TEST RESULTS")
|
|
print("=" * 80)
|
|
print(f"{'State':<6} {'Name':<25} {'Status':<20} {'Available':<10} {'Time':<8} {'Barrier'}")
|
|
print("-" * 80)
|
|
|
|
for r in results:
|
|
barrier = r.get("barrier", r.get("error", ""))[:30]
|
|
print(
|
|
f"{r['state']:<6} {r.get('state_name','?'):<25} {r['status']:<20} "
|
|
f"{str(r.get('available','?')):<10} {r.get('elapsed_seconds',0):>6.1f}s {barrier}"
|
|
)
|
|
|
|
# Summary counts
|
|
statuses = {}
|
|
for r in results:
|
|
s = r["status"]
|
|
statuses[s] = statuses.get(s, 0) + 1
|
|
|
|
print("-" * 80)
|
|
print("Summary:", " | ".join(f"{k}: {v}" for k, v in sorted(statuses.items())))
|
|
print(f"Total: {len(results)} states tested")
|
|
|
|
|
|
def main():
|
|
args = sys.argv[1:]
|
|
|
|
if len(args) < 2:
|
|
print("Usage:")
|
|
print(" python -m scripts.formation.test_name_search WY 'Test Business LLC'")
|
|
print(" python -m scripts.formation.test_name_search --all 'Test Business LLC'")
|
|
sys.exit(1)
|
|
|
|
if args[0] == "--all":
|
|
name = " ".join(args[1:])
|
|
results = asyncio.run(test_all_states(name))
|
|
print_summary(results)
|
|
|
|
# Save results to JSON
|
|
output_path = "/tmp/name_search_test_results.json"
|
|
with open(output_path, "w") as f:
|
|
json.dump(results, f, indent=2)
|
|
print(f"\nFull results saved to: {output_path}")
|
|
else:
|
|
state_code = args[0]
|
|
name = " ".join(args[1:])
|
|
result = asyncio.run(test_state(state_code, name))
|
|
print(json.dumps(result, indent=2))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|