ai-lls-lib 1.5.0rc3__py3-none-any.whl → 1.5.0rc6__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.
Potentially problematic release.
This version of ai-lls-lib might be problematic. Click here for more details.
- ai_lls_lib/__init__.py +1 -1
- ai_lls_lib/cli/__main__.py +35 -2
- ai_lls_lib/cli/aws_client.py +8 -0
- ai_lls_lib/cli/commands/monitor.py +297 -0
- ai_lls_lib/cli/commands/verify.py +147 -54
- ai_lls_lib/cli/env_loader.py +3 -2
- ai_lls_lib/core/verifier.py +17 -11
- ai_lls_lib/providers/__init__.py +2 -1
- ai_lls_lib/providers/external.py +99 -35
- ai_lls_lib/providers/stub.py +1 -1
- {ai_lls_lib-1.5.0rc3.dist-info → ai_lls_lib-1.5.0rc6.dist-info}/METADATA +28 -1
- {ai_lls_lib-1.5.0rc3.dist-info → ai_lls_lib-1.5.0rc6.dist-info}/RECORD +14 -13
- {ai_lls_lib-1.5.0rc3.dist-info → ai_lls_lib-1.5.0rc6.dist-info}/WHEEL +0 -0
- {ai_lls_lib-1.5.0rc3.dist-info → ai_lls_lib-1.5.0rc6.dist-info}/entry_points.txt +0 -0
ai_lls_lib/__init__.py
CHANGED
|
@@ -17,7 +17,7 @@ from ai_lls_lib.core.verifier import PhoneVerifier
|
|
|
17
17
|
from ai_lls_lib.core.processor import BulkProcessor
|
|
18
18
|
from ai_lls_lib.core.cache import DynamoDBCache
|
|
19
19
|
|
|
20
|
-
__version__ = "1.5.0-rc.
|
|
20
|
+
__version__ = "1.5.0-rc.6"
|
|
21
21
|
__all__ = [
|
|
22
22
|
"PhoneVerification",
|
|
23
23
|
"BulkJob",
|
ai_lls_lib/cli/__main__.py
CHANGED
|
@@ -3,13 +3,45 @@ Landline Scrubber CLI entry point
|
|
|
3
3
|
"""
|
|
4
4
|
import click
|
|
5
5
|
import sys
|
|
6
|
-
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from ai_lls_lib.cli.commands import verify, cache, admin, test_stack, stripe, monitor
|
|
9
|
+
from ai_lls_lib.cli.env_loader import load_env_file
|
|
10
|
+
|
|
11
|
+
def load_env_files(verbose=False):
|
|
12
|
+
"""Load .env files from ~/.lls and current directory"""
|
|
13
|
+
# Load from ~/.lls/.env
|
|
14
|
+
home_env = Path.home() / '.lls' / '.env'
|
|
15
|
+
if home_env.exists():
|
|
16
|
+
env_vars = load_env_file(home_env)
|
|
17
|
+
for key, value in env_vars.items():
|
|
18
|
+
if key not in os.environ: # Don't override existing env vars
|
|
19
|
+
os.environ[key] = value
|
|
20
|
+
if env_vars and verbose:
|
|
21
|
+
click.echo(f"Loaded {len(env_vars)} variables from {home_env}", err=True)
|
|
22
|
+
|
|
23
|
+
# Load from current directory .env
|
|
24
|
+
local_env = Path('.env')
|
|
25
|
+
if local_env.exists():
|
|
26
|
+
env_vars = load_env_file(local_env)
|
|
27
|
+
for key, value in env_vars.items():
|
|
28
|
+
if key not in os.environ: # Don't override existing env vars
|
|
29
|
+
os.environ[key] = value
|
|
30
|
+
if env_vars and verbose:
|
|
31
|
+
click.echo(f"Loaded {len(env_vars)} variables from {local_env}", err=True)
|
|
32
|
+
|
|
33
|
+
# Set default environment to 'prod' if not set
|
|
34
|
+
if 'ENVIRONMENT' not in os.environ:
|
|
35
|
+
os.environ['ENVIRONMENT'] = 'prod'
|
|
36
|
+
if verbose:
|
|
37
|
+
click.echo(f"Set default ENVIRONMENT=prod", err=True)
|
|
7
38
|
|
|
8
39
|
@click.group()
|
|
9
40
|
@click.version_option(version="0.1.0", prog_name="ai-lls")
|
|
10
41
|
def cli():
|
|
11
42
|
"""Landline Scrubber CLI - Administrative and debugging tools"""
|
|
12
|
-
|
|
43
|
+
# Load environment files at startup
|
|
44
|
+
load_env_files()
|
|
13
45
|
|
|
14
46
|
# Register command groups
|
|
15
47
|
cli.add_command(verify.verify_group)
|
|
@@ -17,6 +49,7 @@ cli.add_command(cache.cache_group)
|
|
|
17
49
|
cli.add_command(admin.admin_group)
|
|
18
50
|
cli.add_command(test_stack.test_stack_group)
|
|
19
51
|
cli.add_command(stripe.stripe_group)
|
|
52
|
+
cli.add_command(monitor.monitor_group)
|
|
20
53
|
|
|
21
54
|
def main():
|
|
22
55
|
"""Main entry point"""
|
ai_lls_lib/cli/aws_client.py
CHANGED
|
@@ -27,6 +27,7 @@ class AWSClient:
|
|
|
27
27
|
self._sqs = None
|
|
28
28
|
self._secretsmanager = None
|
|
29
29
|
self._cloudformation = None
|
|
30
|
+
self._logs = None
|
|
30
31
|
|
|
31
32
|
@property
|
|
32
33
|
def dynamodb(self):
|
|
@@ -63,6 +64,13 @@ class AWSClient:
|
|
|
63
64
|
self._cloudformation = self.session.client('cloudformation')
|
|
64
65
|
return self._cloudformation
|
|
65
66
|
|
|
67
|
+
@property
|
|
68
|
+
def logs(self):
|
|
69
|
+
"""Get CloudWatch Logs client"""
|
|
70
|
+
if not self._logs:
|
|
71
|
+
self._logs = self.session.client('logs')
|
|
72
|
+
return self._logs
|
|
73
|
+
|
|
66
74
|
def get_stack_outputs(self, stack_name: str) -> Dict[str, str]:
|
|
67
75
|
"""Get CloudFormation stack outputs"""
|
|
68
76
|
try:
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Real-time CloudWatch log monitoring for Lambda functions
|
|
3
|
+
"""
|
|
4
|
+
import click
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
from datetime import datetime, timedelta
|
|
8
|
+
from typing import Optional, List, Dict
|
|
9
|
+
from botocore.exceptions import ClientError
|
|
10
|
+
from ai_lls_lib.cli.aws_client import AWSClient
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.syntax import Syntax
|
|
15
|
+
from rich.table import Table
|
|
16
|
+
from rich.live import Live
|
|
17
|
+
from rich.panel import Panel
|
|
18
|
+
from rich.text import Text
|
|
19
|
+
from rich import box
|
|
20
|
+
HAS_RICH = True
|
|
21
|
+
except ImportError:
|
|
22
|
+
HAS_RICH = False
|
|
23
|
+
Console = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_log_groups(aws_client: AWSClient, environment: str) -> List[str]:
|
|
27
|
+
"""Get relevant CloudWatch log groups for the environment"""
|
|
28
|
+
# Map environment to stack name pattern
|
|
29
|
+
stack_prefix = "landline-api-staging" if environment == "staging" else "landline-api"
|
|
30
|
+
|
|
31
|
+
# Key Lambda functions to monitor
|
|
32
|
+
lambda_functions = [
|
|
33
|
+
"PhoneVerifyHandler",
|
|
34
|
+
"BulkUploadHandler",
|
|
35
|
+
"BulkProcessHandler",
|
|
36
|
+
"PlansHandler",
|
|
37
|
+
"PaymentHandler",
|
|
38
|
+
"UserProfileHandler"
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
log_groups = []
|
|
42
|
+
for func in lambda_functions:
|
|
43
|
+
log_group = f"/aws/lambda/{stack_prefix}-{func}"
|
|
44
|
+
log_groups.append(log_group)
|
|
45
|
+
|
|
46
|
+
return log_groups
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def format_log_event(event: Dict, use_rich: bool = True) -> str:
|
|
50
|
+
"""Format a CloudWatch log event for display"""
|
|
51
|
+
timestamp = datetime.fromtimestamp(event.get('timestamp', 0) / 1000)
|
|
52
|
+
message = event.get('message', '')
|
|
53
|
+
|
|
54
|
+
# Try to parse JSON message
|
|
55
|
+
try:
|
|
56
|
+
if message.strip().startswith('{'):
|
|
57
|
+
parsed = json.loads(message)
|
|
58
|
+
|
|
59
|
+
# Check for external API calls
|
|
60
|
+
if 'external_api_call' in message.lower() or 'landlineremover.com' in message:
|
|
61
|
+
if use_rich and HAS_RICH:
|
|
62
|
+
return f"[bold green]{timestamp.strftime('%H:%M:%S')}[/bold green] [cyan]EXTERNAL API:[/cyan] {message}"
|
|
63
|
+
else:
|
|
64
|
+
return f"{timestamp.strftime('%H:%M:%S')} EXTERNAL API: {message}"
|
|
65
|
+
|
|
66
|
+
# Check for cache events
|
|
67
|
+
if 'cache' in message.lower():
|
|
68
|
+
if use_rich and HAS_RICH:
|
|
69
|
+
return f"[bold blue]{timestamp.strftime('%H:%M:%S')}[/bold blue] [yellow]CACHE:[/yellow] {message}"
|
|
70
|
+
else:
|
|
71
|
+
return f"{timestamp.strftime('%H:%M:%S')} CACHE: {message}"
|
|
72
|
+
|
|
73
|
+
# Check for errors
|
|
74
|
+
if 'error' in message.lower() or 'exception' in message.lower():
|
|
75
|
+
if use_rich and HAS_RICH:
|
|
76
|
+
return f"[bold red]{timestamp.strftime('%H:%M:%S')}[/bold red] [red]ERROR:[/red] {message}"
|
|
77
|
+
else:
|
|
78
|
+
return f"{timestamp.strftime('%H:%M:%S')} ERROR: {message}"
|
|
79
|
+
|
|
80
|
+
# Format structured logs
|
|
81
|
+
if use_rich and HAS_RICH:
|
|
82
|
+
message = json.dumps(parsed, indent=2)
|
|
83
|
+
except (json.JSONDecodeError, ValueError):
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
# Default formatting
|
|
87
|
+
if use_rich and HAS_RICH:
|
|
88
|
+
return f"[dim]{timestamp.strftime('%H:%M:%S')}[/dim] {message}"
|
|
89
|
+
else:
|
|
90
|
+
return f"{timestamp.strftime('%H:%M:%S')} {message}"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def tail_logs(aws_client: AWSClient, log_groups: List[str], duration: int = 300, follow: bool = True):
|
|
94
|
+
"""Tail CloudWatch logs in real-time"""
|
|
95
|
+
console = Console() if HAS_RICH else None
|
|
96
|
+
|
|
97
|
+
# Calculate start time
|
|
98
|
+
start_time = int((datetime.utcnow() - timedelta(seconds=duration)).timestamp() * 1000)
|
|
99
|
+
|
|
100
|
+
# Track last event time for each log group
|
|
101
|
+
last_event_times = {group: start_time for group in log_groups}
|
|
102
|
+
|
|
103
|
+
if console and HAS_RICH:
|
|
104
|
+
console.print(Panel.fit(
|
|
105
|
+
f"[bold cyan]Monitoring {len(log_groups)} Lambda functions[/bold cyan]\n"
|
|
106
|
+
f"[dim]Press Ctrl+C to stop[/dim]",
|
|
107
|
+
title="CloudWatch Log Monitor",
|
|
108
|
+
border_style="cyan"
|
|
109
|
+
))
|
|
110
|
+
console.print()
|
|
111
|
+
else:
|
|
112
|
+
click.echo(f"Monitoring {len(log_groups)} Lambda functions")
|
|
113
|
+
click.echo("Press Ctrl+C to stop\n")
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
while True:
|
|
117
|
+
events_found = False
|
|
118
|
+
|
|
119
|
+
for log_group in log_groups:
|
|
120
|
+
try:
|
|
121
|
+
# Use filter_log_events for real-time tailing
|
|
122
|
+
response = aws_client.logs.filter_log_events(
|
|
123
|
+
logGroupName=log_group,
|
|
124
|
+
startTime=last_event_times[log_group],
|
|
125
|
+
limit=100
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
events = response.get('events', [])
|
|
129
|
+
|
|
130
|
+
if events:
|
|
131
|
+
events_found = True
|
|
132
|
+
# Update last event time
|
|
133
|
+
last_event_times[log_group] = events[-1]['timestamp'] + 1
|
|
134
|
+
|
|
135
|
+
# Display log group header
|
|
136
|
+
if console and HAS_RICH:
|
|
137
|
+
console.print(f"\n[bold magenta]═══ {log_group} ═══[/bold magenta]")
|
|
138
|
+
else:
|
|
139
|
+
click.echo(f"\n=== {log_group} ===")
|
|
140
|
+
|
|
141
|
+
# Display events
|
|
142
|
+
for event in events:
|
|
143
|
+
formatted = format_log_event(event, use_rich=(console is not None))
|
|
144
|
+
if console and HAS_RICH:
|
|
145
|
+
console.print(formatted)
|
|
146
|
+
else:
|
|
147
|
+
click.echo(formatted)
|
|
148
|
+
|
|
149
|
+
except ClientError as e:
|
|
150
|
+
if e.response['Error']['Code'] != 'ResourceNotFoundException':
|
|
151
|
+
if console and HAS_RICH:
|
|
152
|
+
console.print(f"[yellow]Warning: {e}[/yellow]")
|
|
153
|
+
else:
|
|
154
|
+
click.echo(f"Warning: {e}", err=True)
|
|
155
|
+
|
|
156
|
+
if not follow:
|
|
157
|
+
break
|
|
158
|
+
|
|
159
|
+
# Sleep briefly before next poll
|
|
160
|
+
time.sleep(2 if events_found else 5)
|
|
161
|
+
|
|
162
|
+
except KeyboardInterrupt:
|
|
163
|
+
if console and HAS_RICH:
|
|
164
|
+
console.print("\n[yellow]Monitoring stopped[/yellow]")
|
|
165
|
+
else:
|
|
166
|
+
click.echo("\nMonitoring stopped")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def start_live_tail(aws_client: AWSClient, log_groups: List[str]):
|
|
170
|
+
"""Start a CloudWatch Logs Live Tail session (requires AWS SDK v2)"""
|
|
171
|
+
console = Console() if HAS_RICH else None
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
# Start live tail session
|
|
175
|
+
response = aws_client.logs.start_live_tail(
|
|
176
|
+
logGroupIdentifiers=log_groups,
|
|
177
|
+
logStreamNamePrefixes=[],
|
|
178
|
+
logEventFilterPattern=""
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
session_id = response.get('sessionId')
|
|
182
|
+
session_url = response.get('sessionUrl')
|
|
183
|
+
|
|
184
|
+
if console and HAS_RICH:
|
|
185
|
+
console.print(Panel.fit(
|
|
186
|
+
f"[bold green]Live Tail session started[/bold green]\n"
|
|
187
|
+
f"Session ID: {session_id}\n"
|
|
188
|
+
f"[dim]Note: Live Tail API requires WebSocket support[/dim]",
|
|
189
|
+
title="CloudWatch Live Tail",
|
|
190
|
+
border_style="green"
|
|
191
|
+
))
|
|
192
|
+
else:
|
|
193
|
+
click.echo(f"Live Tail session started")
|
|
194
|
+
click.echo(f"Session ID: {session_id}")
|
|
195
|
+
click.echo(f"Note: Live Tail API requires WebSocket support")
|
|
196
|
+
|
|
197
|
+
# Note: Full WebSocket implementation would be needed here
|
|
198
|
+
# For now, fall back to filter_log_events polling
|
|
199
|
+
click.echo("\nFalling back to standard log polling...")
|
|
200
|
+
tail_logs(aws_client, log_groups, duration=300, follow=True)
|
|
201
|
+
|
|
202
|
+
except ClientError as e:
|
|
203
|
+
if 'start_live_tail' in str(e):
|
|
204
|
+
# Fall back to regular tailing if Live Tail not available
|
|
205
|
+
if console and HAS_RICH:
|
|
206
|
+
console.print("[yellow]Live Tail API not available, using standard polling[/yellow]")
|
|
207
|
+
else:
|
|
208
|
+
click.echo("Live Tail API not available, using standard polling")
|
|
209
|
+
tail_logs(aws_client, log_groups, duration=300, follow=True)
|
|
210
|
+
else:
|
|
211
|
+
raise
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@click.group(name="monitor")
|
|
215
|
+
def monitor_group():
|
|
216
|
+
"""Monitor CloudWatch logs in real-time"""
|
|
217
|
+
pass
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@monitor_group.command()
|
|
221
|
+
@click.option('--staging', is_flag=True, help='Monitor staging environment (default: production)')
|
|
222
|
+
@click.option('--production', is_flag=True, help='Monitor production environment')
|
|
223
|
+
@click.option('--duration', default=300, help='How far back to start (seconds, default: 300)')
|
|
224
|
+
@click.option('--follow', is_flag=True, default=True, help='Follow logs in real-time (default: true)')
|
|
225
|
+
@click.option('--filter', 'filter_pattern', help='CloudWatch filter pattern to apply')
|
|
226
|
+
@click.option('--profile', help='AWS profile to use')
|
|
227
|
+
@click.option('--region', default='us-east-1', help='AWS region')
|
|
228
|
+
def logs(staging, production, duration, follow, filter_pattern, profile, region):
|
|
229
|
+
"""Tail CloudWatch logs for Lambda functions
|
|
230
|
+
|
|
231
|
+
Examples:
|
|
232
|
+
ai-lls monitor logs --staging
|
|
233
|
+
ai-lls monitor logs --production
|
|
234
|
+
ai-lls monitor logs --staging --filter "ERROR"
|
|
235
|
+
ai-lls monitor logs --duration 600 --follow
|
|
236
|
+
"""
|
|
237
|
+
# Determine environment
|
|
238
|
+
if staging and production:
|
|
239
|
+
raise click.ClickException("Cannot specify both --staging and --production")
|
|
240
|
+
|
|
241
|
+
environment = "staging" if staging else "production"
|
|
242
|
+
|
|
243
|
+
# Show warning if Rich not installed
|
|
244
|
+
if not HAS_RICH:
|
|
245
|
+
click.echo("Warning: Install 'rich' library for better output formatting", err=True)
|
|
246
|
+
click.echo("Run: pip install rich", err=True)
|
|
247
|
+
click.echo()
|
|
248
|
+
|
|
249
|
+
# Initialize AWS client
|
|
250
|
+
aws_client = AWSClient(region=region, profile=profile)
|
|
251
|
+
|
|
252
|
+
# Get log groups
|
|
253
|
+
log_groups = get_log_groups(aws_client, environment)
|
|
254
|
+
|
|
255
|
+
click.echo(f"Monitoring {environment} environment...")
|
|
256
|
+
click.echo(f"Log groups: {', '.join([g.split('/')[-1] for g in log_groups])}")
|
|
257
|
+
click.echo()
|
|
258
|
+
|
|
259
|
+
# Start tailing logs
|
|
260
|
+
tail_logs(aws_client, log_groups, duration=duration, follow=follow)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@monitor_group.command()
|
|
264
|
+
@click.option('--staging', is_flag=True, help='Monitor staging environment')
|
|
265
|
+
@click.option('--production', is_flag=True, help='Monitor production environment')
|
|
266
|
+
@click.option('--profile', help='AWS profile to use')
|
|
267
|
+
@click.option('--region', default='us-east-1', help='AWS region')
|
|
268
|
+
def live(staging, production, profile, region):
|
|
269
|
+
"""Start CloudWatch Logs Live Tail session (experimental)
|
|
270
|
+
|
|
271
|
+
Uses the CloudWatch Logs Live Tail API for real-time streaming.
|
|
272
|
+
Falls back to standard polling if Live Tail is not available.
|
|
273
|
+
|
|
274
|
+
Examples:
|
|
275
|
+
ai-lls monitor live --staging
|
|
276
|
+
ai-lls monitor live --production
|
|
277
|
+
"""
|
|
278
|
+
# Determine environment
|
|
279
|
+
if staging and production:
|
|
280
|
+
raise click.ClickException("Cannot specify both --staging and --production")
|
|
281
|
+
|
|
282
|
+
environment = "staging" if staging else "production"
|
|
283
|
+
|
|
284
|
+
# Initialize AWS client
|
|
285
|
+
aws_client = AWSClient(region=region, profile=profile)
|
|
286
|
+
|
|
287
|
+
# Get log groups
|
|
288
|
+
log_groups = get_log_groups(aws_client, environment)
|
|
289
|
+
|
|
290
|
+
click.echo(f"Starting Live Tail for {environment} environment...")
|
|
291
|
+
|
|
292
|
+
# Start live tail
|
|
293
|
+
start_live_tail(aws_client, log_groups)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
# Export the group for registration
|
|
297
|
+
__all__ = ['monitor_group']
|
|
@@ -7,6 +7,7 @@ from datetime import datetime
|
|
|
7
7
|
from ai_lls_lib.core.verifier import PhoneVerifier
|
|
8
8
|
from ai_lls_lib.core.cache import DynamoDBCache
|
|
9
9
|
from ai_lls_lib.cli.aws_client import AWSClient
|
|
10
|
+
from ai_lls_lib.providers import StubProvider, ExternalAPIProvider
|
|
10
11
|
|
|
11
12
|
@click.group(name="verify")
|
|
12
13
|
def verify_group():
|
|
@@ -15,72 +16,160 @@ def verify_group():
|
|
|
15
16
|
|
|
16
17
|
@verify_group.command(name="phone")
|
|
17
18
|
@click.argument("phone_number")
|
|
18
|
-
@click.option("--
|
|
19
|
-
@click.option("--
|
|
20
|
-
@click.option("--
|
|
21
|
-
@click.option("--
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
19
|
+
@click.option("--output", type=click.Choice(["json", "text"], case_sensitive=False), default="json", help="Output format")
|
|
20
|
+
@click.option("--provider", type=click.Choice(["external", "stub"], case_sensitive=False), default="external", help="Verification provider to use")
|
|
21
|
+
@click.option("--no-cache", is_flag=True, help="Disable caching even if AWS is available")
|
|
22
|
+
@click.option("--stack", default="landline-api", help="CloudFormation stack name (for caching)")
|
|
23
|
+
@click.option("--profile", help="AWS profile to use (for caching)")
|
|
24
|
+
@click.option("--region", help="AWS region (for caching)")
|
|
25
|
+
@click.option("--verbose", is_flag=True, help="Show detailed output")
|
|
26
|
+
def verify_phone(phone_number, output, provider, no_cache, stack, profile, region, verbose):
|
|
27
|
+
"""
|
|
28
|
+
Verify a phone number (works with or without AWS).
|
|
29
|
+
|
|
30
|
+
Accepts phone numbers in various formats:
|
|
31
|
+
- 6197966726 (10 digits)
|
|
32
|
+
- 16197966726 (11 digits with 1)
|
|
33
|
+
- +16197966726 (E.164 format)
|
|
34
|
+
- 619-796-6726 (with dashes)
|
|
35
|
+
- (619) 796-6726 (with parentheses)
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
# Select provider based on parameter
|
|
39
|
+
if provider == "stub":
|
|
40
|
+
provider_instance = StubProvider()
|
|
41
|
+
if verbose:
|
|
42
|
+
click.echo("Using StubProvider (deterministic test mode)")
|
|
43
|
+
else:
|
|
44
|
+
provider_instance = ExternalAPIProvider()
|
|
45
|
+
if verbose:
|
|
46
|
+
click.echo("Using ExternalAPIProvider (real API calls)")
|
|
47
|
+
|
|
48
|
+
# Try to set up caching if not explicitly disabled
|
|
49
|
+
cache = None
|
|
50
|
+
cache_status = ""
|
|
51
|
+
|
|
52
|
+
if not no_cache:
|
|
53
|
+
try:
|
|
54
|
+
aws = AWSClient(region=region, profile=profile)
|
|
55
|
+
cache_table = aws.get_table_name(stack, "PhoneCacheTable")
|
|
56
|
+
cache = DynamoDBCache(table_name=cache_table)
|
|
57
|
+
cache_status = "(Using cache)"
|
|
58
|
+
if verbose:
|
|
59
|
+
click.echo(f"Using DynamoDB cache table: {cache_table}")
|
|
60
|
+
except Exception as e:
|
|
61
|
+
cache_status = "(No cache - direct API call)"
|
|
62
|
+
if verbose:
|
|
63
|
+
click.echo(f"Cache not available: {e}")
|
|
64
|
+
click.echo("Continuing without cache (direct API calls)")
|
|
65
|
+
else:
|
|
66
|
+
cache_status = "(Cache disabled)"
|
|
67
|
+
if verbose:
|
|
68
|
+
click.echo("Cache explicitly disabled with --no-cache")
|
|
69
|
+
|
|
70
|
+
# Initialize verifier (works with or without cache)
|
|
71
|
+
verifier = PhoneVerifier(cache=cache, provider=provider_instance)
|
|
33
72
|
|
|
34
73
|
try:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
74
|
+
# Verify the phone number
|
|
75
|
+
result = verifier.verify(phone_number)
|
|
76
|
+
result_dict = result.dict() if hasattr(result, 'dict') else result
|
|
77
|
+
|
|
78
|
+
# Prepare clean output
|
|
79
|
+
# Extract line type value if it's an enum
|
|
80
|
+
line_type = result_dict['line_type']
|
|
81
|
+
if hasattr(line_type, 'value'):
|
|
82
|
+
line_type = line_type.value
|
|
83
|
+
|
|
84
|
+
# Build clean output dictionary
|
|
85
|
+
output_data = {
|
|
86
|
+
"phone": result_dict['phone_number'],
|
|
87
|
+
"line_type": line_type,
|
|
88
|
+
"dnc": result_dict['dnc'],
|
|
89
|
+
"cached": result_dict.get('cached', False),
|
|
90
|
+
"verified_at": str(result_dict.get('verified_at', 'Unknown'))
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if output == "json":
|
|
94
|
+
# JSON output (default for composability)
|
|
95
|
+
click.echo(json.dumps(output_data, indent=2))
|
|
48
96
|
else:
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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))
|
|
97
|
+
# Text output (human-readable)
|
|
98
|
+
click.echo(f"Phone: {output_data['phone']}")
|
|
99
|
+
click.echo(f"Line Type: {output_data['line_type']}")
|
|
100
|
+
click.echo(f"DNC: {output_data['dnc']}")
|
|
101
|
+
click.echo(f"Cached: {output_data['cached']}")
|
|
102
|
+
click.echo(f"Verified: {output_data['verified_at']}")
|
|
63
103
|
|
|
64
104
|
except ValueError as e:
|
|
65
|
-
|
|
105
|
+
if output == "json":
|
|
106
|
+
error_data = {"error": str(e)}
|
|
107
|
+
click.echo(json.dumps(error_data), err=True)
|
|
108
|
+
else:
|
|
109
|
+
click.echo(f"Error: {e}", err=True)
|
|
110
|
+
click.echo("\nSupported formats:", err=True)
|
|
111
|
+
click.echo(" 6197966726 (10 digits)", err=True)
|
|
112
|
+
click.echo(" 16197966726 (11 digits)", err=True)
|
|
113
|
+
click.echo(" +16197966726 (E.164)", err=True)
|
|
114
|
+
click.echo(" 619-796-6726 (with dashes)", err=True)
|
|
115
|
+
click.echo(" (619) 796-6726 (with parentheses)", err=True)
|
|
116
|
+
import sys
|
|
117
|
+
sys.exit(1)
|
|
66
118
|
except Exception as e:
|
|
67
|
-
|
|
119
|
+
if output == "json":
|
|
120
|
+
error_data = {"error": str(e)}
|
|
121
|
+
click.echo(json.dumps(error_data), err=True)
|
|
122
|
+
else:
|
|
123
|
+
click.echo(f"Verification failed: {e}", err=True)
|
|
124
|
+
if verbose:
|
|
125
|
+
import traceback
|
|
126
|
+
click.echo(traceback.format_exc(), err=True)
|
|
127
|
+
import sys
|
|
128
|
+
sys.exit(1)
|
|
68
129
|
|
|
69
130
|
@verify_group.command(name="bulk")
|
|
70
131
|
@click.argument("csv_file", type=click.Path(exists=True))
|
|
71
132
|
@click.option("--output", "-o", help="Output CSV file")
|
|
72
|
-
@click.option("--
|
|
73
|
-
@click.option("--
|
|
74
|
-
@click.option("--
|
|
75
|
-
|
|
133
|
+
@click.option("--provider", type=click.Choice(["external", "stub"], case_sensitive=False), default="external", help="Verification provider to use")
|
|
134
|
+
@click.option("--no-cache", is_flag=True, help="Disable caching even if AWS is available")
|
|
135
|
+
@click.option("--stack", default="landline-api", help="CloudFormation stack name (for caching)")
|
|
136
|
+
@click.option("--profile", help="AWS profile to use (for caching)")
|
|
137
|
+
@click.option("--region", help="AWS region (for caching)")
|
|
138
|
+
@click.option("--verbose", is_flag=True, help="Show detailed output")
|
|
139
|
+
def verify_bulk(csv_file, output, provider, no_cache, stack, profile, region, verbose):
|
|
76
140
|
"""Process a CSV file for bulk verification"""
|
|
77
141
|
from ai_lls_lib.core.processor import BulkProcessor
|
|
78
142
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
143
|
+
# Select provider based on parameter
|
|
144
|
+
if provider == "stub":
|
|
145
|
+
provider_instance = StubProvider()
|
|
146
|
+
if verbose:
|
|
147
|
+
click.echo("Using StubProvider (deterministic test mode)")
|
|
148
|
+
else:
|
|
149
|
+
provider_instance = ExternalAPIProvider()
|
|
150
|
+
if verbose:
|
|
151
|
+
click.echo("Using ExternalAPIProvider (real API calls)")
|
|
152
|
+
|
|
153
|
+
# Try to set up caching if not explicitly disabled
|
|
154
|
+
cache = None
|
|
155
|
+
|
|
156
|
+
if not no_cache:
|
|
157
|
+
try:
|
|
158
|
+
aws = AWSClient(region=region, profile=profile)
|
|
159
|
+
cache_table = aws.get_table_name(stack, "PhoneCacheTable")
|
|
160
|
+
cache = DynamoDBCache(table_name=cache_table)
|
|
161
|
+
if verbose:
|
|
162
|
+
click.echo(f"Using DynamoDB cache table: {cache_table}")
|
|
163
|
+
except Exception as e:
|
|
164
|
+
if verbose:
|
|
165
|
+
click.echo(f"Cache not available: {e}")
|
|
166
|
+
click.echo("Continuing without cache (direct API calls)")
|
|
167
|
+
else:
|
|
168
|
+
if verbose:
|
|
169
|
+
click.echo("Cache explicitly disabled with --no-cache")
|
|
170
|
+
|
|
171
|
+
# Initialize verifier (works with or without cache)
|
|
172
|
+
verifier = PhoneVerifier(cache=cache, provider=provider_instance)
|
|
84
173
|
processor = BulkProcessor(verifier=verifier)
|
|
85
174
|
|
|
86
175
|
click.echo(f"Processing {csv_file}...")
|
|
@@ -100,7 +189,8 @@ def verify_bulk(csv_file, output, stack, profile, region):
|
|
|
100
189
|
click.echo(f" Mobile: {mobile_count}")
|
|
101
190
|
click.echo(f" Landline: {landline_count}")
|
|
102
191
|
click.echo(f" On DNC: {dnc_count}")
|
|
103
|
-
|
|
192
|
+
if cache:
|
|
193
|
+
click.echo(f" From Cache: {cached_count}")
|
|
104
194
|
|
|
105
195
|
# Generate output if requested
|
|
106
196
|
if output:
|
|
@@ -109,3 +199,6 @@ def verify_bulk(csv_file, output, stack, profile, region):
|
|
|
109
199
|
|
|
110
200
|
except Exception as e:
|
|
111
201
|
click.echo(f"Bulk processing failed: {e}", err=True)
|
|
202
|
+
if verbose:
|
|
203
|
+
import traceback
|
|
204
|
+
click.echo(traceback.format_exc(), err=True)
|
ai_lls_lib/cli/env_loader.py
CHANGED
|
@@ -6,7 +6,7 @@ from typing import Optional, Dict
|
|
|
6
6
|
import click
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
def load_env_file(env_path: Path) -> Dict[str, str]:
|
|
9
|
+
def load_env_file(env_path: Path, verbose: bool = False) -> Dict[str, str]:
|
|
10
10
|
"""Load environment variables from a .env file."""
|
|
11
11
|
env_vars = {}
|
|
12
12
|
if env_path.exists():
|
|
@@ -20,7 +20,8 @@ def load_env_file(env_path: Path) -> Dict[str, str]:
|
|
|
20
20
|
value = value.strip().strip('"').strip("'")
|
|
21
21
|
env_vars[key.strip()] = value
|
|
22
22
|
except Exception as e:
|
|
23
|
-
|
|
23
|
+
if verbose:
|
|
24
|
+
click.echo(f"Warning: Could not read {env_path}: {e}", err=True)
|
|
24
25
|
return env_vars
|
|
25
26
|
|
|
26
27
|
|
ai_lls_lib/core/verifier.py
CHANGED
|
@@ -8,7 +8,7 @@ import phonenumbers
|
|
|
8
8
|
from aws_lambda_powertools import Logger
|
|
9
9
|
from .models import PhoneVerification, LineType, VerificationSource
|
|
10
10
|
from .cache import DynamoDBCache
|
|
11
|
-
from ..providers import VerificationProvider, StubProvider
|
|
11
|
+
from ..providers import VerificationProvider, StubProvider, ExternalAPIProvider
|
|
12
12
|
|
|
13
13
|
logger = Logger()
|
|
14
14
|
|
|
@@ -16,16 +16,16 @@ logger = Logger()
|
|
|
16
16
|
class PhoneVerifier:
|
|
17
17
|
"""Verifies phone numbers for line type and DNC status"""
|
|
18
18
|
|
|
19
|
-
def __init__(self, cache: DynamoDBCache, provider: Optional[VerificationProvider] = None):
|
|
19
|
+
def __init__(self, cache: Optional[DynamoDBCache] = None, provider: Optional[VerificationProvider] = None):
|
|
20
20
|
"""
|
|
21
21
|
Initialize phone verifier.
|
|
22
22
|
|
|
23
23
|
Args:
|
|
24
|
-
cache: DynamoDB cache for storing results
|
|
25
|
-
provider: Verification provider (defaults to
|
|
24
|
+
cache: Optional DynamoDB cache for storing results
|
|
25
|
+
provider: Verification provider (defaults to ExternalAPIProvider)
|
|
26
26
|
"""
|
|
27
27
|
self.cache = cache
|
|
28
|
-
self.provider = provider or
|
|
28
|
+
self.provider = provider or ExternalAPIProvider()
|
|
29
29
|
|
|
30
30
|
def normalize_phone(self, phone: str) -> str:
|
|
31
31
|
"""Normalize phone to E.164 format"""
|
|
@@ -45,10 +45,11 @@ class PhoneVerifier:
|
|
|
45
45
|
"""Verify phone number for line type and DNC status"""
|
|
46
46
|
normalized = self.normalize_phone(phone)
|
|
47
47
|
|
|
48
|
-
# Check cache first
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
# Check cache first if available
|
|
49
|
+
if self.cache:
|
|
50
|
+
cached = self.cache.get(normalized)
|
|
51
|
+
if cached:
|
|
52
|
+
return cached
|
|
52
53
|
|
|
53
54
|
# Use provider to verify
|
|
54
55
|
line_type, dnc_status = self.provider.verify_phone(normalized)
|
|
@@ -62,8 +63,13 @@ class PhoneVerifier:
|
|
|
62
63
|
source=VerificationSource.API
|
|
63
64
|
)
|
|
64
65
|
|
|
65
|
-
# Store in cache
|
|
66
|
-
self.cache
|
|
66
|
+
# Store in cache if available
|
|
67
|
+
if self.cache:
|
|
68
|
+
try:
|
|
69
|
+
self.cache.set(normalized, result)
|
|
70
|
+
except Exception as e:
|
|
71
|
+
logger.warning(f"Failed to cache result: {e}")
|
|
72
|
+
# Continue without caching - don't fail the verification
|
|
67
73
|
|
|
68
74
|
return result
|
|
69
75
|
|
ai_lls_lib/providers/__init__.py
CHANGED
|
@@ -3,5 +3,6 @@ Verification providers for phone number checking
|
|
|
3
3
|
"""
|
|
4
4
|
from .base import VerificationProvider
|
|
5
5
|
from .stub import StubProvider
|
|
6
|
+
from .external import ExternalAPIProvider
|
|
6
7
|
|
|
7
|
-
__all__ = ["VerificationProvider", "StubProvider"]
|
|
8
|
+
__all__ = ["VerificationProvider", "StubProvider", "ExternalAPIProvider"]
|
ai_lls_lib/providers/external.py
CHANGED
|
@@ -13,29 +13,45 @@ logger = Logger()
|
|
|
13
13
|
class ExternalAPIProvider:
|
|
14
14
|
"""
|
|
15
15
|
Production provider that calls external verification APIs.
|
|
16
|
+
Uses landlineremover.com which returns both line type and DNC status in a single call.
|
|
16
17
|
"""
|
|
17
18
|
|
|
18
19
|
def __init__(
|
|
19
20
|
self,
|
|
20
|
-
|
|
21
|
-
dnc_api_key: Optional[str] = None,
|
|
21
|
+
api_key: Optional[str] = None,
|
|
22
22
|
timeout: float = 10.0
|
|
23
23
|
):
|
|
24
24
|
"""
|
|
25
25
|
Initialize external API provider.
|
|
26
26
|
|
|
27
27
|
Args:
|
|
28
|
-
|
|
29
|
-
dnc_api_key: API key for DNC list checking
|
|
28
|
+
api_key: API key for landlineremover.com (if not provided, uses environment)
|
|
30
29
|
timeout: HTTP request timeout in seconds
|
|
31
30
|
"""
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
# Get environment-specific API key
|
|
32
|
+
# Default to 'prod' if not specified
|
|
33
|
+
env = os.environ.get("ENVIRONMENT", "prod").upper()
|
|
34
|
+
|
|
35
|
+
# Try environment-specific key first, then fall back to provided key
|
|
36
|
+
self.api_key = (
|
|
37
|
+
api_key or
|
|
38
|
+
os.environ.get(f"{env}_LANDLINE_API_KEY") or
|
|
39
|
+
os.environ.get("PHONE_VERIFY_API_KEY", "")
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if not self.api_key:
|
|
43
|
+
logger.warning(f"No API key found for environment {env}")
|
|
44
|
+
|
|
45
|
+
self.api_url = "https://app.landlineremover.com/api/check-number"
|
|
46
|
+
self.timeout = timeout
|
|
34
47
|
self.http_client = httpx.Client(timeout=timeout)
|
|
35
48
|
|
|
36
49
|
def verify_phone(self, phone: str) -> Tuple[LineType, bool]:
|
|
37
50
|
"""
|
|
38
|
-
Verify phone using
|
|
51
|
+
Verify phone using landlineremover.com API.
|
|
52
|
+
|
|
53
|
+
This API returns both line type and DNC status in a single call,
|
|
54
|
+
which is more efficient than making two separate API calls.
|
|
39
55
|
|
|
40
56
|
Args:
|
|
41
57
|
phone: E.164 formatted phone number
|
|
@@ -47,41 +63,89 @@ class ExternalAPIProvider:
|
|
|
47
63
|
httpx.HTTPError: For API communication errors
|
|
48
64
|
ValueError: For invalid responses
|
|
49
65
|
"""
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
66
|
+
logger.debug(f"Verifying phone {phone[:6]}*** via external API")
|
|
67
|
+
|
|
68
|
+
if not self.api_key:
|
|
69
|
+
raise ValueError("API key not configured")
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
# Make single API call that returns both line type and DNC status
|
|
73
|
+
# The API may redirect, so we need to follow redirects
|
|
74
|
+
response = self.http_client.get(
|
|
75
|
+
self.api_url,
|
|
76
|
+
params={
|
|
77
|
+
"apikey": self.api_key,
|
|
78
|
+
"number": phone
|
|
79
|
+
},
|
|
80
|
+
follow_redirects=True
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Raise for HTTP errors
|
|
84
|
+
response.raise_for_status()
|
|
85
|
+
|
|
86
|
+
# Parse response
|
|
87
|
+
json_response = response.json()
|
|
88
|
+
|
|
89
|
+
# Extract data from response wrapper
|
|
90
|
+
if "data" in json_response:
|
|
91
|
+
data = json_response["data"]
|
|
92
|
+
else:
|
|
93
|
+
data = json_response
|
|
94
|
+
|
|
95
|
+
# Map line type from API response
|
|
96
|
+
line_type = self._map_line_type(data)
|
|
97
|
+
|
|
98
|
+
# Map DNC status - API uses "DNCType" field
|
|
99
|
+
# Values can be "dnc", "clean", etc.
|
|
100
|
+
dnc_type = data.get("DNCType", data.get("dnc_type", "")).lower()
|
|
101
|
+
is_dnc = dnc_type != "clean" and dnc_type != ""
|
|
102
|
+
|
|
103
|
+
logger.debug(
|
|
104
|
+
f"Verification complete for {phone[:6]}***",
|
|
105
|
+
extra={
|
|
106
|
+
"line_type": line_type.value,
|
|
107
|
+
"is_dnc": is_dnc,
|
|
108
|
+
"dnc_type": dnc_type
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return line_type, is_dnc
|
|
113
|
+
|
|
114
|
+
except httpx.HTTPStatusError as e:
|
|
115
|
+
logger.error(f"API error: {e.response.status_code} - {e.response.text}")
|
|
116
|
+
raise ValueError(f"API request failed with status {e.response.status_code}")
|
|
117
|
+
except httpx.RequestError as e:
|
|
118
|
+
logger.error(f"Network error: {str(e)}")
|
|
119
|
+
raise ValueError(f"Network error during API call: {str(e)}")
|
|
120
|
+
except Exception as e:
|
|
121
|
+
logger.error(f"Unexpected error during verification: {str(e)}")
|
|
122
|
+
raise
|
|
123
|
+
|
|
124
|
+
def _map_line_type(self, data: dict) -> LineType:
|
|
62
125
|
"""
|
|
63
|
-
|
|
126
|
+
Map API response to LineType enum.
|
|
64
127
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
raise NotImplementedError("External line type API not yet configured")
|
|
128
|
+
Args:
|
|
129
|
+
data: API response dictionary
|
|
68
130
|
|
|
69
|
-
|
|
131
|
+
Returns:
|
|
132
|
+
LineType enum value
|
|
70
133
|
"""
|
|
71
|
-
|
|
134
|
+
# API uses "LineType" (capitalized) field
|
|
135
|
+
line_type_str = data.get("LineType", data.get("line_type", "")).lower()
|
|
72
136
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
137
|
+
# Map common line types
|
|
138
|
+
line_type_map = {
|
|
139
|
+
"mobile": LineType.MOBILE,
|
|
140
|
+
"landline": LineType.LANDLINE,
|
|
141
|
+
"voip": LineType.VOIP,
|
|
142
|
+
"wireless": LineType.MOBILE, # Some APIs return "wireless" for mobile
|
|
143
|
+
"fixed": LineType.LANDLINE, # Some APIs return "fixed" for landline
|
|
144
|
+
}
|
|
79
145
|
|
|
80
|
-
|
|
81
|
-
# In production, this would make an actual API call
|
|
82
|
-
raise NotImplementedError("External DNC API not yet configured")
|
|
146
|
+
return line_type_map.get(line_type_str, LineType.UNKNOWN)
|
|
83
147
|
|
|
84
148
|
def __del__(self):
|
|
85
149
|
"""Cleanup HTTP client"""
|
|
86
150
|
if hasattr(self, 'http_client'):
|
|
87
|
-
self.http_client.close()
|
|
151
|
+
self.http_client.close()
|
ai_lls_lib/providers/stub.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ai-lls-lib
|
|
3
|
-
Version: 1.5.
|
|
3
|
+
Version: 1.5.0rc6
|
|
4
4
|
Summary: Landline Scrubber core library - phone verification and DNC checking
|
|
5
5
|
Author: LandlineScrubber Team
|
|
6
6
|
Requires-Python: >=3.12,<4.0
|
|
@@ -186,6 +186,33 @@ ai-lls test-stack test
|
|
|
186
186
|
ai-lls test-stack delete
|
|
187
187
|
```
|
|
188
188
|
|
|
189
|
+
### CloudWatch Log Monitoring
|
|
190
|
+
```bash
|
|
191
|
+
# Monitor staging environment logs in real-time
|
|
192
|
+
ai-lls monitor logs --staging
|
|
193
|
+
|
|
194
|
+
# Monitor production environment logs
|
|
195
|
+
ai-lls monitor logs --production
|
|
196
|
+
|
|
197
|
+
# Monitor with custom duration (look back 10 minutes)
|
|
198
|
+
ai-lls monitor logs --staging --duration 600
|
|
199
|
+
|
|
200
|
+
# Filter logs for errors only
|
|
201
|
+
ai-lls monitor logs --staging --filter "ERROR"
|
|
202
|
+
|
|
203
|
+
# Use specific AWS profile
|
|
204
|
+
ai-lls monitor logs --staging --profile myprofile
|
|
205
|
+
|
|
206
|
+
# Experimental: Use CloudWatch Logs Live Tail API
|
|
207
|
+
ai-lls monitor live --staging
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
The monitor command provides real-time log streaming from Lambda functions with:
|
|
211
|
+
- Color-coded output for different event types (external API calls, cache events, errors)
|
|
212
|
+
- Support for multiple log groups simultaneously
|
|
213
|
+
- Rich formatting when the `rich` library is installed
|
|
214
|
+
- Automatic detection of external API calls to landlineremover.com
|
|
215
|
+
|
|
189
216
|
## Project Structure
|
|
190
217
|
|
|
191
218
|
```
|
|
@@ -1,33 +1,34 @@
|
|
|
1
|
-
ai_lls_lib/__init__.py,sha256=
|
|
1
|
+
ai_lls_lib/__init__.py,sha256=VEn9fUTfDKihNIElOeNEacIk1RuEqYShI84UcSSzC3U,734
|
|
2
2
|
ai_lls_lib/auth/__init__.py,sha256=ZjwlfTvYUo9A9BaLinaXW2elxYectScmu0XtKAppSFs,198
|
|
3
3
|
ai_lls_lib/auth/context_parser.py,sha256=AWyGWejrFR0jJ7fMhajdA1ABo656Qlvs2YAzz-IfTrs,2290
|
|
4
4
|
ai_lls_lib/cli/__init__.py,sha256=HyjnIBGlrbTlHQ82n_UppjtJbZl4_7QsnAgRqNlXKMU,77
|
|
5
|
-
ai_lls_lib/cli/__main__.py,sha256=
|
|
6
|
-
ai_lls_lib/cli/aws_client.py,sha256=
|
|
5
|
+
ai_lls_lib/cli/__main__.py,sha256=OLpqz78s2nAtib7soqI-WQ2_V9aqa9Cd4NRwvso0IbM,2120
|
|
6
|
+
ai_lls_lib/cli/aws_client.py,sha256=yJ_seXyAOSy1xFqUHD8CScnN5un18b2412a7pUG_UQs,4283
|
|
7
7
|
ai_lls_lib/cli/commands/__init__.py,sha256=yi0bdkZSPIXY33apvJRcuK410-Ta0-_-n31MQYuL_aU,31
|
|
8
8
|
ai_lls_lib/cli/commands/admin.py,sha256=JlFljbblVWJkJ2z2FIM6P0fy3Pmc0VqG62Y3POtvRFw,6861
|
|
9
9
|
ai_lls_lib/cli/commands/cache.py,sha256=1Vm3m5vXJkRagNmQXpT8_YEsQEv92veJUFwA_v9FPb0,5822
|
|
10
|
+
ai_lls_lib/cli/commands/monitor.py,sha256=TLbf5Q_viwF7q_aPXo9XenzY4W0R1bjO68Iu8KlzvoE,11315
|
|
10
11
|
ai_lls_lib/cli/commands/stripe.py,sha256=_aRsz6Z7BN-S9_07efUovXwsbL-eCE-raw1uBOkDP5g,15276
|
|
11
12
|
ai_lls_lib/cli/commands/test_stack.py,sha256=FK6ITOek7j4e0QiSNTv_taLghNnR8d_kWG02cl0A3rY,7594
|
|
12
|
-
ai_lls_lib/cli/commands/verify.py,sha256=
|
|
13
|
-
ai_lls_lib/cli/env_loader.py,sha256=
|
|
13
|
+
ai_lls_lib/cli/commands/verify.py,sha256=8rTg8xbdQbJdebDy5CT7HFcJ-ODFZdAOAo2JX82ARJs,8522
|
|
14
|
+
ai_lls_lib/cli/env_loader.py,sha256=ZL8GykQw8gf--aSMvIR4pdfQYKIA8bbJDWWNUpxZYsI,4260
|
|
14
15
|
ai_lls_lib/core/__init__.py,sha256=ATh5sGhhB9g5Sd2H0kRqoCA0hxqe6UWonlPq2Wnl_i8,39
|
|
15
16
|
ai_lls_lib/core/cache.py,sha256=4HSVaRvv_wueLV30fVbTk_KWZmXjR2mBrGl7UNT_5kQ,3817
|
|
16
17
|
ai_lls_lib/core/models.py,sha256=jzvc-SlP-nLgvkom9hj-TwKHxBePbEfC9xL_Tt1Uh4Y,2458
|
|
17
18
|
ai_lls_lib/core/processor.py,sha256=8PI_j1ZMOD1hru3PyV5QMmMx7Em8X0ivq8prMXwait4,10269
|
|
18
|
-
ai_lls_lib/core/verifier.py,sha256=
|
|
19
|
+
ai_lls_lib/core/verifier.py,sha256=mQBgzV-vZiiKGzeeVl3TwfOWGrYTeB_zQ6wpaI07u54,3147
|
|
19
20
|
ai_lls_lib/payment/__init__.py,sha256=uQwWZ2tw-cxS06hbWiKu8ubxzzYCmxeKBQQU7m_mRX4,308
|
|
20
21
|
ai_lls_lib/payment/credit_manager.py,sha256=DH_t8AMx74vexegoTD3UkFNA1XuomIQWOZu9VoJMO3E,7340
|
|
21
22
|
ai_lls_lib/payment/models.py,sha256=LxGop5e1hhUmBCpnzJwU-dDQSLHy6uM6gSFYYo2V_FI,3609
|
|
22
23
|
ai_lls_lib/payment/stripe_manager.py,sha256=cSJvRWk1OT16SlDh5BMyoEQD8TU0IzeP0TaL8e2U4zA,18423
|
|
23
24
|
ai_lls_lib/payment/webhook_processor.py,sha256=bAf8oUBzvCsb1stzKZ-cjZs02T-RJl-MeVnZmnnj5UM,8980
|
|
24
|
-
ai_lls_lib/providers/__init__.py,sha256=
|
|
25
|
+
ai_lls_lib/providers/__init__.py,sha256=dBjXJHn2nnA8KUuLYkgCJjZZzWeYAPPd6IJHUd7b-Kk,252
|
|
25
26
|
ai_lls_lib/providers/base.py,sha256=WSnSkHtRvwpX8QtGINjO-EWsZL0augp5E8M1DPwpf00,709
|
|
26
|
-
ai_lls_lib/providers/external.py,sha256=
|
|
27
|
-
ai_lls_lib/providers/stub.py,sha256=
|
|
27
|
+
ai_lls_lib/providers/external.py,sha256=MKu3BZV8gfSqV3lNcF9Vt4UmKIJixQ5Q7FI2wFeSBz0,5075
|
|
28
|
+
ai_lls_lib/providers/stub.py,sha256=XXUObkIb3hs7_fYMA9mlLh1llJUGIgqQHVjgZHZl1T8,1199
|
|
28
29
|
ai_lls_lib/testing/__init__.py,sha256=NytBz7T0YtNLDArRYHsczaeopIQmmBeV21-XXUprdeY,42
|
|
29
30
|
ai_lls_lib/testing/fixtures.py,sha256=OC1huorIqUndePoUTfgoU-aDvsWAFJqmQ6fGDjH9E14,3414
|
|
30
|
-
ai_lls_lib-1.5.
|
|
31
|
-
ai_lls_lib-1.5.
|
|
32
|
-
ai_lls_lib-1.5.
|
|
33
|
-
ai_lls_lib-1.5.
|
|
31
|
+
ai_lls_lib-1.5.0rc6.dist-info/METADATA,sha256=H5Jg1MxGH3ZQHmsohefmIhHOrdlyVp-ooqtjKIb5nf8,8175
|
|
32
|
+
ai_lls_lib-1.5.0rc6.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
33
|
+
ai_lls_lib-1.5.0rc6.dist-info/entry_points.txt,sha256=Pi0V_HBViEKGFbNQKatl5lhhnHHBXlxaom-5gH9gXZ0,55
|
|
34
|
+
ai_lls_lib-1.5.0rc6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|