ai-lls-lib 1.4.0rc2__py3-none-any.whl → 1.4.0rc4__py3-none-any.whl

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 (34) hide show
  1. ai_lls_lib/__init__.py +1 -1
  2. ai_lls_lib/auth/__init__.py +4 -4
  3. ai_lls_lib/auth/context_parser.py +68 -68
  4. ai_lls_lib/cli/__init__.py +3 -3
  5. ai_lls_lib/cli/__main__.py +30 -30
  6. ai_lls_lib/cli/aws_client.py +115 -115
  7. ai_lls_lib/cli/commands/__init__.py +3 -3
  8. ai_lls_lib/cli/commands/admin.py +174 -174
  9. ai_lls_lib/cli/commands/cache.py +142 -142
  10. ai_lls_lib/cli/commands/stripe.py +377 -377
  11. ai_lls_lib/cli/commands/test_stack.py +216 -216
  12. ai_lls_lib/cli/commands/verify.py +111 -111
  13. ai_lls_lib/cli/env_loader.py +122 -122
  14. ai_lls_lib/core/__init__.py +3 -3
  15. ai_lls_lib/core/cache.py +106 -106
  16. ai_lls_lib/core/models.py +77 -77
  17. ai_lls_lib/core/processor.py +295 -295
  18. ai_lls_lib/core/verifier.py +84 -84
  19. ai_lls_lib/payment/__init__.py +13 -13
  20. ai_lls_lib/payment/credit_manager.py +186 -193
  21. ai_lls_lib/payment/models.py +102 -102
  22. ai_lls_lib/payment/stripe_manager.py +487 -487
  23. ai_lls_lib/payment/webhook_processor.py +215 -215
  24. ai_lls_lib/providers/__init__.py +7 -7
  25. ai_lls_lib/providers/base.py +28 -28
  26. ai_lls_lib/providers/external.py +87 -87
  27. ai_lls_lib/providers/stub.py +48 -48
  28. ai_lls_lib/testing/__init__.py +3 -3
  29. ai_lls_lib/testing/fixtures.py +104 -104
  30. {ai_lls_lib-1.4.0rc2.dist-info → ai_lls_lib-1.4.0rc4.dist-info}/METADATA +1 -1
  31. ai_lls_lib-1.4.0rc4.dist-info/RECORD +33 -0
  32. ai_lls_lib-1.4.0rc2.dist-info/RECORD +0 -33
  33. {ai_lls_lib-1.4.0rc2.dist-info → ai_lls_lib-1.4.0rc4.dist-info}/WHEEL +0 -0
  34. {ai_lls_lib-1.4.0rc2.dist-info → ai_lls_lib-1.4.0rc4.dist-info}/entry_points.txt +0 -0
@@ -1,216 +1,216 @@
1
- """
2
- Test stack management commands
3
- """
4
- import click
5
- import subprocess
6
- import os
7
- import time
8
- from ai_lls_lib.cli.aws_client import AWSClient
9
-
10
- @click.group(name="test-stack")
11
- def test_stack_group():
12
- """Test stack management"""
13
- pass
14
-
15
- @test_stack_group.command(name="deploy")
16
- @click.option("--stack-name", default="ai-lls-lib-test", help="Test stack name")
17
- @click.option("--profile", help="AWS profile")
18
- @click.option("--region", help="AWS region")
19
- def deploy_test_stack(stack_name, profile, region):
20
- """Deploy the test stack"""
21
- template_path = os.path.join(
22
- os.path.dirname(__file__),
23
- "..", "..", "..", "..", "template.yaml"
24
- )
25
-
26
- if not os.path.exists(template_path):
27
- click.echo(f"Test stack template not found at {template_path}")
28
- click.echo("Creating minimal test stack template...")
29
-
30
- # Create a minimal test stack
31
- template_content = """AWSTemplateFormatVersion: '2010-09-09'
32
- Description: Minimal test stack for ai-lls-lib integration testing
33
-
34
- Resources:
35
- TestPhoneCache:
36
- Type: AWS::DynamoDB::Table
37
- Properties:
38
- TableName: !Sub "${AWS::StackName}-phone-cache"
39
- BillingMode: PAY_PER_REQUEST
40
- AttributeDefinitions:
41
- - AttributeName: phone_number
42
- AttributeType: S
43
- KeySchema:
44
- - AttributeName: phone_number
45
- KeyType: HASH
46
- TimeToLiveSpecification:
47
- AttributeName: ttl
48
- Enabled: true
49
-
50
- TestUploadBucket:
51
- Type: AWS::S3::Bucket
52
- Properties:
53
- BucketName: !Sub "${AWS::StackName}-uploads-${AWS::AccountId}"
54
- LifecycleConfiguration:
55
- Rules:
56
- - Id: DeleteOldTestFiles
57
- Status: Enabled
58
- ExpirationInDays: 1
59
-
60
- TestQueue:
61
- Type: AWS::SQS::Queue
62
- Properties:
63
- QueueName: !Sub "${AWS::StackName}-test-queue"
64
- MessageRetentionPeriod: 3600 # 1 hour for test
65
-
66
- Outputs:
67
- CacheTableName:
68
- Value: !Ref TestPhoneCache
69
- BucketName:
70
- Value: !Ref TestUploadBucket
71
- QueueUrl:
72
- Value: !Ref TestQueue
73
- """
74
-
75
- os.makedirs(os.path.dirname(template_path), exist_ok=True)
76
- with open(template_path, 'w') as f:
77
- f.write(template_content)
78
- click.echo(f"Created {template_path}")
79
-
80
- # Deploy using AWS CLI
81
- cmd = [
82
- "aws", "cloudformation", "deploy",
83
- "--template-file", template_path,
84
- "--stack-name", stack_name,
85
- "--capabilities", "CAPABILITY_IAM"
86
- ]
87
-
88
- if profile:
89
- cmd.extend(["--profile", profile])
90
- if region:
91
- cmd.extend(["--region", region])
92
-
93
- click.echo(f"Deploying test stack '{stack_name}'...")
94
- try:
95
- result = subprocess.run(cmd, capture_output=True, text=True)
96
- if result.returncode == 0:
97
- click.echo("Test stack deployed successfully!")
98
-
99
- # Show outputs
100
- aws = AWSClient(region=region, profile=profile)
101
- outputs = aws.get_stack_outputs(stack_name)
102
- if outputs:
103
- click.echo("\nStack Outputs:")
104
- for key, value in outputs.items():
105
- click.echo(f" {key}: {value}")
106
- else:
107
- click.echo(f"Deployment failed: {result.stderr}", err=True)
108
- except Exception as e:
109
- click.echo(f"Error deploying test stack: {e}", err=True)
110
-
111
- @test_stack_group.command(name="delete")
112
- @click.option("--stack-name", default="ai-lls-lib-test", help="Test stack name")
113
- @click.option("--profile", help="AWS profile")
114
- @click.option("--region", help="AWS region")
115
- @click.confirmation_option(prompt="Delete test stack and all resources?")
116
- def delete_test_stack(stack_name, profile, region):
117
- """Delete the test stack"""
118
- aws = AWSClient(region=region, profile=profile)
119
-
120
- try:
121
- # Empty S3 bucket first
122
- outputs = aws.get_stack_outputs(stack_name)
123
- if 'BucketName' in outputs:
124
- bucket_name = outputs['BucketName']
125
- click.echo(f"Emptying bucket {bucket_name}...")
126
-
127
- # List and delete all objects
128
- try:
129
- objects = aws.s3.list_objects_v2(Bucket=bucket_name)
130
- if 'Contents' in objects:
131
- for obj in objects['Contents']:
132
- aws.s3.delete_object(Bucket=bucket_name, Key=obj['Key'])
133
- except:
134
- pass # Bucket might not exist
135
-
136
- # Delete stack
137
- click.echo(f"Deleting stack '{stack_name}'...")
138
- aws.cloudformation.delete_stack(StackName=stack_name)
139
-
140
- # Wait for deletion
141
- click.echo("Waiting for stack deletion...")
142
- waiter = aws.cloudformation.get_waiter('stack_delete_complete')
143
- waiter.wait(StackName=stack_name)
144
-
145
- click.echo("Test stack deleted successfully!")
146
-
147
- except Exception as e:
148
- click.echo(f"Error deleting test stack: {e}", err=True)
149
-
150
- @test_stack_group.command(name="status")
151
- @click.option("--stack-name", default="ai-lls-lib-test", help="Test stack name")
152
- @click.option("--profile", help="AWS profile")
153
- @click.option("--region", help="AWS region")
154
- def test_stack_status(stack_name, profile, region):
155
- """Show test stack status"""
156
- aws = AWSClient(region=region, profile=profile)
157
-
158
- try:
159
- response = aws.cloudformation.describe_stacks(StackName=stack_name)
160
- stack = response['Stacks'][0]
161
-
162
- click.echo(f"\nTest Stack: {stack_name}")
163
- click.echo("=" * 50)
164
- click.echo(f"Status: {stack['StackStatus']}")
165
- click.echo(f"Created: {stack.get('CreationTime', 'N/A')}")
166
-
167
- if 'Outputs' in stack:
168
- click.echo("\nOutputs:")
169
- for output in stack['Outputs']:
170
- click.echo(f" {output['OutputKey']}: {output['OutputValue']}")
171
-
172
- except aws.cloudformation.exceptions.ClientError as e:
173
- if 'does not exist' in str(e):
174
- click.echo(f"Test stack '{stack_name}' does not exist")
175
- else:
176
- click.echo(f"Error: {e}", err=True)
177
-
178
- @test_stack_group.command(name="test")
179
- @click.option("--stack-name", default="ai-lls-lib-test", help="Test stack name")
180
- @click.option("--profile", help="AWS profile")
181
- @click.option("--region", help="AWS region")
182
- def run_integration_tests(stack_name, profile, region):
183
- """Run integration tests against test stack"""
184
- aws = AWSClient(region=region, profile=profile)
185
-
186
- try:
187
- # Check stack exists
188
- outputs = aws.get_stack_outputs(stack_name)
189
- if not outputs:
190
- click.echo(f"Test stack '{stack_name}' not found. Deploy it first.")
191
- return
192
-
193
- # Set environment variables for tests
194
- os.environ['TEST_STACK_NAME'] = stack_name
195
- os.environ['TEST_CACHE_TABLE'] = outputs.get('CacheTableName', '')
196
- os.environ['TEST_BUCKET'] = outputs.get('BucketName', '')
197
- os.environ['TEST_QUEUE_URL'] = outputs.get('QueueUrl', '')
198
-
199
- if profile:
200
- os.environ['AWS_PROFILE'] = profile
201
- if region:
202
- os.environ['AWS_REGION'] = region
203
-
204
- click.echo(f"Running integration tests against '{stack_name}'...")
205
-
206
- # Run pytest
207
- cmd = ["pytest", "tests/integration", "-v"]
208
- result = subprocess.run(cmd, cwd=os.path.dirname(template_path))
209
-
210
- if result.returncode == 0:
211
- click.echo("\nIntegration tests passed!")
212
- else:
213
- click.echo("\nSome tests failed", err=True)
214
-
215
- except Exception as e:
216
- click.echo(f"Error running tests: {e}", err=True)
1
+ """
2
+ Test stack management commands
3
+ """
4
+ import click
5
+ import subprocess
6
+ import os
7
+ import time
8
+ from ai_lls_lib.cli.aws_client import AWSClient
9
+
10
+ @click.group(name="test-stack")
11
+ def test_stack_group():
12
+ """Test stack management"""
13
+ pass
14
+
15
+ @test_stack_group.command(name="deploy")
16
+ @click.option("--stack-name", default="ai-lls-lib-test", help="Test stack name")
17
+ @click.option("--profile", help="AWS profile")
18
+ @click.option("--region", help="AWS region")
19
+ def deploy_test_stack(stack_name, profile, region):
20
+ """Deploy the test stack"""
21
+ template_path = os.path.join(
22
+ os.path.dirname(__file__),
23
+ "..", "..", "..", "..", "template.yaml"
24
+ )
25
+
26
+ if not os.path.exists(template_path):
27
+ click.echo(f"Test stack template not found at {template_path}")
28
+ click.echo("Creating minimal test stack template...")
29
+
30
+ # Create a minimal test stack
31
+ template_content = """AWSTemplateFormatVersion: '2010-09-09'
32
+ Description: Minimal test stack for ai-lls-lib integration testing
33
+
34
+ Resources:
35
+ TestPhoneCache:
36
+ Type: AWS::DynamoDB::Table
37
+ Properties:
38
+ TableName: !Sub "${AWS::StackName}-phone-cache"
39
+ BillingMode: PAY_PER_REQUEST
40
+ AttributeDefinitions:
41
+ - AttributeName: phone_number
42
+ AttributeType: S
43
+ KeySchema:
44
+ - AttributeName: phone_number
45
+ KeyType: HASH
46
+ TimeToLiveSpecification:
47
+ AttributeName: ttl
48
+ Enabled: true
49
+
50
+ TestUploadBucket:
51
+ Type: AWS::S3::Bucket
52
+ Properties:
53
+ BucketName: !Sub "${AWS::StackName}-uploads-${AWS::AccountId}"
54
+ LifecycleConfiguration:
55
+ Rules:
56
+ - Id: DeleteOldTestFiles
57
+ Status: Enabled
58
+ ExpirationInDays: 1
59
+
60
+ TestQueue:
61
+ Type: AWS::SQS::Queue
62
+ Properties:
63
+ QueueName: !Sub "${AWS::StackName}-test-queue"
64
+ MessageRetentionPeriod: 3600 # 1 hour for test
65
+
66
+ Outputs:
67
+ CacheTableName:
68
+ Value: !Ref TestPhoneCache
69
+ BucketName:
70
+ Value: !Ref TestUploadBucket
71
+ QueueUrl:
72
+ Value: !Ref TestQueue
73
+ """
74
+
75
+ os.makedirs(os.path.dirname(template_path), exist_ok=True)
76
+ with open(template_path, 'w') as f:
77
+ f.write(template_content)
78
+ click.echo(f"Created {template_path}")
79
+
80
+ # Deploy using AWS CLI
81
+ cmd = [
82
+ "aws", "cloudformation", "deploy",
83
+ "--template-file", template_path,
84
+ "--stack-name", stack_name,
85
+ "--capabilities", "CAPABILITY_IAM"
86
+ ]
87
+
88
+ if profile:
89
+ cmd.extend(["--profile", profile])
90
+ if region:
91
+ cmd.extend(["--region", region])
92
+
93
+ click.echo(f"Deploying test stack '{stack_name}'...")
94
+ try:
95
+ result = subprocess.run(cmd, capture_output=True, text=True)
96
+ if result.returncode == 0:
97
+ click.echo("Test stack deployed successfully!")
98
+
99
+ # Show outputs
100
+ aws = AWSClient(region=region, profile=profile)
101
+ outputs = aws.get_stack_outputs(stack_name)
102
+ if outputs:
103
+ click.echo("\nStack Outputs:")
104
+ for key, value in outputs.items():
105
+ click.echo(f" {key}: {value}")
106
+ else:
107
+ click.echo(f"Deployment failed: {result.stderr}", err=True)
108
+ except Exception as e:
109
+ click.echo(f"Error deploying test stack: {e}", err=True)
110
+
111
+ @test_stack_group.command(name="delete")
112
+ @click.option("--stack-name", default="ai-lls-lib-test", help="Test stack name")
113
+ @click.option("--profile", help="AWS profile")
114
+ @click.option("--region", help="AWS region")
115
+ @click.confirmation_option(prompt="Delete test stack and all resources?")
116
+ def delete_test_stack(stack_name, profile, region):
117
+ """Delete the test stack"""
118
+ aws = AWSClient(region=region, profile=profile)
119
+
120
+ try:
121
+ # Empty S3 bucket first
122
+ outputs = aws.get_stack_outputs(stack_name)
123
+ if 'BucketName' in outputs:
124
+ bucket_name = outputs['BucketName']
125
+ click.echo(f"Emptying bucket {bucket_name}...")
126
+
127
+ # List and delete all objects
128
+ try:
129
+ objects = aws.s3.list_objects_v2(Bucket=bucket_name)
130
+ if 'Contents' in objects:
131
+ for obj in objects['Contents']:
132
+ aws.s3.delete_object(Bucket=bucket_name, Key=obj['Key'])
133
+ except:
134
+ pass # Bucket might not exist
135
+
136
+ # Delete stack
137
+ click.echo(f"Deleting stack '{stack_name}'...")
138
+ aws.cloudformation.delete_stack(StackName=stack_name)
139
+
140
+ # Wait for deletion
141
+ click.echo("Waiting for stack deletion...")
142
+ waiter = aws.cloudformation.get_waiter('stack_delete_complete')
143
+ waiter.wait(StackName=stack_name)
144
+
145
+ click.echo("Test stack deleted successfully!")
146
+
147
+ except Exception as e:
148
+ click.echo(f"Error deleting test stack: {e}", err=True)
149
+
150
+ @test_stack_group.command(name="status")
151
+ @click.option("--stack-name", default="ai-lls-lib-test", help="Test stack name")
152
+ @click.option("--profile", help="AWS profile")
153
+ @click.option("--region", help="AWS region")
154
+ def test_stack_status(stack_name, profile, region):
155
+ """Show test stack status"""
156
+ aws = AWSClient(region=region, profile=profile)
157
+
158
+ try:
159
+ response = aws.cloudformation.describe_stacks(StackName=stack_name)
160
+ stack = response['Stacks'][0]
161
+
162
+ click.echo(f"\nTest Stack: {stack_name}")
163
+ click.echo("=" * 50)
164
+ click.echo(f"Status: {stack['StackStatus']}")
165
+ click.echo(f"Created: {stack.get('CreationTime', 'N/A')}")
166
+
167
+ if 'Outputs' in stack:
168
+ click.echo("\nOutputs:")
169
+ for output in stack['Outputs']:
170
+ click.echo(f" {output['OutputKey']}: {output['OutputValue']}")
171
+
172
+ except aws.cloudformation.exceptions.ClientError as e:
173
+ if 'does not exist' in str(e):
174
+ click.echo(f"Test stack '{stack_name}' does not exist")
175
+ else:
176
+ click.echo(f"Error: {e}", err=True)
177
+
178
+ @test_stack_group.command(name="test")
179
+ @click.option("--stack-name", default="ai-lls-lib-test", help="Test stack name")
180
+ @click.option("--profile", help="AWS profile")
181
+ @click.option("--region", help="AWS region")
182
+ def run_integration_tests(stack_name, profile, region):
183
+ """Run integration tests against test stack"""
184
+ aws = AWSClient(region=region, profile=profile)
185
+
186
+ try:
187
+ # Check stack exists
188
+ outputs = aws.get_stack_outputs(stack_name)
189
+ if not outputs:
190
+ click.echo(f"Test stack '{stack_name}' not found. Deploy it first.")
191
+ return
192
+
193
+ # Set environment variables for tests
194
+ os.environ['TEST_STACK_NAME'] = stack_name
195
+ os.environ['TEST_CACHE_TABLE'] = outputs.get('CacheTableName', '')
196
+ os.environ['TEST_BUCKET'] = outputs.get('BucketName', '')
197
+ os.environ['TEST_QUEUE_URL'] = outputs.get('QueueUrl', '')
198
+
199
+ if profile:
200
+ os.environ['AWS_PROFILE'] = profile
201
+ if region:
202
+ os.environ['AWS_REGION'] = region
203
+
204
+ click.echo(f"Running integration tests against '{stack_name}'...")
205
+
206
+ # Run pytest
207
+ cmd = ["pytest", "tests/integration", "-v"]
208
+ result = subprocess.run(cmd, cwd=os.path.dirname(template_path))
209
+
210
+ if result.returncode == 0:
211
+ click.echo("\nIntegration tests passed!")
212
+ else:
213
+ click.echo("\nSome tests failed", err=True)
214
+
215
+ except Exception as e:
216
+ click.echo(f"Error running tests: {e}", err=True)
@@ -1,111 +1,111 @@
1
- """
2
- Verification commands - direct phone verification bypassing API
3
- """
4
- import click
5
- import json
6
- from datetime import datetime
7
- from ai_lls_lib.core.verifier import PhoneVerifier
8
- from ai_lls_lib.core.cache import DynamoDBCache
9
- from ai_lls_lib.cli.aws_client import AWSClient
10
-
11
- @click.group(name="verify")
12
- def verify_group():
13
- """Phone verification commands"""
14
- pass
15
-
16
- @verify_group.command(name="phone")
17
- @click.argument("phone_number")
18
- @click.option("--stack", default="landline-api", help="CloudFormation stack name")
19
- @click.option("--skip-cache", is_flag=True, help="Skip cache lookup")
20
- @click.option("--profile", help="AWS profile to use")
21
- @click.option("--region", help="AWS region")
22
- def verify_phone(phone_number, stack, skip_cache, profile, region):
23
- """Verify a single phone number"""
24
- aws = AWSClient(region=region, profile=profile)
25
-
26
- # Get cache table name from stack
27
- cache_table = aws.get_table_name(stack, "PhoneCacheTable")
28
- click.echo(f"Using cache table: {cache_table}")
29
-
30
- # Initialize cache and verifier
31
- cache = DynamoDBCache(table_name=cache_table)
32
- verifier = PhoneVerifier(cache=cache)
33
-
34
- try:
35
- if skip_cache:
36
- # Force fresh lookup
37
- normalized = verifier.normalize_phone(phone_number)
38
- line_type = verifier._check_line_type(normalized)
39
- dnc = verifier._check_dnc(normalized)
40
- result = {
41
- "phone_number": normalized,
42
- "line_type": line_type,
43
- "dnc": dnc,
44
- "cached": False,
45
- "verified_at": datetime.utcnow().isoformat(),
46
- "source": "cli-direct"
47
- }
48
- else:
49
- result = verifier.verify(phone_number)
50
- result = result.dict() if hasattr(result, 'dict') else result
51
-
52
- # Display results
53
- click.echo("\n" + "=" * 40)
54
- click.echo(f"Phone: {result['phone_number']}")
55
- click.echo(f"Line Type: {result['line_type']}")
56
- click.echo(f"DNC Status: {'Yes' if result['dnc'] else 'No'}")
57
- click.echo(f"From Cache: {'Yes' if result.get('cached') else 'No'}")
58
- click.echo(f"Verified: {result.get('verified_at', 'Unknown')}")
59
- click.echo("=" * 40)
60
-
61
- if click.confirm("\nShow JSON output?"):
62
- click.echo(json.dumps(result, indent=2, default=str))
63
-
64
- except ValueError as e:
65
- click.echo(f"Error: {e}", err=True)
66
- except Exception as e:
67
- click.echo(f"Verification failed: {e}", err=True)
68
-
69
- @verify_group.command(name="bulk")
70
- @click.argument("csv_file", type=click.Path(exists=True))
71
- @click.option("--output", "-o", help="Output CSV file")
72
- @click.option("--stack", default="landline-api", help="CloudFormation stack name")
73
- @click.option("--profile", help="AWS profile to use")
74
- @click.option("--region", help="AWS region")
75
- def verify_bulk(csv_file, output, stack, profile, region):
76
- """Process a CSV file for bulk verification"""
77
- from ai_lls_lib.core.processor import BulkProcessor
78
-
79
- aws = AWSClient(region=region, profile=profile)
80
- cache_table = aws.get_table_name(stack, "PhoneCacheTable")
81
-
82
- cache = DynamoDBCache(table_name=cache_table)
83
- verifier = PhoneVerifier(cache=cache)
84
- processor = BulkProcessor(verifier=verifier)
85
-
86
- click.echo(f"Processing {csv_file}...")
87
-
88
- try:
89
- # Process CSV
90
- results = processor.process_csv_sync(csv_file)
91
- click.echo(f"\nProcessed {len(results)} phone numbers")
92
-
93
- # Show summary
94
- mobile_count = sum(1 for r in results if r.line_type == "mobile")
95
- landline_count = sum(1 for r in results if r.line_type == "landline")
96
- dnc_count = sum(1 for r in results if r.dnc)
97
- cached_count = sum(1 for r in results if r.cached)
98
-
99
- click.echo("\nSummary:")
100
- click.echo(f" Mobile: {mobile_count}")
101
- click.echo(f" Landline: {landline_count}")
102
- click.echo(f" On DNC: {dnc_count}")
103
- click.echo(f" From Cache: {cached_count}")
104
-
105
- # Generate output if requested
106
- if output:
107
- processor.generate_results_csv(csv_file, results, output)
108
- click.echo(f"\nResults saved to: {output}")
109
-
110
- except Exception as e:
111
- click.echo(f"Bulk processing failed: {e}", err=True)
1
+ """
2
+ Verification commands - direct phone verification bypassing API
3
+ """
4
+ import click
5
+ import json
6
+ from datetime import datetime
7
+ from ai_lls_lib.core.verifier import PhoneVerifier
8
+ from ai_lls_lib.core.cache import DynamoDBCache
9
+ from ai_lls_lib.cli.aws_client import AWSClient
10
+
11
+ @click.group(name="verify")
12
+ def verify_group():
13
+ """Phone verification commands"""
14
+ pass
15
+
16
+ @verify_group.command(name="phone")
17
+ @click.argument("phone_number")
18
+ @click.option("--stack", default="landline-api", help="CloudFormation stack name")
19
+ @click.option("--skip-cache", is_flag=True, help="Skip cache lookup")
20
+ @click.option("--profile", help="AWS profile to use")
21
+ @click.option("--region", help="AWS region")
22
+ def verify_phone(phone_number, stack, skip_cache, profile, region):
23
+ """Verify a single phone number"""
24
+ aws = AWSClient(region=region, profile=profile)
25
+
26
+ # Get cache table name from stack
27
+ cache_table = aws.get_table_name(stack, "PhoneCacheTable")
28
+ click.echo(f"Using cache table: {cache_table}")
29
+
30
+ # Initialize cache and verifier
31
+ cache = DynamoDBCache(table_name=cache_table)
32
+ verifier = PhoneVerifier(cache=cache)
33
+
34
+ try:
35
+ if skip_cache:
36
+ # Force fresh lookup
37
+ normalized = verifier.normalize_phone(phone_number)
38
+ line_type = verifier._check_line_type(normalized)
39
+ dnc = verifier._check_dnc(normalized)
40
+ result = {
41
+ "phone_number": normalized,
42
+ "line_type": line_type,
43
+ "dnc": dnc,
44
+ "cached": False,
45
+ "verified_at": datetime.utcnow().isoformat(),
46
+ "source": "cli-direct"
47
+ }
48
+ else:
49
+ result = verifier.verify(phone_number)
50
+ result = result.dict() if hasattr(result, 'dict') else result
51
+
52
+ # Display results
53
+ click.echo("\n" + "=" * 40)
54
+ click.echo(f"Phone: {result['phone_number']}")
55
+ click.echo(f"Line Type: {result['line_type']}")
56
+ click.echo(f"DNC Status: {'Yes' if result['dnc'] else 'No'}")
57
+ click.echo(f"From Cache: {'Yes' if result.get('cached') else 'No'}")
58
+ click.echo(f"Verified: {result.get('verified_at', 'Unknown')}")
59
+ click.echo("=" * 40)
60
+
61
+ if click.confirm("\nShow JSON output?"):
62
+ click.echo(json.dumps(result, indent=2, default=str))
63
+
64
+ except ValueError as e:
65
+ click.echo(f"Error: {e}", err=True)
66
+ except Exception as e:
67
+ click.echo(f"Verification failed: {e}", err=True)
68
+
69
+ @verify_group.command(name="bulk")
70
+ @click.argument("csv_file", type=click.Path(exists=True))
71
+ @click.option("--output", "-o", help="Output CSV file")
72
+ @click.option("--stack", default="landline-api", help="CloudFormation stack name")
73
+ @click.option("--profile", help="AWS profile to use")
74
+ @click.option("--region", help="AWS region")
75
+ def verify_bulk(csv_file, output, stack, profile, region):
76
+ """Process a CSV file for bulk verification"""
77
+ from ai_lls_lib.core.processor import BulkProcessor
78
+
79
+ aws = AWSClient(region=region, profile=profile)
80
+ cache_table = aws.get_table_name(stack, "PhoneCacheTable")
81
+
82
+ cache = DynamoDBCache(table_name=cache_table)
83
+ verifier = PhoneVerifier(cache=cache)
84
+ processor = BulkProcessor(verifier=verifier)
85
+
86
+ click.echo(f"Processing {csv_file}...")
87
+
88
+ try:
89
+ # Process CSV
90
+ results = processor.process_csv_sync(csv_file)
91
+ click.echo(f"\nProcessed {len(results)} phone numbers")
92
+
93
+ # Show summary
94
+ mobile_count = sum(1 for r in results if r.line_type == "mobile")
95
+ landline_count = sum(1 for r in results if r.line_type == "landline")
96
+ dnc_count = sum(1 for r in results if r.dnc)
97
+ cached_count = sum(1 for r in results if r.cached)
98
+
99
+ click.echo("\nSummary:")
100
+ click.echo(f" Mobile: {mobile_count}")
101
+ click.echo(f" Landline: {landline_count}")
102
+ click.echo(f" On DNC: {dnc_count}")
103
+ click.echo(f" From Cache: {cached_count}")
104
+
105
+ # Generate output if requested
106
+ if output:
107
+ processor.generate_results_csv(csv_file, results, output)
108
+ click.echo(f"\nResults saved to: {output}")
109
+
110
+ except Exception as e:
111
+ click.echo(f"Bulk processing failed: {e}", err=True)