AD-SearchAPI 1.0.2__tar.gz → 1.0.3__tar.gz
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.
- {ad_searchapi-1.0.2 → ad_searchapi-1.0.3}/AD_SearchAPI.egg-info/PKG-INFO +2 -2
- {ad_searchapi-1.0.2 → ad_searchapi-1.0.3}/PKG-INFO +2 -2
- {ad_searchapi-1.0.2 → ad_searchapi-1.0.3}/README.md +1 -1
- {ad_searchapi-1.0.2 → ad_searchapi-1.0.3}/search_api/client.py +95 -4
- {ad_searchapi-1.0.2 → ad_searchapi-1.0.3}/search_api/models.py +1 -1
- {ad_searchapi-1.0.2 → ad_searchapi-1.0.3}/setup.py +1 -1
- ad_searchapi-1.0.3/tests/test_client.py +340 -0
- ad_searchapi-1.0.2/tests/test_client.py +0 -178
- {ad_searchapi-1.0.2 → ad_searchapi-1.0.3}/AD_SearchAPI.egg-info/SOURCES.txt +0 -0
- {ad_searchapi-1.0.2 → ad_searchapi-1.0.3}/AD_SearchAPI.egg-info/dependency_links.txt +0 -0
- {ad_searchapi-1.0.2 → ad_searchapi-1.0.3}/AD_SearchAPI.egg-info/requires.txt +0 -0
- {ad_searchapi-1.0.2 → ad_searchapi-1.0.3}/AD_SearchAPI.egg-info/top_level.txt +0 -0
- {ad_searchapi-1.0.2 → ad_searchapi-1.0.3}/LICENSE +0 -0
- {ad_searchapi-1.0.2 → ad_searchapi-1.0.3}/search_api/__init__.py +0 -0
- {ad_searchapi-1.0.2 → ad_searchapi-1.0.3}/search_api/exceptions.py +0 -0
- {ad_searchapi-1.0.2 → ad_searchapi-1.0.3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: AD-SearchAPI
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.3
|
|
4
4
|
Summary: A Python client library for the Search API
|
|
5
5
|
Home-page: https://github.com/AntiChrist-Coder/search_api_library
|
|
6
6
|
Author: Search API Team
|
|
@@ -40,7 +40,7 @@ Acquire your API key through @ADSearchEngine_bot on Telegram.
|
|
|
40
40
|
|
|
41
41
|
## Installation
|
|
42
42
|
|
|
43
|
-
pip install
|
|
43
|
+
pip install AD-SearchAPI
|
|
44
44
|
|
|
45
45
|
## Quick Start
|
|
46
46
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: AD-SearchAPI
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.3
|
|
4
4
|
Summary: A Python client library for the Search API
|
|
5
5
|
Home-page: https://github.com/AntiChrist-Coder/search_api_library
|
|
6
6
|
Author: Search API Team
|
|
@@ -40,7 +40,7 @@ Acquire your API key through @ADSearchEngine_bot on Telegram.
|
|
|
40
40
|
|
|
41
41
|
## Installation
|
|
42
42
|
|
|
43
|
-
pip install
|
|
43
|
+
pip install AD-SearchAPI
|
|
44
44
|
|
|
45
45
|
## Quick Start
|
|
46
46
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import re
|
|
3
|
+
import gzip
|
|
4
|
+
import io
|
|
3
5
|
from datetime import date
|
|
4
6
|
from decimal import Decimal
|
|
5
7
|
from typing import Dict, List, Optional, Union
|
|
@@ -15,6 +17,12 @@ import logging
|
|
|
15
17
|
from requests import Session, Response
|
|
16
18
|
from requests.exceptions import RequestException, Timeout, ConnectionError, HTTPError
|
|
17
19
|
|
|
20
|
+
try:
|
|
21
|
+
import brotli
|
|
22
|
+
BROTLI_AVAILABLE = True
|
|
23
|
+
except ImportError:
|
|
24
|
+
BROTLI_AVAILABLE = False
|
|
25
|
+
|
|
18
26
|
from .exceptions import (
|
|
19
27
|
AuthenticationError,
|
|
20
28
|
InsufficientBalanceError,
|
|
@@ -198,6 +206,12 @@ class SearchAPI:
|
|
|
198
206
|
def _make_request(
|
|
199
207
|
self, params: Optional[Dict] = None
|
|
200
208
|
) -> Dict:
|
|
209
|
+
"""
|
|
210
|
+
Make a GET request to the Search API.
|
|
211
|
+
|
|
212
|
+
Handles gzipped and Brotli-compressed responses explicitly since some APIs
|
|
213
|
+
don't properly set headers for automatic decompression.
|
|
214
|
+
"""
|
|
201
215
|
if params is None:
|
|
202
216
|
params = {}
|
|
203
217
|
params['api_key'] = self.config.api_key
|
|
@@ -217,18 +231,33 @@ class SearchAPI:
|
|
|
217
231
|
)
|
|
218
232
|
response.raise_for_status()
|
|
219
233
|
|
|
220
|
-
if
|
|
234
|
+
if self.config.debug_mode:
|
|
235
|
+
content_encoding = response.headers.get('Content-Encoding', 'none')
|
|
236
|
+
content_length = response.headers.get('Content-Length', 'unknown')
|
|
237
|
+
logger.debug(f"Response headers - Content-Encoding: {content_encoding}, Content-Length: {content_length}")
|
|
238
|
+
logger.debug(f"Response encoding: {response.encoding}")
|
|
239
|
+
|
|
240
|
+
content_encoding = response.headers.get('Content-Encoding', '')
|
|
241
|
+
response_content = response.content
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
response_text = self._try_decompress_response(response_content, content_encoding)
|
|
245
|
+
except Exception as e:
|
|
246
|
+
logger.error(f"Failed to decompress/decode response: {str(e)}")
|
|
247
|
+
raise SearchAPIError(f"Failed to decompress/decode response: {str(e)}")
|
|
248
|
+
|
|
249
|
+
if not response_text.strip():
|
|
221
250
|
if self.config.debug_mode:
|
|
222
251
|
logger.warning(f"Empty response received from {self.BASE_URL}")
|
|
223
252
|
return {}
|
|
224
253
|
|
|
225
254
|
try:
|
|
226
|
-
result =
|
|
255
|
+
result = json.loads(response_text)
|
|
227
256
|
if self.config.debug_mode:
|
|
228
257
|
logger.debug(f"Response received: {result}")
|
|
229
258
|
return result
|
|
230
259
|
except json.JSONDecodeError as e:
|
|
231
|
-
logger.error(f"Failed to parse JSON response: {
|
|
260
|
+
logger.error(f"Failed to parse JSON response: {response_text}")
|
|
232
261
|
raise SearchAPIError(f"Invalid JSON response: {str(e)}")
|
|
233
262
|
|
|
234
263
|
except Timeout as e:
|
|
@@ -458,4 +487,66 @@ class SearchAPI:
|
|
|
458
487
|
domain=domain,
|
|
459
488
|
results=email_results,
|
|
460
489
|
total_results=len(email_results)
|
|
461
|
-
)
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
def _try_decompress_response(self, response_content: bytes, content_encoding: str) -> str:
|
|
493
|
+
if self.config.debug_mode:
|
|
494
|
+
logger.debug(f"Attempting to decompress content with encoding: {content_encoding}")
|
|
495
|
+
logger.debug(f"Content length: {len(response_content)}")
|
|
496
|
+
if len(response_content) >= 4:
|
|
497
|
+
logger.debug(f"First 4 bytes: {response_content[:4].hex()}")
|
|
498
|
+
|
|
499
|
+
if 'br' in content_encoding.lower() and BROTLI_AVAILABLE:
|
|
500
|
+
try:
|
|
501
|
+
decompressed = brotli.decompress(response_content)
|
|
502
|
+
result = decompressed.decode('utf-8')
|
|
503
|
+
if self.config.debug_mode:
|
|
504
|
+
logger.debug("Successfully decompressed with Brotli")
|
|
505
|
+
return result
|
|
506
|
+
except Exception as e:
|
|
507
|
+
if self.config.debug_mode:
|
|
508
|
+
logger.debug(f"Brotli decompression failed: {str(e)}")
|
|
509
|
+
|
|
510
|
+
if 'gzip' in content_encoding.lower():
|
|
511
|
+
try:
|
|
512
|
+
decompressed = gzip.decompress(response_content)
|
|
513
|
+
result = decompressed.decode('utf-8')
|
|
514
|
+
if self.config.debug_mode:
|
|
515
|
+
logger.debug("Successfully decompressed with gzip")
|
|
516
|
+
return result
|
|
517
|
+
except Exception as e:
|
|
518
|
+
if self.config.debug_mode:
|
|
519
|
+
logger.debug(f"Gzip decompression failed: {str(e)}")
|
|
520
|
+
|
|
521
|
+
if len(response_content) >= 2 and response_content[:2] == b'\x1f\x8b':
|
|
522
|
+
try:
|
|
523
|
+
decompressed = gzip.decompress(response_content)
|
|
524
|
+
result = decompressed.decode('utf-8')
|
|
525
|
+
if self.config.debug_mode:
|
|
526
|
+
logger.debug("Successfully decompressed gzip by magic bytes")
|
|
527
|
+
return result
|
|
528
|
+
except Exception as e:
|
|
529
|
+
if self.config.debug_mode:
|
|
530
|
+
logger.debug(f"Gzip magic bytes decompression failed: {str(e)}")
|
|
531
|
+
|
|
532
|
+
if len(response_content) >= 2 and response_content[:2] == b'\xce\xb2' and BROTLI_AVAILABLE:
|
|
533
|
+
try:
|
|
534
|
+
decompressed = brotli.decompress(response_content)
|
|
535
|
+
result = decompressed.decode('utf-8')
|
|
536
|
+
if self.config.debug_mode:
|
|
537
|
+
logger.debug("Successfully decompressed Brotli by magic bytes")
|
|
538
|
+
return result
|
|
539
|
+
except Exception as e:
|
|
540
|
+
if self.config.debug_mode:
|
|
541
|
+
logger.debug(f"Brotli magic bytes decompression failed: {str(e)}")
|
|
542
|
+
|
|
543
|
+
try:
|
|
544
|
+
result = response_content.decode('utf-8')
|
|
545
|
+
if self.config.debug_mode:
|
|
546
|
+
logger.debug("Successfully decoded as plain text")
|
|
547
|
+
return result
|
|
548
|
+
except Exception as e:
|
|
549
|
+
if self.config.debug_mode:
|
|
550
|
+
logger.debug(f"Plain text decoding failed: {str(e)}")
|
|
551
|
+
|
|
552
|
+
raise SearchAPIError(f"Failed to decompress or decode response content. Content-Encoding: {content_encoding}, Content length: {len(response_content)}")
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from unittest.mock import Mock, patch
|
|
3
|
+
from datetime import date
|
|
4
|
+
from decimal import Decimal
|
|
5
|
+
|
|
6
|
+
from search_api import SearchAPI, SearchAPIConfig
|
|
7
|
+
from search_api.exceptions import (
|
|
8
|
+
AuthenticationError,
|
|
9
|
+
ValidationError,
|
|
10
|
+
SearchAPIError,
|
|
11
|
+
)
|
|
12
|
+
from search_api.models import Address, PhoneNumber
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture
|
|
16
|
+
def client():
|
|
17
|
+
return SearchAPI(api_key="test_api_key")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.fixture
|
|
21
|
+
def mock_response():
|
|
22
|
+
return {
|
|
23
|
+
"name": "John Doe",
|
|
24
|
+
"dob": "1990-01-01",
|
|
25
|
+
"addresses": [
|
|
26
|
+
"123 Main St, New York, NY 10001",
|
|
27
|
+
{
|
|
28
|
+
"street": "456 Park Ave",
|
|
29
|
+
"city": "New York",
|
|
30
|
+
"state": "NY",
|
|
31
|
+
"postal_code": "10022",
|
|
32
|
+
"zestimate": 1500000,
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
"numbers": ["+12125551234", "+12125556789"],
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_init_with_api_key():
|
|
40
|
+
client = SearchAPI(api_key="test_api_key")
|
|
41
|
+
assert client.config.api_key == "test_api_key"
|
|
42
|
+
assert client.config.base_url == "https://search-api.dev"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_init_with_config():
|
|
46
|
+
config = SearchAPIConfig(
|
|
47
|
+
api_key="test_api_key",
|
|
48
|
+
max_retries=5,
|
|
49
|
+
timeout=60,
|
|
50
|
+
base_url="https://custom-api.dev",
|
|
51
|
+
)
|
|
52
|
+
client = SearchAPI(config=config)
|
|
53
|
+
assert client.config == config
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_init_without_api_key_or_config():
|
|
57
|
+
with pytest.raises(ValueError):
|
|
58
|
+
SearchAPI()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@patch("requests.Session.request")
|
|
62
|
+
def test_search_email_success(mock_request, client, mock_response):
|
|
63
|
+
mock_request.return_value.json.return_value = mock_response
|
|
64
|
+
mock_request.return_value.status_code = 200
|
|
65
|
+
|
|
66
|
+
result = client.search_email("test@example.com")
|
|
67
|
+
|
|
68
|
+
assert result.name == "John Doe"
|
|
69
|
+
assert result.dob == date(1990, 1, 1)
|
|
70
|
+
assert len(result.addresses) == 2
|
|
71
|
+
assert len(result.phone_numbers) == 2
|
|
72
|
+
assert isinstance(result.addresses[0], Address)
|
|
73
|
+
assert isinstance(result.phone_numbers[0], PhoneNumber)
|
|
74
|
+
assert result.addresses[1].zestimate == Decimal("1500000")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@patch("requests.Session.request")
|
|
78
|
+
def test_search_email_invalid_format(client):
|
|
79
|
+
with pytest.raises(ValidationError):
|
|
80
|
+
client.search_email("invalid-email")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@patch("requests.Session.request")
|
|
84
|
+
def test_search_email_api_error(client):
|
|
85
|
+
mock_response = Mock()
|
|
86
|
+
mock_response.status_code = 401
|
|
87
|
+
mock_response.text = "Invalid API key"
|
|
88
|
+
mock_response.json.return_value = {"error": "Invalid API key"}
|
|
89
|
+
|
|
90
|
+
with patch("requests.Session.request", return_value=mock_response):
|
|
91
|
+
with pytest.raises(AuthenticationError):
|
|
92
|
+
client.search_email("test@example.com")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@patch("requests.Session.request")
|
|
96
|
+
def test_search_phone_success(mock_request, client, mock_response):
|
|
97
|
+
mock_request.return_value.json.return_value = mock_response
|
|
98
|
+
mock_request.return_value.status_code = 200
|
|
99
|
+
|
|
100
|
+
result = client.search_phone("+12125551234")
|
|
101
|
+
|
|
102
|
+
assert result.name == "John Doe"
|
|
103
|
+
assert result.dob == date(1990, 1, 1)
|
|
104
|
+
assert len(result.addresses) == 2
|
|
105
|
+
assert len(result.phone_numbers) == 2
|
|
106
|
+
assert isinstance(result.addresses[0], Address)
|
|
107
|
+
assert isinstance(result.phone_numbers[0], PhoneNumber)
|
|
108
|
+
assert result.addresses[1].zestimate == Decimal("1500000")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@patch("requests.Session.request")
|
|
112
|
+
def test_search_phone_invalid_format(client):
|
|
113
|
+
with pytest.raises(ValidationError):
|
|
114
|
+
client.search_phone("invalid-phone")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@patch("requests.Session.request")
|
|
118
|
+
def test_search_domain_success(mock_request, client):
|
|
119
|
+
mock_response = {
|
|
120
|
+
"results": [
|
|
121
|
+
{
|
|
122
|
+
"email": "test1@example.com",
|
|
123
|
+
"name": "John Doe",
|
|
124
|
+
"addresses": ["123 Main St, New York, NY 10001"],
|
|
125
|
+
"phone_numbers": ["+12125551234"],
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"email": "test2@example.com",
|
|
129
|
+
"name": "Jane Smith",
|
|
130
|
+
"addresses": ["456 Park Ave, New York, NY 10022"],
|
|
131
|
+
"phone_numbers": ["+12125556789"],
|
|
132
|
+
},
|
|
133
|
+
]
|
|
134
|
+
}
|
|
135
|
+
mock_request.return_value.json.return_value = mock_response
|
|
136
|
+
mock_request.return_value.status_code = 200
|
|
137
|
+
|
|
138
|
+
result = client.search_domain("example.com")
|
|
139
|
+
|
|
140
|
+
assert result.domain == "example.com"
|
|
141
|
+
assert result.total_results == 2
|
|
142
|
+
assert len(result.results) == 2
|
|
143
|
+
assert result.results[0].email == "test1@example.com"
|
|
144
|
+
assert result.results[1].email == "test2@example.com"
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@patch("requests.Session.request")
|
|
148
|
+
def test_search_domain_major_domain(client):
|
|
149
|
+
with pytest.raises(ValidationError):
|
|
150
|
+
client.search_domain("gmail.com")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@patch("requests.Session.request")
|
|
154
|
+
def test_search_domain_invalid_format(client):
|
|
155
|
+
with pytest.raises(ValidationError):
|
|
156
|
+
client.search_domain("invalid-domain")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_format_address(client):
|
|
160
|
+
address = "123 main st, new york, ny 10001"
|
|
161
|
+
formatted = client._format_address(address)
|
|
162
|
+
assert formatted == "123 Main Street, New York, NY 10001"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_parse_phone_number(client):
|
|
166
|
+
phone = "+12125551234"
|
|
167
|
+
result = client._parse_phone_number(phone)
|
|
168
|
+
assert isinstance(result, PhoneNumber)
|
|
169
|
+
assert result.number == "+12125551234"
|
|
170
|
+
assert result.is_valid is True
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_parse_phone_number_invalid(client):
|
|
174
|
+
phone = "invalid-phone"
|
|
175
|
+
result = client._parse_phone_number(phone)
|
|
176
|
+
assert isinstance(result, PhoneNumber)
|
|
177
|
+
assert result.number == "invalid-phone"
|
|
178
|
+
assert result.is_valid is False
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@patch("requests.Session.request")
|
|
182
|
+
def test_compression_handling(mock_request, client):
|
|
183
|
+
"""Test that the client properly handles gzipped/compressed responses."""
|
|
184
|
+
# Mock response with gzip encoding
|
|
185
|
+
mock_response = Mock()
|
|
186
|
+
mock_response.status_code = 200
|
|
187
|
+
mock_response.headers = {
|
|
188
|
+
'Content-Encoding': 'gzip',
|
|
189
|
+
'Content-Length': '1234'
|
|
190
|
+
}
|
|
191
|
+
mock_response.text = '{"name": "John Doe", "email": "test@example.com"}'
|
|
192
|
+
mock_response.json.return_value = {"name": "John Doe", "email": "test@example.com"}
|
|
193
|
+
mock_response.raise_for_status.return_value = None
|
|
194
|
+
|
|
195
|
+
mock_request.return_value = mock_response
|
|
196
|
+
|
|
197
|
+
# Test that the request is made with proper headers
|
|
198
|
+
result = client.search_email("test@example.com")
|
|
199
|
+
|
|
200
|
+
# Verify that Accept-Encoding header is set in the session
|
|
201
|
+
assert "Accept-Encoding" in client.session.headers
|
|
202
|
+
assert "gzip" in client.session.headers["Accept-Encoding"]
|
|
203
|
+
|
|
204
|
+
# Verify that the request was made
|
|
205
|
+
mock_request.assert_called_once()
|
|
206
|
+
|
|
207
|
+
# Verify that the response was properly handled
|
|
208
|
+
assert result.name == "John Doe"
|
|
209
|
+
assert result.email == "test@example.com"
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@patch("requests.Session.request")
|
|
213
|
+
def test_gzip_decompression(mock_request, client):
|
|
214
|
+
"""Test that gzipped responses are properly decompressed."""
|
|
215
|
+
import gzip
|
|
216
|
+
import json
|
|
217
|
+
|
|
218
|
+
# Create a gzipped JSON response
|
|
219
|
+
original_data = {"name": "Jane Smith", "email": "jane@example.com"}
|
|
220
|
+
json_str = json.dumps(original_data)
|
|
221
|
+
gzipped_content = gzip.compress(json_str.encode('utf-8'))
|
|
222
|
+
|
|
223
|
+
# Mock response with gzipped content
|
|
224
|
+
mock_response = Mock()
|
|
225
|
+
mock_response.status_code = 200
|
|
226
|
+
mock_response.headers = {
|
|
227
|
+
'Content-Encoding': 'gzip',
|
|
228
|
+
'Content-Length': str(len(gzipped_content))
|
|
229
|
+
}
|
|
230
|
+
mock_response.content = gzipped_content
|
|
231
|
+
mock_response.text = json_str # This should be the decompressed text
|
|
232
|
+
mock_response.raise_for_status.return_value = None
|
|
233
|
+
|
|
234
|
+
mock_request.return_value = mock_response
|
|
235
|
+
|
|
236
|
+
# Test that gzipped response is properly handled
|
|
237
|
+
result = client.search_email("jane@example.com")
|
|
238
|
+
|
|
239
|
+
# Verify that the response was properly decompressed and parsed
|
|
240
|
+
assert result.name == "Jane Smith"
|
|
241
|
+
assert result.email == "jane@example.com"
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@patch("requests.Session.request")
|
|
245
|
+
def test_compression_debug_logging(mock_request, client):
|
|
246
|
+
"""Test that compression information is logged in debug mode."""
|
|
247
|
+
# Create client with debug mode enabled
|
|
248
|
+
debug_config = SearchAPIConfig(api_key="test_api_key", debug_mode=True)
|
|
249
|
+
debug_client = SearchAPI(config=debug_config)
|
|
250
|
+
|
|
251
|
+
# Mock response with compression headers
|
|
252
|
+
mock_response = Mock()
|
|
253
|
+
mock_response.status_code = 200
|
|
254
|
+
mock_response.headers = {
|
|
255
|
+
'Content-Encoding': 'gzip',
|
|
256
|
+
'Content-Length': '567'
|
|
257
|
+
}
|
|
258
|
+
mock_response.text = '{"name": "Jane Smith"}'
|
|
259
|
+
mock_response.json.return_value = {"name": "Jane Smith"}
|
|
260
|
+
mock_response.raise_for_status.return_value = None
|
|
261
|
+
mock_response.encoding = 'utf-8'
|
|
262
|
+
|
|
263
|
+
mock_request.return_value = mock_response
|
|
264
|
+
|
|
265
|
+
# Test that debug logging includes compression info
|
|
266
|
+
with patch('logging.Logger.debug') as mock_debug:
|
|
267
|
+
debug_client.search_email("test@example.com")
|
|
268
|
+
|
|
269
|
+
# Verify that compression headers were logged
|
|
270
|
+
debug_calls = [call[0][0] for call in mock_debug.call_args_list]
|
|
271
|
+
compression_logged = any('Content-Encoding' in str(call) for call in debug_calls)
|
|
272
|
+
assert compression_logged, "Compression headers should be logged in debug mode"
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@patch("requests.Session.request")
|
|
276
|
+
def test_gzip_magic_bytes_detection(mock_request, client):
|
|
277
|
+
"""Test that gzipped responses are detected by magic bytes even without Content-Encoding header."""
|
|
278
|
+
import gzip
|
|
279
|
+
import json
|
|
280
|
+
|
|
281
|
+
# Create a gzipped JSON response
|
|
282
|
+
original_data = {"name": "Bob Johnson", "email": "bob@example.com"}
|
|
283
|
+
json_str = json.dumps(original_data)
|
|
284
|
+
gzipped_content = gzip.compress(json_str.encode('utf-8'))
|
|
285
|
+
|
|
286
|
+
# Mock response with gzipped content but no Content-Encoding header
|
|
287
|
+
mock_response = Mock()
|
|
288
|
+
mock_response.status_code = 200
|
|
289
|
+
mock_response.headers = {
|
|
290
|
+
'Content-Length': str(len(gzipped_content))
|
|
291
|
+
# No Content-Encoding header
|
|
292
|
+
}
|
|
293
|
+
mock_response.content = gzipped_content
|
|
294
|
+
mock_response.text = json_str
|
|
295
|
+
mock_response.raise_for_status.return_value = None
|
|
296
|
+
|
|
297
|
+
mock_request.return_value = mock_response
|
|
298
|
+
|
|
299
|
+
# Test that gzipped response is detected by magic bytes and properly handled
|
|
300
|
+
result = client.search_email("bob@example.com")
|
|
301
|
+
|
|
302
|
+
# Verify that the response was properly decompressed and parsed
|
|
303
|
+
assert result.name == "Bob Johnson"
|
|
304
|
+
assert result.email == "bob@example.com"
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
@patch("requests.Session.request")
|
|
308
|
+
def test_brotli_decompression(mock_request, client):
|
|
309
|
+
"""Test that Brotli-compressed responses are properly decompressed."""
|
|
310
|
+
try:
|
|
311
|
+
import brotli
|
|
312
|
+
except ImportError:
|
|
313
|
+
pytest.skip("brotli library not available")
|
|
314
|
+
|
|
315
|
+
import json
|
|
316
|
+
|
|
317
|
+
# Create a Brotli-compressed JSON response
|
|
318
|
+
original_data = {"name": "Alice Brown", "email": "alice@example.com"}
|
|
319
|
+
json_str = json.dumps(original_data)
|
|
320
|
+
brotli_content = brotli.compress(json_str.encode('utf-8'))
|
|
321
|
+
|
|
322
|
+
# Mock response with Brotli-compressed content
|
|
323
|
+
mock_response = Mock()
|
|
324
|
+
mock_response.status_code = 200
|
|
325
|
+
mock_response.headers = {
|
|
326
|
+
'Content-Encoding': 'br',
|
|
327
|
+
'Content-Length': str(len(brotli_content))
|
|
328
|
+
}
|
|
329
|
+
mock_response.content = brotli_content
|
|
330
|
+
mock_response.text = json_str
|
|
331
|
+
mock_response.raise_for_status.return_value = None
|
|
332
|
+
|
|
333
|
+
mock_request.return_value = mock_response
|
|
334
|
+
|
|
335
|
+
# Test that Brotli-compressed response is properly handled
|
|
336
|
+
result = client.search_email("alice@example.com")
|
|
337
|
+
|
|
338
|
+
# Verify that the response was properly decompressed and parsed
|
|
339
|
+
assert result.name == "Alice Brown"
|
|
340
|
+
assert result.email == "alice@example.com"
|
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
from unittest.mock import Mock, patch
|
|
3
|
-
from datetime import date
|
|
4
|
-
from decimal import Decimal
|
|
5
|
-
|
|
6
|
-
from search_api import SearchAPI, SearchAPIConfig
|
|
7
|
-
from search_api.exceptions import (
|
|
8
|
-
AuthenticationError,
|
|
9
|
-
ValidationError,
|
|
10
|
-
SearchAPIError,
|
|
11
|
-
)
|
|
12
|
-
from search_api.models import Address, PhoneNumber
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
@pytest.fixture
|
|
16
|
-
def client():
|
|
17
|
-
return SearchAPI(api_key="test_api_key")
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
@pytest.fixture
|
|
21
|
-
def mock_response():
|
|
22
|
-
return {
|
|
23
|
-
"name": "John Doe",
|
|
24
|
-
"dob": "1990-01-01",
|
|
25
|
-
"addresses": [
|
|
26
|
-
"123 Main St, New York, NY 10001",
|
|
27
|
-
{
|
|
28
|
-
"street": "456 Park Ave",
|
|
29
|
-
"city": "New York",
|
|
30
|
-
"state": "NY",
|
|
31
|
-
"postal_code": "10022",
|
|
32
|
-
"zestimate": 1500000,
|
|
33
|
-
},
|
|
34
|
-
],
|
|
35
|
-
"numbers": ["+12125551234", "+12125556789"],
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def test_init_with_api_key():
|
|
40
|
-
client = SearchAPI(api_key="test_api_key")
|
|
41
|
-
assert client.config.api_key == "test_api_key"
|
|
42
|
-
assert client.config.base_url == "https://search-api.dev"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def test_init_with_config():
|
|
46
|
-
config = SearchAPIConfig(
|
|
47
|
-
api_key="test_api_key",
|
|
48
|
-
max_retries=5,
|
|
49
|
-
timeout=60,
|
|
50
|
-
base_url="https://custom-api.dev",
|
|
51
|
-
)
|
|
52
|
-
client = SearchAPI(config=config)
|
|
53
|
-
assert client.config == config
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def test_init_without_api_key_or_config():
|
|
57
|
-
with pytest.raises(ValueError):
|
|
58
|
-
SearchAPI()
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
@patch("requests.Session.request")
|
|
62
|
-
def test_search_email_success(mock_request, client, mock_response):
|
|
63
|
-
mock_request.return_value.json.return_value = mock_response
|
|
64
|
-
mock_request.return_value.status_code = 200
|
|
65
|
-
|
|
66
|
-
result = client.search_email("test@example.com")
|
|
67
|
-
|
|
68
|
-
assert result.name == "John Doe"
|
|
69
|
-
assert result.dob == date(1990, 1, 1)
|
|
70
|
-
assert len(result.addresses) == 2
|
|
71
|
-
assert len(result.phone_numbers) == 2
|
|
72
|
-
assert isinstance(result.addresses[0], Address)
|
|
73
|
-
assert isinstance(result.phone_numbers[0], PhoneNumber)
|
|
74
|
-
assert result.addresses[1].zestimate == Decimal("1500000")
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
@patch("requests.Session.request")
|
|
78
|
-
def test_search_email_invalid_format(client):
|
|
79
|
-
with pytest.raises(ValidationError):
|
|
80
|
-
client.search_email("invalid-email")
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
@patch("requests.Session.request")
|
|
84
|
-
def test_search_email_api_error(client):
|
|
85
|
-
mock_response = Mock()
|
|
86
|
-
mock_response.status_code = 401
|
|
87
|
-
mock_response.text = "Invalid API key"
|
|
88
|
-
mock_response.json.return_value = {"error": "Invalid API key"}
|
|
89
|
-
|
|
90
|
-
with patch("requests.Session.request", return_value=mock_response):
|
|
91
|
-
with pytest.raises(AuthenticationError):
|
|
92
|
-
client.search_email("test@example.com")
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
@patch("requests.Session.request")
|
|
96
|
-
def test_search_phone_success(mock_request, client, mock_response):
|
|
97
|
-
mock_request.return_value.json.return_value = mock_response
|
|
98
|
-
mock_request.return_value.status_code = 200
|
|
99
|
-
|
|
100
|
-
result = client.search_phone("+12125551234")
|
|
101
|
-
|
|
102
|
-
assert result.name == "John Doe"
|
|
103
|
-
assert result.dob == date(1990, 1, 1)
|
|
104
|
-
assert len(result.addresses) == 2
|
|
105
|
-
assert len(result.phone_numbers) == 2
|
|
106
|
-
assert isinstance(result.addresses[0], Address)
|
|
107
|
-
assert isinstance(result.phone_numbers[0], PhoneNumber)
|
|
108
|
-
assert result.addresses[1].zestimate == Decimal("1500000")
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
@patch("requests.Session.request")
|
|
112
|
-
def test_search_phone_invalid_format(client):
|
|
113
|
-
with pytest.raises(ValidationError):
|
|
114
|
-
client.search_phone("invalid-phone")
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
@patch("requests.Session.request")
|
|
118
|
-
def test_search_domain_success(mock_request, client):
|
|
119
|
-
mock_response = {
|
|
120
|
-
"results": [
|
|
121
|
-
{
|
|
122
|
-
"email": "test1@example.com",
|
|
123
|
-
"name": "John Doe",
|
|
124
|
-
"addresses": ["123 Main St, New York, NY 10001"],
|
|
125
|
-
"phone_numbers": ["+12125551234"],
|
|
126
|
-
},
|
|
127
|
-
{
|
|
128
|
-
"email": "test2@example.com",
|
|
129
|
-
"name": "Jane Smith",
|
|
130
|
-
"addresses": ["456 Park Ave, New York, NY 10022"],
|
|
131
|
-
"phone_numbers": ["+12125556789"],
|
|
132
|
-
},
|
|
133
|
-
]
|
|
134
|
-
}
|
|
135
|
-
mock_request.return_value.json.return_value = mock_response
|
|
136
|
-
mock_request.return_value.status_code = 200
|
|
137
|
-
|
|
138
|
-
result = client.search_domain("example.com")
|
|
139
|
-
|
|
140
|
-
assert result.domain == "example.com"
|
|
141
|
-
assert result.total_results == 2
|
|
142
|
-
assert len(result.results) == 2
|
|
143
|
-
assert result.results[0].email == "test1@example.com"
|
|
144
|
-
assert result.results[1].email == "test2@example.com"
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
@patch("requests.Session.request")
|
|
148
|
-
def test_search_domain_major_domain(client):
|
|
149
|
-
with pytest.raises(ValidationError):
|
|
150
|
-
client.search_domain("gmail.com")
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
@patch("requests.Session.request")
|
|
154
|
-
def test_search_domain_invalid_format(client):
|
|
155
|
-
with pytest.raises(ValidationError):
|
|
156
|
-
client.search_domain("invalid-domain")
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
def test_format_address(client):
|
|
160
|
-
address = "123 main st, new york, ny 10001"
|
|
161
|
-
formatted = client._format_address(address)
|
|
162
|
-
assert formatted == "123 Main Street, New York, NY 10001"
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
def test_parse_phone_number(client):
|
|
166
|
-
phone = "+12125551234"
|
|
167
|
-
result = client._parse_phone_number(phone)
|
|
168
|
-
assert isinstance(result, PhoneNumber)
|
|
169
|
-
assert result.number == "+12125551234"
|
|
170
|
-
assert result.is_valid is True
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
def test_parse_phone_number_invalid(client):
|
|
174
|
-
phone = "invalid-phone"
|
|
175
|
-
result = client._parse_phone_number(phone)
|
|
176
|
-
assert isinstance(result, PhoneNumber)
|
|
177
|
-
assert result.number == "invalid-phone"
|
|
178
|
-
assert result.is_valid is False
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|