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.
- ai_lls_lib/__init__.py +1 -1
- ai_lls_lib/auth/__init__.py +4 -4
- ai_lls_lib/auth/context_parser.py +68 -68
- ai_lls_lib/cli/__init__.py +3 -3
- ai_lls_lib/cli/__main__.py +30 -30
- ai_lls_lib/cli/aws_client.py +115 -115
- ai_lls_lib/cli/commands/__init__.py +3 -3
- ai_lls_lib/cli/commands/admin.py +174 -174
- ai_lls_lib/cli/commands/cache.py +142 -142
- ai_lls_lib/cli/commands/stripe.py +377 -377
- ai_lls_lib/cli/commands/test_stack.py +216 -216
- ai_lls_lib/cli/commands/verify.py +111 -111
- ai_lls_lib/cli/env_loader.py +122 -122
- ai_lls_lib/core/__init__.py +3 -3
- ai_lls_lib/core/cache.py +106 -106
- ai_lls_lib/core/models.py +77 -77
- ai_lls_lib/core/processor.py +295 -295
- ai_lls_lib/core/verifier.py +84 -84
- ai_lls_lib/payment/__init__.py +13 -13
- ai_lls_lib/payment/credit_manager.py +186 -193
- ai_lls_lib/payment/models.py +102 -102
- ai_lls_lib/payment/stripe_manager.py +487 -487
- ai_lls_lib/payment/webhook_processor.py +215 -215
- ai_lls_lib/providers/__init__.py +7 -7
- ai_lls_lib/providers/base.py +28 -28
- ai_lls_lib/providers/external.py +87 -87
- ai_lls_lib/providers/stub.py +48 -48
- ai_lls_lib/testing/__init__.py +3 -3
- ai_lls_lib/testing/fixtures.py +104 -104
- {ai_lls_lib-1.4.0rc2.dist-info → ai_lls_lib-1.4.0rc4.dist-info}/METADATA +1 -1
- ai_lls_lib-1.4.0rc4.dist-info/RECORD +33 -0
- ai_lls_lib-1.4.0rc2.dist-info/RECORD +0 -33
- {ai_lls_lib-1.4.0rc2.dist-info → ai_lls_lib-1.4.0rc4.dist-info}/WHEEL +0 -0
- {ai_lls_lib-1.4.0rc2.dist-info → ai_lls_lib-1.4.0rc4.dist-info}/entry_points.txt +0 -0
ai_lls_lib/__init__.py
CHANGED
@@ -13,7 +13,7 @@ from ai_lls_lib.core.verifier import PhoneVerifier
|
|
13
13
|
from ai_lls_lib.core.processor import BulkProcessor
|
14
14
|
from ai_lls_lib.core.cache import DynamoDBCache
|
15
15
|
|
16
|
-
__version__ = "1.4.0-rc.
|
16
|
+
__version__ = "1.4.0-rc.4"
|
17
17
|
__all__ = [
|
18
18
|
"PhoneVerification",
|
19
19
|
"BulkJob",
|
ai_lls_lib/auth/__init__.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
"""Auth module for handling authentication and authorization."""
|
2
|
-
from .context_parser import get_email_from_event, get_user_from_event
|
3
|
-
|
4
|
-
__all__ = ["get_user_from_event", "get_email_from_event"]
|
1
|
+
"""Auth module for handling authentication and authorization."""
|
2
|
+
from .context_parser import get_email_from_event, get_user_from_event
|
3
|
+
|
4
|
+
__all__ = ["get_user_from_event", "get_email_from_event"]
|
@@ -1,68 +1,68 @@
|
|
1
|
-
"""Auth context parser for HTTP API v2.0 events."""
|
2
|
-
from typing import Any, Dict, Optional
|
3
|
-
|
4
|
-
|
5
|
-
def get_user_from_event(event: Dict[str, Any]) -> Optional[str]:
|
6
|
-
"""
|
7
|
-
Extract user ID from HTTP API v2.0 event with all possible paths.
|
8
|
-
Handles both JWT and API key authentication contexts.
|
9
|
-
|
10
|
-
This function handles the complexities of AWS API Gateway authorizer contexts,
|
11
|
-
especially when EnableSimpleResponses is set to false, which wraps the
|
12
|
-
context in a 'lambda' key.
|
13
|
-
|
14
|
-
Args:
|
15
|
-
event: The Lambda event from API Gateway HTTP API v2.0
|
16
|
-
|
17
|
-
Returns:
|
18
|
-
User ID string if found, None otherwise
|
19
|
-
"""
|
20
|
-
request_context = event.get("requestContext", {})
|
21
|
-
auth = request_context.get("authorizer", {})
|
22
|
-
|
23
|
-
# Handle lambda-wrapped context (EnableSimpleResponses: false)
|
24
|
-
# When EnableSimpleResponses is false, the authorizer context is wrapped
|
25
|
-
lam_ctx = auth.get("lambda", auth) if isinstance(auth.get("lambda"), dict) else auth
|
26
|
-
|
27
|
-
# Try all possible paths for user_id in priority order
|
28
|
-
user_id = (
|
29
|
-
# Lambda authorizer paths (most common with current setup)
|
30
|
-
lam_ctx.get("principal_id") or
|
31
|
-
lam_ctx.get("principalId") or
|
32
|
-
lam_ctx.get("sub") or
|
33
|
-
lam_ctx.get("user_id") or
|
34
|
-
# JWT paths (when using JWT authorizer directly)
|
35
|
-
auth.get("jwt", {}).get("claims", {}).get("sub") or
|
36
|
-
# Direct auth paths (fallback)
|
37
|
-
auth.get("principal_id") or
|
38
|
-
auth.get("principalId") or
|
39
|
-
auth.get("sub")
|
40
|
-
)
|
41
|
-
|
42
|
-
return user_id
|
43
|
-
|
44
|
-
|
45
|
-
def get_email_from_event(event: Dict[str, Any]) -> Optional[str]:
|
46
|
-
"""
|
47
|
-
Extract email from HTTP API v2.0 event.
|
48
|
-
|
49
|
-
Args:
|
50
|
-
event: The Lambda event from API Gateway HTTP API v2.0
|
51
|
-
|
52
|
-
Returns:
|
53
|
-
Email string if found, None otherwise
|
54
|
-
"""
|
55
|
-
request_context = event.get("requestContext", {})
|
56
|
-
auth = request_context.get("authorizer", {})
|
57
|
-
|
58
|
-
# Handle lambda-wrapped context
|
59
|
-
lam_ctx = auth.get("lambda", auth) if isinstance(auth.get("lambda"), dict) else auth
|
60
|
-
|
61
|
-
# Try to get email from various locations
|
62
|
-
email = (
|
63
|
-
lam_ctx.get("email") or
|
64
|
-
auth.get("jwt", {}).get("claims", {}).get("email") or
|
65
|
-
auth.get("email")
|
66
|
-
)
|
67
|
-
|
68
|
-
return email
|
1
|
+
"""Auth context parser for HTTP API v2.0 events."""
|
2
|
+
from typing import Any, Dict, Optional
|
3
|
+
|
4
|
+
|
5
|
+
def get_user_from_event(event: Dict[str, Any]) -> Optional[str]:
|
6
|
+
"""
|
7
|
+
Extract user ID from HTTP API v2.0 event with all possible paths.
|
8
|
+
Handles both JWT and API key authentication contexts.
|
9
|
+
|
10
|
+
This function handles the complexities of AWS API Gateway authorizer contexts,
|
11
|
+
especially when EnableSimpleResponses is set to false, which wraps the
|
12
|
+
context in a 'lambda' key.
|
13
|
+
|
14
|
+
Args:
|
15
|
+
event: The Lambda event from API Gateway HTTP API v2.0
|
16
|
+
|
17
|
+
Returns:
|
18
|
+
User ID string if found, None otherwise
|
19
|
+
"""
|
20
|
+
request_context = event.get("requestContext", {})
|
21
|
+
auth = request_context.get("authorizer", {})
|
22
|
+
|
23
|
+
# Handle lambda-wrapped context (EnableSimpleResponses: false)
|
24
|
+
# When EnableSimpleResponses is false, the authorizer context is wrapped
|
25
|
+
lam_ctx = auth.get("lambda", auth) if isinstance(auth.get("lambda"), dict) else auth
|
26
|
+
|
27
|
+
# Try all possible paths for user_id in priority order
|
28
|
+
user_id = (
|
29
|
+
# Lambda authorizer paths (most common with current setup)
|
30
|
+
lam_ctx.get("principal_id") or
|
31
|
+
lam_ctx.get("principalId") or
|
32
|
+
lam_ctx.get("sub") or
|
33
|
+
lam_ctx.get("user_id") or
|
34
|
+
# JWT paths (when using JWT authorizer directly)
|
35
|
+
auth.get("jwt", {}).get("claims", {}).get("sub") or
|
36
|
+
# Direct auth paths (fallback)
|
37
|
+
auth.get("principal_id") or
|
38
|
+
auth.get("principalId") or
|
39
|
+
auth.get("sub")
|
40
|
+
)
|
41
|
+
|
42
|
+
return user_id
|
43
|
+
|
44
|
+
|
45
|
+
def get_email_from_event(event: Dict[str, Any]) -> Optional[str]:
|
46
|
+
"""
|
47
|
+
Extract email from HTTP API v2.0 event.
|
48
|
+
|
49
|
+
Args:
|
50
|
+
event: The Lambda event from API Gateway HTTP API v2.0
|
51
|
+
|
52
|
+
Returns:
|
53
|
+
Email string if found, None otherwise
|
54
|
+
"""
|
55
|
+
request_context = event.get("requestContext", {})
|
56
|
+
auth = request_context.get("authorizer", {})
|
57
|
+
|
58
|
+
# Handle lambda-wrapped context
|
59
|
+
lam_ctx = auth.get("lambda", auth) if isinstance(auth.get("lambda"), dict) else auth
|
60
|
+
|
61
|
+
# Try to get email from various locations
|
62
|
+
email = (
|
63
|
+
lam_ctx.get("email") or
|
64
|
+
auth.get("jwt", {}).get("claims", {}).get("email") or
|
65
|
+
auth.get("email")
|
66
|
+
)
|
67
|
+
|
68
|
+
return email
|
ai_lls_lib/cli/__init__.py
CHANGED
@@ -1,3 +1,3 @@
|
|
1
|
-
"""
|
2
|
-
Landline Scrubber CLI - Infrastructure-aware administrative tools
|
3
|
-
"""
|
1
|
+
"""
|
2
|
+
Landline Scrubber CLI - Infrastructure-aware administrative tools
|
3
|
+
"""
|
ai_lls_lib/cli/__main__.py
CHANGED
@@ -1,30 +1,30 @@
|
|
1
|
-
"""
|
2
|
-
Landline Scrubber CLI entry point
|
3
|
-
"""
|
4
|
-
import click
|
5
|
-
import sys
|
6
|
-
from ai_lls_lib.cli.commands import verify, cache, admin, test_stack, stripe
|
7
|
-
|
8
|
-
@click.group()
|
9
|
-
@click.version_option(version="0.1.0", prog_name="ai-lls")
|
10
|
-
def cli():
|
11
|
-
"""Landline Scrubber CLI - Administrative and debugging tools"""
|
12
|
-
pass
|
13
|
-
|
14
|
-
# Register command groups
|
15
|
-
cli.add_command(verify.verify_group)
|
16
|
-
cli.add_command(cache.cache_group)
|
17
|
-
cli.add_command(admin.admin_group)
|
18
|
-
cli.add_command(test_stack.test_stack_group)
|
19
|
-
cli.add_command(stripe.stripe_group)
|
20
|
-
|
21
|
-
def main():
|
22
|
-
"""Main entry point"""
|
23
|
-
try:
|
24
|
-
cli()
|
25
|
-
except Exception as e:
|
26
|
-
click.echo(f"Error: {e}", err=True)
|
27
|
-
sys.exit(1)
|
28
|
-
|
29
|
-
if __name__ == "__main__":
|
30
|
-
main()
|
1
|
+
"""
|
2
|
+
Landline Scrubber CLI entry point
|
3
|
+
"""
|
4
|
+
import click
|
5
|
+
import sys
|
6
|
+
from ai_lls_lib.cli.commands import verify, cache, admin, test_stack, stripe
|
7
|
+
|
8
|
+
@click.group()
|
9
|
+
@click.version_option(version="0.1.0", prog_name="ai-lls")
|
10
|
+
def cli():
|
11
|
+
"""Landline Scrubber CLI - Administrative and debugging tools"""
|
12
|
+
pass
|
13
|
+
|
14
|
+
# Register command groups
|
15
|
+
cli.add_command(verify.verify_group)
|
16
|
+
cli.add_command(cache.cache_group)
|
17
|
+
cli.add_command(admin.admin_group)
|
18
|
+
cli.add_command(test_stack.test_stack_group)
|
19
|
+
cli.add_command(stripe.stripe_group)
|
20
|
+
|
21
|
+
def main():
|
22
|
+
"""Main entry point"""
|
23
|
+
try:
|
24
|
+
cli()
|
25
|
+
except Exception as e:
|
26
|
+
click.echo(f"Error: {e}", err=True)
|
27
|
+
sys.exit(1)
|
28
|
+
|
29
|
+
if __name__ == "__main__":
|
30
|
+
main()
|
ai_lls_lib/cli/aws_client.py
CHANGED
@@ -1,115 +1,115 @@
|
|
1
|
-
"""
|
2
|
-
AWS client utilities for CLI operations
|
3
|
-
"""
|
4
|
-
import os
|
5
|
-
import boto3
|
6
|
-
from typing import Optional, Any, Dict
|
7
|
-
from botocore.exceptions import ClientError
|
8
|
-
import click
|
9
|
-
|
10
|
-
class AWSClient:
|
11
|
-
"""Wrapper for AWS operations with proper error handling"""
|
12
|
-
|
13
|
-
def __init__(self, region: Optional[str] = None, profile: Optional[str] = None):
|
14
|
-
"""Initialize AWS clients"""
|
15
|
-
self.region = region or os.environ.get("AWS_REGION", "us-east-1")
|
16
|
-
self.profile = profile
|
17
|
-
|
18
|
-
# Create session
|
19
|
-
if profile:
|
20
|
-
self.session = boto3.Session(profile_name=profile, region_name=self.region)
|
21
|
-
else:
|
22
|
-
self.session = boto3.Session(region_name=self.region)
|
23
|
-
|
24
|
-
# Lazy-load clients
|
25
|
-
self._dynamodb = None
|
26
|
-
self._s3 = None
|
27
|
-
self._sqs = None
|
28
|
-
self._secretsmanager = None
|
29
|
-
self._cloudformation = None
|
30
|
-
|
31
|
-
@property
|
32
|
-
def dynamodb(self):
|
33
|
-
"""Get DynamoDB client"""
|
34
|
-
if not self._dynamodb:
|
35
|
-
self._dynamodb = self.session.resource('dynamodb')
|
36
|
-
return self._dynamodb
|
37
|
-
|
38
|
-
@property
|
39
|
-
def s3(self):
|
40
|
-
"""Get S3 client"""
|
41
|
-
if not self._s3:
|
42
|
-
self._s3 = self.session.client('s3')
|
43
|
-
return self._s3
|
44
|
-
|
45
|
-
@property
|
46
|
-
def sqs(self):
|
47
|
-
"""Get SQS client"""
|
48
|
-
if not self._sqs:
|
49
|
-
self._sqs = self.session.client('sqs')
|
50
|
-
return self._sqs
|
51
|
-
|
52
|
-
@property
|
53
|
-
def secretsmanager(self):
|
54
|
-
"""Get Secrets Manager client"""
|
55
|
-
if not self._secretsmanager:
|
56
|
-
self._secretsmanager = self.session.client('secretsmanager')
|
57
|
-
return self._secretsmanager
|
58
|
-
|
59
|
-
@property
|
60
|
-
def cloudformation(self):
|
61
|
-
"""Get CloudFormation client"""
|
62
|
-
if not self._cloudformation:
|
63
|
-
self._cloudformation = self.session.client('cloudformation')
|
64
|
-
return self._cloudformation
|
65
|
-
|
66
|
-
def get_stack_outputs(self, stack_name: str) -> Dict[str, str]:
|
67
|
-
"""Get CloudFormation stack outputs"""
|
68
|
-
try:
|
69
|
-
response = self.cloudformation.describe_stacks(StackName=stack_name)
|
70
|
-
stack = response['Stacks'][0]
|
71
|
-
outputs = {}
|
72
|
-
for output in stack.get('Outputs', []):
|
73
|
-
outputs[output['OutputKey']] = output['OutputValue']
|
74
|
-
return outputs
|
75
|
-
except ClientError as e:
|
76
|
-
if e.response['Error']['Code'] == 'ValidationError':
|
77
|
-
raise click.ClickException(f"Stack '{stack_name}' not found")
|
78
|
-
raise
|
79
|
-
|
80
|
-
def get_table_name(self, stack_name: str, logical_name: str) -> str:
|
81
|
-
"""Get actual table name from stack"""
|
82
|
-
try:
|
83
|
-
response = self.cloudformation.describe_stack_resource(
|
84
|
-
StackName=stack_name,
|
85
|
-
LogicalResourceId=logical_name
|
86
|
-
)
|
87
|
-
return response['StackResourceDetail']['PhysicalResourceId']
|
88
|
-
except ClientError:
|
89
|
-
# Fallback to conventional naming
|
90
|
-
return f"{stack_name}-{logical_name.lower()}"
|
91
|
-
|
92
|
-
def scan_table(self, table_name: str, limit: int = 100) -> list:
|
93
|
-
"""Scan DynamoDB table"""
|
94
|
-
try:
|
95
|
-
table = self.dynamodb.Table(table_name)
|
96
|
-
response = table.scan(Limit=limit)
|
97
|
-
return response.get('Items', [])
|
98
|
-
except ClientError as e:
|
99
|
-
raise click.ClickException(f"Error scanning table: {e}")
|
100
|
-
|
101
|
-
def put_item(self, table_name: str, item: dict) -> None:
|
102
|
-
"""Put item to DynamoDB"""
|
103
|
-
try:
|
104
|
-
table = self.dynamodb.Table(table_name)
|
105
|
-
table.put_item(Item=item)
|
106
|
-
except ClientError as e:
|
107
|
-
raise click.ClickException(f"Error putting item: {e}")
|
108
|
-
|
109
|
-
def delete_item(self, table_name: str, key: dict) -> None:
|
110
|
-
"""Delete item from DynamoDB"""
|
111
|
-
try:
|
112
|
-
table = self.dynamodb.Table(table_name)
|
113
|
-
table.delete_item(Key=key)
|
114
|
-
except ClientError as e:
|
115
|
-
raise click.ClickException(f"Error deleting item: {e}")
|
1
|
+
"""
|
2
|
+
AWS client utilities for CLI operations
|
3
|
+
"""
|
4
|
+
import os
|
5
|
+
import boto3
|
6
|
+
from typing import Optional, Any, Dict
|
7
|
+
from botocore.exceptions import ClientError
|
8
|
+
import click
|
9
|
+
|
10
|
+
class AWSClient:
|
11
|
+
"""Wrapper for AWS operations with proper error handling"""
|
12
|
+
|
13
|
+
def __init__(self, region: Optional[str] = None, profile: Optional[str] = None):
|
14
|
+
"""Initialize AWS clients"""
|
15
|
+
self.region = region or os.environ.get("AWS_REGION", "us-east-1")
|
16
|
+
self.profile = profile
|
17
|
+
|
18
|
+
# Create session
|
19
|
+
if profile:
|
20
|
+
self.session = boto3.Session(profile_name=profile, region_name=self.region)
|
21
|
+
else:
|
22
|
+
self.session = boto3.Session(region_name=self.region)
|
23
|
+
|
24
|
+
# Lazy-load clients
|
25
|
+
self._dynamodb = None
|
26
|
+
self._s3 = None
|
27
|
+
self._sqs = None
|
28
|
+
self._secretsmanager = None
|
29
|
+
self._cloudformation = None
|
30
|
+
|
31
|
+
@property
|
32
|
+
def dynamodb(self):
|
33
|
+
"""Get DynamoDB client"""
|
34
|
+
if not self._dynamodb:
|
35
|
+
self._dynamodb = self.session.resource('dynamodb')
|
36
|
+
return self._dynamodb
|
37
|
+
|
38
|
+
@property
|
39
|
+
def s3(self):
|
40
|
+
"""Get S3 client"""
|
41
|
+
if not self._s3:
|
42
|
+
self._s3 = self.session.client('s3')
|
43
|
+
return self._s3
|
44
|
+
|
45
|
+
@property
|
46
|
+
def sqs(self):
|
47
|
+
"""Get SQS client"""
|
48
|
+
if not self._sqs:
|
49
|
+
self._sqs = self.session.client('sqs')
|
50
|
+
return self._sqs
|
51
|
+
|
52
|
+
@property
|
53
|
+
def secretsmanager(self):
|
54
|
+
"""Get Secrets Manager client"""
|
55
|
+
if not self._secretsmanager:
|
56
|
+
self._secretsmanager = self.session.client('secretsmanager')
|
57
|
+
return self._secretsmanager
|
58
|
+
|
59
|
+
@property
|
60
|
+
def cloudformation(self):
|
61
|
+
"""Get CloudFormation client"""
|
62
|
+
if not self._cloudformation:
|
63
|
+
self._cloudformation = self.session.client('cloudformation')
|
64
|
+
return self._cloudformation
|
65
|
+
|
66
|
+
def get_stack_outputs(self, stack_name: str) -> Dict[str, str]:
|
67
|
+
"""Get CloudFormation stack outputs"""
|
68
|
+
try:
|
69
|
+
response = self.cloudformation.describe_stacks(StackName=stack_name)
|
70
|
+
stack = response['Stacks'][0]
|
71
|
+
outputs = {}
|
72
|
+
for output in stack.get('Outputs', []):
|
73
|
+
outputs[output['OutputKey']] = output['OutputValue']
|
74
|
+
return outputs
|
75
|
+
except ClientError as e:
|
76
|
+
if e.response['Error']['Code'] == 'ValidationError':
|
77
|
+
raise click.ClickException(f"Stack '{stack_name}' not found")
|
78
|
+
raise
|
79
|
+
|
80
|
+
def get_table_name(self, stack_name: str, logical_name: str) -> str:
|
81
|
+
"""Get actual table name from stack"""
|
82
|
+
try:
|
83
|
+
response = self.cloudformation.describe_stack_resource(
|
84
|
+
StackName=stack_name,
|
85
|
+
LogicalResourceId=logical_name
|
86
|
+
)
|
87
|
+
return response['StackResourceDetail']['PhysicalResourceId']
|
88
|
+
except ClientError:
|
89
|
+
# Fallback to conventional naming
|
90
|
+
return f"{stack_name}-{logical_name.lower()}"
|
91
|
+
|
92
|
+
def scan_table(self, table_name: str, limit: int = 100) -> list:
|
93
|
+
"""Scan DynamoDB table"""
|
94
|
+
try:
|
95
|
+
table = self.dynamodb.Table(table_name)
|
96
|
+
response = table.scan(Limit=limit)
|
97
|
+
return response.get('Items', [])
|
98
|
+
except ClientError as e:
|
99
|
+
raise click.ClickException(f"Error scanning table: {e}")
|
100
|
+
|
101
|
+
def put_item(self, table_name: str, item: dict) -> None:
|
102
|
+
"""Put item to DynamoDB"""
|
103
|
+
try:
|
104
|
+
table = self.dynamodb.Table(table_name)
|
105
|
+
table.put_item(Item=item)
|
106
|
+
except ClientError as e:
|
107
|
+
raise click.ClickException(f"Error putting item: {e}")
|
108
|
+
|
109
|
+
def delete_item(self, table_name: str, key: dict) -> None:
|
110
|
+
"""Delete item from DynamoDB"""
|
111
|
+
try:
|
112
|
+
table = self.dynamodb.Table(table_name)
|
113
|
+
table.delete_item(Key=key)
|
114
|
+
except ClientError as e:
|
115
|
+
raise click.ClickException(f"Error deleting item: {e}")
|
@@ -1,3 +1,3 @@
|
|
1
|
-
"""
|
2
|
-
CLI command modules
|
3
|
-
"""
|
1
|
+
"""
|
2
|
+
CLI command modules
|
3
|
+
"""
|