ai-lls-lib 1.4.0rc3__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 -186
  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.0rc3.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.0rc3.dist-info/RECORD +0 -33
  33. {ai_lls_lib-1.4.0rc3.dist-info → ai_lls_lib-1.4.0rc4.dist-info}/WHEEL +0 -0
  34. {ai_lls_lib-1.4.0rc3.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.3"
16
+ __version__ = "1.4.0-rc.4"
17
17
  __all__ = [
18
18
  "PhoneVerification",
19
19
  "BulkJob",
@@ -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
@@ -1,3 +1,3 @@
1
- """
2
- Landline Scrubber CLI - Infrastructure-aware administrative tools
3
- """
1
+ """
2
+ Landline Scrubber CLI - Infrastructure-aware administrative tools
3
+ """
@@ -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()
@@ -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
+ """