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.
- 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 -186
- 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.0rc3.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.0rc3.dist-info/RECORD +0 -33
- {ai_lls_lib-1.4.0rc3.dist-info → ai_lls_lib-1.4.0rc4.dist-info}/WHEEL +0 -0
- {ai_lls_lib-1.4.0rc3.dist-info → ai_lls_lib-1.4.0rc4.dist-info}/entry_points.txt +0 -0
ai_lls_lib/cli/env_loader.py
CHANGED
@@ -1,122 +1,122 @@
|
|
1
|
-
"""Environment variable loader for CLI commands."""
|
2
|
-
|
3
|
-
import os
|
4
|
-
from pathlib import Path
|
5
|
-
from typing import Optional, Dict
|
6
|
-
import click
|
7
|
-
|
8
|
-
|
9
|
-
def load_env_file(env_path: Path) -> Dict[str, str]:
|
10
|
-
"""Load environment variables from a .env file."""
|
11
|
-
env_vars = {}
|
12
|
-
if env_path.exists():
|
13
|
-
try:
|
14
|
-
with open(env_path, 'r') as f:
|
15
|
-
for line in f:
|
16
|
-
line = line.strip()
|
17
|
-
if line and not line.startswith('#') and '=' in line:
|
18
|
-
key, value = line.split('=', 1)
|
19
|
-
# Remove quotes if present
|
20
|
-
value = value.strip().strip('"').strip("'")
|
21
|
-
env_vars[key.strip()] = value
|
22
|
-
except Exception as e:
|
23
|
-
click.echo(f"Warning: Could not read {env_path}: {e}", err=True)
|
24
|
-
return env_vars
|
25
|
-
|
26
|
-
|
27
|
-
def load_environment_config() -> Dict[str, str]:
|
28
|
-
"""
|
29
|
-
Load environment variables from multiple sources in order:
|
30
|
-
1. ~/.lls/.env (user global config)
|
31
|
-
2. ./.env (project local config)
|
32
|
-
3. System environment variables (highest priority)
|
33
|
-
|
34
|
-
Returns merged dictionary with system env taking precedence.
|
35
|
-
"""
|
36
|
-
env_vars = {}
|
37
|
-
|
38
|
-
# Load from ~/.lls/.env
|
39
|
-
home_env = Path.home() / '.lls' / '.env'
|
40
|
-
if home_env.exists():
|
41
|
-
home_vars = load_env_file(home_env)
|
42
|
-
env_vars.update(home_vars)
|
43
|
-
click.echo(f"Loaded {len(home_vars)} variables from {home_env}", err=True)
|
44
|
-
|
45
|
-
# Load from ./.env
|
46
|
-
local_env = Path('.env')
|
47
|
-
if local_env.exists():
|
48
|
-
local_vars = load_env_file(local_env)
|
49
|
-
env_vars.update(local_vars)
|
50
|
-
click.echo(f"Loaded {len(local_vars)} variables from {local_env}", err=True)
|
51
|
-
|
52
|
-
# System environment variables override file-based ones
|
53
|
-
env_vars.update(os.environ)
|
54
|
-
|
55
|
-
return env_vars
|
56
|
-
|
57
|
-
|
58
|
-
def get_stripe_key(environment: str, env_vars: Optional[Dict[str, str]] = None) -> Optional[str]:
|
59
|
-
"""
|
60
|
-
Get Stripe API key for the specified environment.
|
61
|
-
|
62
|
-
Looks for keys in this order:
|
63
|
-
1. STAGING_STRIPE_SECRET_KEY or PROD_STRIPE_SECRET_KEY (based on environment)
|
64
|
-
2. STRIPE_SECRET_KEY (fallback)
|
65
|
-
|
66
|
-
Args:
|
67
|
-
environment: 'staging' or 'production'
|
68
|
-
env_vars: Optional pre-loaded environment variables
|
69
|
-
|
70
|
-
Returns:
|
71
|
-
Stripe API key or None if not found
|
72
|
-
"""
|
73
|
-
if env_vars is None:
|
74
|
-
env_vars = load_environment_config()
|
75
|
-
|
76
|
-
# Map environment names to prefixes
|
77
|
-
env_prefix = {
|
78
|
-
'staging': 'STAGING',
|
79
|
-
'production': 'PROD'
|
80
|
-
}.get(environment, environment.upper())
|
81
|
-
|
82
|
-
# Try environment-specific key first
|
83
|
-
env_key = f"{env_prefix}_STRIPE_SECRET_KEY"
|
84
|
-
if env_key in env_vars:
|
85
|
-
return env_vars[env_key]
|
86
|
-
|
87
|
-
# Fall back to generic key
|
88
|
-
if 'STRIPE_SECRET_KEY' in env_vars:
|
89
|
-
return env_vars['STRIPE_SECRET_KEY']
|
90
|
-
|
91
|
-
return None
|
92
|
-
|
93
|
-
|
94
|
-
def get_env_variable(key: str, environment: Optional[str] = None,
|
95
|
-
env_vars: Optional[Dict[str, str]] = None) -> Optional[str]:
|
96
|
-
"""
|
97
|
-
Get an environment variable, optionally with environment prefix.
|
98
|
-
|
99
|
-
Args:
|
100
|
-
key: Variable name (e.g., 'API_URL')
|
101
|
-
environment: Optional environment ('staging' or 'production')
|
102
|
-
env_vars: Optional pre-loaded environment variables
|
103
|
-
|
104
|
-
Returns:
|
105
|
-
Variable value or None if not found
|
106
|
-
"""
|
107
|
-
if env_vars is None:
|
108
|
-
env_vars = load_environment_config()
|
109
|
-
|
110
|
-
if environment:
|
111
|
-
env_prefix = {
|
112
|
-
'staging': 'STAGING',
|
113
|
-
'production': 'PROD'
|
114
|
-
}.get(environment, environment.upper())
|
115
|
-
|
116
|
-
# Try environment-specific key first
|
117
|
-
env_key = f"{env_prefix}_{key}"
|
118
|
-
if env_key in env_vars:
|
119
|
-
return env_vars[env_key]
|
120
|
-
|
121
|
-
# Fall back to non-prefixed key
|
122
|
-
return env_vars.get(key)
|
1
|
+
"""Environment variable loader for CLI commands."""
|
2
|
+
|
3
|
+
import os
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import Optional, Dict
|
6
|
+
import click
|
7
|
+
|
8
|
+
|
9
|
+
def load_env_file(env_path: Path) -> Dict[str, str]:
|
10
|
+
"""Load environment variables from a .env file."""
|
11
|
+
env_vars = {}
|
12
|
+
if env_path.exists():
|
13
|
+
try:
|
14
|
+
with open(env_path, 'r') as f:
|
15
|
+
for line in f:
|
16
|
+
line = line.strip()
|
17
|
+
if line and not line.startswith('#') and '=' in line:
|
18
|
+
key, value = line.split('=', 1)
|
19
|
+
# Remove quotes if present
|
20
|
+
value = value.strip().strip('"').strip("'")
|
21
|
+
env_vars[key.strip()] = value
|
22
|
+
except Exception as e:
|
23
|
+
click.echo(f"Warning: Could not read {env_path}: {e}", err=True)
|
24
|
+
return env_vars
|
25
|
+
|
26
|
+
|
27
|
+
def load_environment_config() -> Dict[str, str]:
|
28
|
+
"""
|
29
|
+
Load environment variables from multiple sources in order:
|
30
|
+
1. ~/.lls/.env (user global config)
|
31
|
+
2. ./.env (project local config)
|
32
|
+
3. System environment variables (highest priority)
|
33
|
+
|
34
|
+
Returns merged dictionary with system env taking precedence.
|
35
|
+
"""
|
36
|
+
env_vars = {}
|
37
|
+
|
38
|
+
# Load from ~/.lls/.env
|
39
|
+
home_env = Path.home() / '.lls' / '.env'
|
40
|
+
if home_env.exists():
|
41
|
+
home_vars = load_env_file(home_env)
|
42
|
+
env_vars.update(home_vars)
|
43
|
+
click.echo(f"Loaded {len(home_vars)} variables from {home_env}", err=True)
|
44
|
+
|
45
|
+
# Load from ./.env
|
46
|
+
local_env = Path('.env')
|
47
|
+
if local_env.exists():
|
48
|
+
local_vars = load_env_file(local_env)
|
49
|
+
env_vars.update(local_vars)
|
50
|
+
click.echo(f"Loaded {len(local_vars)} variables from {local_env}", err=True)
|
51
|
+
|
52
|
+
# System environment variables override file-based ones
|
53
|
+
env_vars.update(os.environ)
|
54
|
+
|
55
|
+
return env_vars
|
56
|
+
|
57
|
+
|
58
|
+
def get_stripe_key(environment: str, env_vars: Optional[Dict[str, str]] = None) -> Optional[str]:
|
59
|
+
"""
|
60
|
+
Get Stripe API key for the specified environment.
|
61
|
+
|
62
|
+
Looks for keys in this order:
|
63
|
+
1. STAGING_STRIPE_SECRET_KEY or PROD_STRIPE_SECRET_KEY (based on environment)
|
64
|
+
2. STRIPE_SECRET_KEY (fallback)
|
65
|
+
|
66
|
+
Args:
|
67
|
+
environment: 'staging' or 'production'
|
68
|
+
env_vars: Optional pre-loaded environment variables
|
69
|
+
|
70
|
+
Returns:
|
71
|
+
Stripe API key or None if not found
|
72
|
+
"""
|
73
|
+
if env_vars is None:
|
74
|
+
env_vars = load_environment_config()
|
75
|
+
|
76
|
+
# Map environment names to prefixes
|
77
|
+
env_prefix = {
|
78
|
+
'staging': 'STAGING',
|
79
|
+
'production': 'PROD'
|
80
|
+
}.get(environment, environment.upper())
|
81
|
+
|
82
|
+
# Try environment-specific key first
|
83
|
+
env_key = f"{env_prefix}_STRIPE_SECRET_KEY"
|
84
|
+
if env_key in env_vars:
|
85
|
+
return env_vars[env_key]
|
86
|
+
|
87
|
+
# Fall back to generic key
|
88
|
+
if 'STRIPE_SECRET_KEY' in env_vars:
|
89
|
+
return env_vars['STRIPE_SECRET_KEY']
|
90
|
+
|
91
|
+
return None
|
92
|
+
|
93
|
+
|
94
|
+
def get_env_variable(key: str, environment: Optional[str] = None,
|
95
|
+
env_vars: Optional[Dict[str, str]] = None) -> Optional[str]:
|
96
|
+
"""
|
97
|
+
Get an environment variable, optionally with environment prefix.
|
98
|
+
|
99
|
+
Args:
|
100
|
+
key: Variable name (e.g., 'API_URL')
|
101
|
+
environment: Optional environment ('staging' or 'production')
|
102
|
+
env_vars: Optional pre-loaded environment variables
|
103
|
+
|
104
|
+
Returns:
|
105
|
+
Variable value or None if not found
|
106
|
+
"""
|
107
|
+
if env_vars is None:
|
108
|
+
env_vars = load_environment_config()
|
109
|
+
|
110
|
+
if environment:
|
111
|
+
env_prefix = {
|
112
|
+
'staging': 'STAGING',
|
113
|
+
'production': 'PROD'
|
114
|
+
}.get(environment, environment.upper())
|
115
|
+
|
116
|
+
# Try environment-specific key first
|
117
|
+
env_key = f"{env_prefix}_{key}"
|
118
|
+
if env_key in env_vars:
|
119
|
+
return env_vars[env_key]
|
120
|
+
|
121
|
+
# Fall back to non-prefixed key
|
122
|
+
return env_vars.get(key)
|
ai_lls_lib/core/__init__.py
CHANGED
@@ -1,3 +1,3 @@
|
|
1
|
-
"""
|
2
|
-
Core business logic modules
|
3
|
-
"""
|
1
|
+
"""
|
2
|
+
Core business logic modules
|
3
|
+
"""
|
ai_lls_lib/core/cache.py
CHANGED
@@ -1,106 +1,106 @@
|
|
1
|
-
"""
|
2
|
-
DynamoDB cache implementation for phone verifications
|
3
|
-
"""
|
4
|
-
import os
|
5
|
-
from datetime import datetime, timedelta, timezone
|
6
|
-
from typing import Optional
|
7
|
-
import boto3
|
8
|
-
from aws_lambda_powertools import Logger
|
9
|
-
from .models import PhoneVerification, CacheEntry
|
10
|
-
|
11
|
-
logger = Logger()
|
12
|
-
|
13
|
-
|
14
|
-
class DynamoDBCache:
|
15
|
-
"""Cache for phone verification results using DynamoDB with TTL"""
|
16
|
-
|
17
|
-
def __init__(self, table_name: str, ttl_days: int = 30):
|
18
|
-
self.table_name = table_name
|
19
|
-
self.ttl_days = ttl_days
|
20
|
-
self.dynamodb = boto3.resource("dynamodb")
|
21
|
-
self.table = self.dynamodb.Table(table_name)
|
22
|
-
|
23
|
-
def get(self, phone_number: str) -> Optional[PhoneVerification]:
|
24
|
-
"""Get cached verification result"""
|
25
|
-
try:
|
26
|
-
response = self.table.get_item(Key={"phone_number": phone_number})
|
27
|
-
|
28
|
-
if "Item" not in response:
|
29
|
-
logger.info(f"Cache miss for {phone_number[:6]}***")
|
30
|
-
return None
|
31
|
-
|
32
|
-
item = response["Item"]
|
33
|
-
logger.info(f"Cache hit for {phone_number[:6]}***")
|
34
|
-
|
35
|
-
return PhoneVerification(
|
36
|
-
phone_number=item["phone_number"],
|
37
|
-
line_type=item["line_type"],
|
38
|
-
dnc=item["dnc"],
|
39
|
-
cached=True,
|
40
|
-
verified_at=datetime.fromisoformat(item["verified_at"]),
|
41
|
-
source="cache"
|
42
|
-
)
|
43
|
-
|
44
|
-
except Exception as e:
|
45
|
-
logger.error(f"Cache get error: {str(e)}")
|
46
|
-
return None
|
47
|
-
|
48
|
-
def set(self, phone_number: str, verification: PhoneVerification) -> None:
|
49
|
-
"""Store verification result in cache"""
|
50
|
-
try:
|
51
|
-
ttl = int((datetime.now(timezone.utc) + timedelta(days=self.ttl_days)).timestamp())
|
52
|
-
|
53
|
-
self.table.put_item(
|
54
|
-
Item={
|
55
|
-
"phone_number": phone_number,
|
56
|
-
"line_type": verification.line_type,
|
57
|
-
"dnc": verification.dnc,
|
58
|
-
"verified_at": verification.verified_at.isoformat(),
|
59
|
-
"source": verification.source,
|
60
|
-
"ttl": ttl
|
61
|
-
}
|
62
|
-
)
|
63
|
-
|
64
|
-
logger.info(f"Cached result for {phone_number[:6]}***")
|
65
|
-
|
66
|
-
except Exception as e:
|
67
|
-
logger.error(f"Cache set error: {str(e)}")
|
68
|
-
# Don't fail the request if cache write fails
|
69
|
-
|
70
|
-
def batch_get(self, phone_numbers: list[str]) -> dict[str, Optional[PhoneVerification]]:
|
71
|
-
"""Get multiple cached results"""
|
72
|
-
results = {}
|
73
|
-
|
74
|
-
# DynamoDB batch get (max 100 items per request)
|
75
|
-
for i in range(0, len(phone_numbers), 100):
|
76
|
-
batch = phone_numbers[i:i+100]
|
77
|
-
|
78
|
-
try:
|
79
|
-
response = self.dynamodb.batch_get_item(
|
80
|
-
RequestItems={
|
81
|
-
self.table_name: {
|
82
|
-
"Keys": [{"phone_number": phone} for phone in batch]
|
83
|
-
}
|
84
|
-
}
|
85
|
-
)
|
86
|
-
|
87
|
-
for item in response.get("Responses", {}).get(self.table_name, []):
|
88
|
-
phone = item["phone_number"]
|
89
|
-
results[phone] = PhoneVerification(
|
90
|
-
phone_number=phone,
|
91
|
-
line_type=item["line_type"],
|
92
|
-
dnc=item["dnc"],
|
93
|
-
cached=True,
|
94
|
-
verified_at=datetime.fromisoformat(item["verified_at"]),
|
95
|
-
source="cache"
|
96
|
-
)
|
97
|
-
|
98
|
-
except Exception as e:
|
99
|
-
logger.error(f"Batch cache get error: {str(e)}")
|
100
|
-
|
101
|
-
# Fill in None for misses
|
102
|
-
for phone in phone_numbers:
|
103
|
-
if phone not in results:
|
104
|
-
results[phone] = None
|
105
|
-
|
106
|
-
return results
|
1
|
+
"""
|
2
|
+
DynamoDB cache implementation for phone verifications
|
3
|
+
"""
|
4
|
+
import os
|
5
|
+
from datetime import datetime, timedelta, timezone
|
6
|
+
from typing import Optional
|
7
|
+
import boto3
|
8
|
+
from aws_lambda_powertools import Logger
|
9
|
+
from .models import PhoneVerification, CacheEntry
|
10
|
+
|
11
|
+
logger = Logger()
|
12
|
+
|
13
|
+
|
14
|
+
class DynamoDBCache:
|
15
|
+
"""Cache for phone verification results using DynamoDB with TTL"""
|
16
|
+
|
17
|
+
def __init__(self, table_name: str, ttl_days: int = 30):
|
18
|
+
self.table_name = table_name
|
19
|
+
self.ttl_days = ttl_days
|
20
|
+
self.dynamodb = boto3.resource("dynamodb")
|
21
|
+
self.table = self.dynamodb.Table(table_name)
|
22
|
+
|
23
|
+
def get(self, phone_number: str) -> Optional[PhoneVerification]:
|
24
|
+
"""Get cached verification result"""
|
25
|
+
try:
|
26
|
+
response = self.table.get_item(Key={"phone_number": phone_number})
|
27
|
+
|
28
|
+
if "Item" not in response:
|
29
|
+
logger.info(f"Cache miss for {phone_number[:6]}***")
|
30
|
+
return None
|
31
|
+
|
32
|
+
item = response["Item"]
|
33
|
+
logger.info(f"Cache hit for {phone_number[:6]}***")
|
34
|
+
|
35
|
+
return PhoneVerification(
|
36
|
+
phone_number=item["phone_number"],
|
37
|
+
line_type=item["line_type"],
|
38
|
+
dnc=item["dnc"],
|
39
|
+
cached=True,
|
40
|
+
verified_at=datetime.fromisoformat(item["verified_at"]),
|
41
|
+
source="cache"
|
42
|
+
)
|
43
|
+
|
44
|
+
except Exception as e:
|
45
|
+
logger.error(f"Cache get error: {str(e)}")
|
46
|
+
return None
|
47
|
+
|
48
|
+
def set(self, phone_number: str, verification: PhoneVerification) -> None:
|
49
|
+
"""Store verification result in cache"""
|
50
|
+
try:
|
51
|
+
ttl = int((datetime.now(timezone.utc) + timedelta(days=self.ttl_days)).timestamp())
|
52
|
+
|
53
|
+
self.table.put_item(
|
54
|
+
Item={
|
55
|
+
"phone_number": phone_number,
|
56
|
+
"line_type": verification.line_type,
|
57
|
+
"dnc": verification.dnc,
|
58
|
+
"verified_at": verification.verified_at.isoformat(),
|
59
|
+
"source": verification.source,
|
60
|
+
"ttl": ttl
|
61
|
+
}
|
62
|
+
)
|
63
|
+
|
64
|
+
logger.info(f"Cached result for {phone_number[:6]}***")
|
65
|
+
|
66
|
+
except Exception as e:
|
67
|
+
logger.error(f"Cache set error: {str(e)}")
|
68
|
+
# Don't fail the request if cache write fails
|
69
|
+
|
70
|
+
def batch_get(self, phone_numbers: list[str]) -> dict[str, Optional[PhoneVerification]]:
|
71
|
+
"""Get multiple cached results"""
|
72
|
+
results = {}
|
73
|
+
|
74
|
+
# DynamoDB batch get (max 100 items per request)
|
75
|
+
for i in range(0, len(phone_numbers), 100):
|
76
|
+
batch = phone_numbers[i:i+100]
|
77
|
+
|
78
|
+
try:
|
79
|
+
response = self.dynamodb.batch_get_item(
|
80
|
+
RequestItems={
|
81
|
+
self.table_name: {
|
82
|
+
"Keys": [{"phone_number": phone} for phone in batch]
|
83
|
+
}
|
84
|
+
}
|
85
|
+
)
|
86
|
+
|
87
|
+
for item in response.get("Responses", {}).get(self.table_name, []):
|
88
|
+
phone = item["phone_number"]
|
89
|
+
results[phone] = PhoneVerification(
|
90
|
+
phone_number=phone,
|
91
|
+
line_type=item["line_type"],
|
92
|
+
dnc=item["dnc"],
|
93
|
+
cached=True,
|
94
|
+
verified_at=datetime.fromisoformat(item["verified_at"]),
|
95
|
+
source="cache"
|
96
|
+
)
|
97
|
+
|
98
|
+
except Exception as e:
|
99
|
+
logger.error(f"Batch cache get error: {str(e)}")
|
100
|
+
|
101
|
+
# Fill in None for misses
|
102
|
+
for phone in phone_numbers:
|
103
|
+
if phone not in results:
|
104
|
+
results[phone] = None
|
105
|
+
|
106
|
+
return results
|
ai_lls_lib/core/models.py
CHANGED
@@ -1,77 +1,77 @@
|
|
1
|
-
"""
|
2
|
-
Data models for phone verification
|
3
|
-
"""
|
4
|
-
from datetime import datetime
|
5
|
-
from enum import Enum
|
6
|
-
from typing import Optional
|
7
|
-
from pydantic import BaseModel, Field
|
8
|
-
|
9
|
-
|
10
|
-
class LineType(str, Enum):
|
11
|
-
"""Phone line type enumeration"""
|
12
|
-
MOBILE = "mobile"
|
13
|
-
LANDLINE = "landline"
|
14
|
-
VOIP = "voip"
|
15
|
-
UNKNOWN = "unknown"
|
16
|
-
|
17
|
-
|
18
|
-
class VerificationSource(str, Enum):
|
19
|
-
"""Source of verification data"""
|
20
|
-
API = "api"
|
21
|
-
CACHE = "cache"
|
22
|
-
BULK_IMPORT = "bulk_import"
|
23
|
-
|
24
|
-
|
25
|
-
class JobStatus(str, Enum):
|
26
|
-
"""Bulk job status enumeration"""
|
27
|
-
PENDING = "pending"
|
28
|
-
PROCESSING = "processing"
|
29
|
-
COMPLETED = "completed"
|
30
|
-
FAILED = "failed"
|
31
|
-
|
32
|
-
|
33
|
-
class PhoneVerification(BaseModel):
|
34
|
-
"""Result of phone number verification"""
|
35
|
-
phone_number: str = Field(..., description="E.164 formatted phone number")
|
36
|
-
line_type: LineType = Field(
|
37
|
-
..., description="Type of phone line"
|
38
|
-
)
|
39
|
-
dnc: bool = Field(..., description="Whether number is on DNC list")
|
40
|
-
cached: bool = Field(..., description="Whether result came from cache")
|
41
|
-
verified_at: datetime = Field(..., description="When verification occurred")
|
42
|
-
source: VerificationSource = Field(
|
43
|
-
..., description="Source of verification data"
|
44
|
-
)
|
45
|
-
|
46
|
-
class Config:
|
47
|
-
json_encoders = {
|
48
|
-
datetime: lambda v: v.isoformat()
|
49
|
-
}
|
50
|
-
|
51
|
-
|
52
|
-
class BulkJob(BaseModel):
|
53
|
-
"""Bulk processing job metadata"""
|
54
|
-
job_id: str = Field(..., description="Unique job identifier")
|
55
|
-
status: JobStatus = Field(
|
56
|
-
..., description="Current job status"
|
57
|
-
)
|
58
|
-
|
59
|
-
|
60
|
-
class BulkJobStatus(BulkJob):
|
61
|
-
"""Extended bulk job status with progress info"""
|
62
|
-
total_rows: Optional[int] = Field(None, description="Total rows to process")
|
63
|
-
processed_rows: Optional[int] = Field(None, description="Rows processed so far")
|
64
|
-
result_url: Optional[str] = Field(None, description="S3 URL of results")
|
65
|
-
created_at: datetime = Field(..., description="Job creation time")
|
66
|
-
completed_at: Optional[datetime] = Field(None, description="Job completion time")
|
67
|
-
error: Optional[str] = Field(None, description="Error message if failed")
|
68
|
-
|
69
|
-
|
70
|
-
class CacheEntry(BaseModel):
|
71
|
-
"""DynamoDB cache entry"""
|
72
|
-
phone_number: str
|
73
|
-
line_type: str # Stored as string in DynamoDB
|
74
|
-
dnc: bool
|
75
|
-
verified_at: str # ISO format string
|
76
|
-
source: str # Stored as string in DynamoDB
|
77
|
-
ttl: int # Unix timestamp for DynamoDB TTL
|
1
|
+
"""
|
2
|
+
Data models for phone verification
|
3
|
+
"""
|
4
|
+
from datetime import datetime
|
5
|
+
from enum import Enum
|
6
|
+
from typing import Optional
|
7
|
+
from pydantic import BaseModel, Field
|
8
|
+
|
9
|
+
|
10
|
+
class LineType(str, Enum):
|
11
|
+
"""Phone line type enumeration"""
|
12
|
+
MOBILE = "mobile"
|
13
|
+
LANDLINE = "landline"
|
14
|
+
VOIP = "voip"
|
15
|
+
UNKNOWN = "unknown"
|
16
|
+
|
17
|
+
|
18
|
+
class VerificationSource(str, Enum):
|
19
|
+
"""Source of verification data"""
|
20
|
+
API = "api"
|
21
|
+
CACHE = "cache"
|
22
|
+
BULK_IMPORT = "bulk_import"
|
23
|
+
|
24
|
+
|
25
|
+
class JobStatus(str, Enum):
|
26
|
+
"""Bulk job status enumeration"""
|
27
|
+
PENDING = "pending"
|
28
|
+
PROCESSING = "processing"
|
29
|
+
COMPLETED = "completed"
|
30
|
+
FAILED = "failed"
|
31
|
+
|
32
|
+
|
33
|
+
class PhoneVerification(BaseModel):
|
34
|
+
"""Result of phone number verification"""
|
35
|
+
phone_number: str = Field(..., description="E.164 formatted phone number")
|
36
|
+
line_type: LineType = Field(
|
37
|
+
..., description="Type of phone line"
|
38
|
+
)
|
39
|
+
dnc: bool = Field(..., description="Whether number is on DNC list")
|
40
|
+
cached: bool = Field(..., description="Whether result came from cache")
|
41
|
+
verified_at: datetime = Field(..., description="When verification occurred")
|
42
|
+
source: VerificationSource = Field(
|
43
|
+
..., description="Source of verification data"
|
44
|
+
)
|
45
|
+
|
46
|
+
class Config:
|
47
|
+
json_encoders = {
|
48
|
+
datetime: lambda v: v.isoformat()
|
49
|
+
}
|
50
|
+
|
51
|
+
|
52
|
+
class BulkJob(BaseModel):
|
53
|
+
"""Bulk processing job metadata"""
|
54
|
+
job_id: str = Field(..., description="Unique job identifier")
|
55
|
+
status: JobStatus = Field(
|
56
|
+
..., description="Current job status"
|
57
|
+
)
|
58
|
+
|
59
|
+
|
60
|
+
class BulkJobStatus(BulkJob):
|
61
|
+
"""Extended bulk job status with progress info"""
|
62
|
+
total_rows: Optional[int] = Field(None, description="Total rows to process")
|
63
|
+
processed_rows: Optional[int] = Field(None, description="Rows processed so far")
|
64
|
+
result_url: Optional[str] = Field(None, description="S3 URL of results")
|
65
|
+
created_at: datetime = Field(..., description="Job creation time")
|
66
|
+
completed_at: Optional[datetime] = Field(None, description="Job completion time")
|
67
|
+
error: Optional[str] = Field(None, description="Error message if failed")
|
68
|
+
|
69
|
+
|
70
|
+
class CacheEntry(BaseModel):
|
71
|
+
"""DynamoDB cache entry"""
|
72
|
+
phone_number: str
|
73
|
+
line_type: str # Stored as string in DynamoDB
|
74
|
+
dnc: bool
|
75
|
+
verified_at: str # ISO format string
|
76
|
+
source: str # Stored as string in DynamoDB
|
77
|
+
ttl: int # Unix timestamp for DynamoDB TTL
|