AD-SearchAPI 1.0.0__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.
@@ -0,0 +1,137 @@
1
+ Metadata-Version: 2.4
2
+ Name: AD-SearchAPI
3
+ Version: 1.0.0
4
+ Summary: A Python client library for the Search API
5
+ Home-page: https://github.com/AntiChrist-Coder/search_api_library
6
+ Author: Search API Team
7
+ Author-email: support@search-api.dev
8
+ Classifier: Development Status :: 5 - Production/Stable
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.7
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Requires-Python: >=3.7
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: requests>=2.31.0
21
+ Requires-Dist: phonenumbers>=8.13.0
22
+ Requires-Dist: python-dateutil>=2.8.2
23
+ Requires-Dist: cachetools>=5.3.0
24
+ Requires-Dist: typing-extensions>=4.7.0
25
+ Dynamic: author
26
+ Dynamic: author-email
27
+ Dynamic: classifier
28
+ Dynamic: description
29
+ Dynamic: description-content-type
30
+ Dynamic: home-page
31
+ Dynamic: license-file
32
+ Dynamic: requires-dist
33
+ Dynamic: requires-python
34
+ Dynamic: summary
35
+
36
+ # Search API Python Client
37
+
38
+ A Python client library for the Search API, providing easy access to email, phone, and domain search functionality.
39
+ Acquire your API-Key through @ADSearchEngine_bot on Telegram
40
+
41
+ ## Installation
42
+
43
+ ```bash
44
+ pip install search-api
45
+ ```
46
+
47
+ ## Quick Start
48
+
49
+ ```python
50
+ from search_api import SearchAPI
51
+
52
+ # Initialize the client with your API key
53
+ client = SearchAPI(api_key="your_api_key")
54
+
55
+ # Search by email
56
+ result = client.search_email("example@domain.com", include_house_value=True)
57
+ print(result)
58
+
59
+ # Search by phone
60
+ result = client.search_phone("+1234567890", include_extra_info=True)
61
+ print(result)
62
+
63
+ # Search by domain
64
+ result = client.search_domain("example.com")
65
+ print(result)
66
+ ```
67
+
68
+ ## Features
69
+
70
+ - Email search with optional house value and extra info
71
+ - Phone number search with validation and formatting
72
+ - Domain search with comprehensive results
73
+ - Automatic caching of results
74
+ - Rate limiting and retry handling
75
+ - Type hints and comprehensive documentation
76
+
77
+ ## Advanced Usage
78
+
79
+ ### Configuration
80
+
81
+ ```python
82
+ from search_api import SearchAPI, SearchAPIConfig
83
+
84
+ config = SearchAPIConfig(
85
+ api_key="your_api_key",
86
+ cache_ttl=3600, # Cache results for 1 hour
87
+ max_retries=3,
88
+ timeout=30,
89
+ base_url="https://search-api.dev"
90
+ )
91
+
92
+ client = SearchAPI(config=config)
93
+ ```
94
+
95
+ ### Error Handling
96
+
97
+ ```python
98
+ from search_api import SearchAPIError
99
+
100
+ try:
101
+ result = client.search_email("example@domain.com")
102
+ except SearchAPIError as e:
103
+ print(f"Error: {e.message}")
104
+ print(f"Status code: {e.status_code}")
105
+ ```
106
+
107
+ ## API Reference
108
+
109
+ ### SearchAPI
110
+
111
+ Main client class for interacting with the Search API.
112
+
113
+ #### Methods
114
+
115
+ - `search_email(email: str, include_house_value: bool = False, include_extra_info: bool = False) -> Dict`
116
+ - `search_phone(phone: str, include_house_value: bool = False, include_extra_info: bool = False) -> Dict`
117
+ - `search_domain(domain: str) -> Dict`
118
+
119
+ ### SearchAPIConfig
120
+
121
+ Configuration class for customizing client behavior.
122
+
123
+ #### Parameters
124
+
125
+ - `api_key: str` - Your API key
126
+ - `cache_ttl: int` - Cache time-to-live in seconds
127
+ - `max_retries: int` - Maximum number of retry attempts
128
+ - `timeout: int` - Request timeout in seconds
129
+ - `base_url: str` - API base URL
130
+
131
+ ## Contributing
132
+
133
+ Contributions are welcome! Please feel free to submit a Pull Request.
134
+
135
+ ## License
136
+
137
+ This project is licensed under the MIT License - see the LICENSE file for details.
@@ -0,0 +1,13 @@
1
+ LICENSE
2
+ README.md
3
+ setup.py
4
+ AD_SearchAPI.egg-info/PKG-INFO
5
+ AD_SearchAPI.egg-info/SOURCES.txt
6
+ AD_SearchAPI.egg-info/dependency_links.txt
7
+ AD_SearchAPI.egg-info/requires.txt
8
+ AD_SearchAPI.egg-info/top_level.txt
9
+ search_api/__init__.py
10
+ search_api/client.py
11
+ search_api/exceptions.py
12
+ search_api/models.py
13
+ tests/test_client.py
@@ -0,0 +1,5 @@
1
+ requests>=2.31.0
2
+ phonenumbers>=8.13.0
3
+ python-dateutil>=2.8.2
4
+ cachetools>=5.3.0
5
+ typing-extensions>=4.7.0
@@ -0,0 +1 @@
1
+ search_api
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Search API Team
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,137 @@
1
+ Metadata-Version: 2.4
2
+ Name: AD-SearchAPI
3
+ Version: 1.0.0
4
+ Summary: A Python client library for the Search API
5
+ Home-page: https://github.com/AntiChrist-Coder/search_api_library
6
+ Author: Search API Team
7
+ Author-email: support@search-api.dev
8
+ Classifier: Development Status :: 5 - Production/Stable
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.7
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Requires-Python: >=3.7
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: requests>=2.31.0
21
+ Requires-Dist: phonenumbers>=8.13.0
22
+ Requires-Dist: python-dateutil>=2.8.2
23
+ Requires-Dist: cachetools>=5.3.0
24
+ Requires-Dist: typing-extensions>=4.7.0
25
+ Dynamic: author
26
+ Dynamic: author-email
27
+ Dynamic: classifier
28
+ Dynamic: description
29
+ Dynamic: description-content-type
30
+ Dynamic: home-page
31
+ Dynamic: license-file
32
+ Dynamic: requires-dist
33
+ Dynamic: requires-python
34
+ Dynamic: summary
35
+
36
+ # Search API Python Client
37
+
38
+ A Python client library for the Search API, providing easy access to email, phone, and domain search functionality.
39
+ Acquire your API-Key through @ADSearchEngine_bot on Telegram
40
+
41
+ ## Installation
42
+
43
+ ```bash
44
+ pip install search-api
45
+ ```
46
+
47
+ ## Quick Start
48
+
49
+ ```python
50
+ from search_api import SearchAPI
51
+
52
+ # Initialize the client with your API key
53
+ client = SearchAPI(api_key="your_api_key")
54
+
55
+ # Search by email
56
+ result = client.search_email("example@domain.com", include_house_value=True)
57
+ print(result)
58
+
59
+ # Search by phone
60
+ result = client.search_phone("+1234567890", include_extra_info=True)
61
+ print(result)
62
+
63
+ # Search by domain
64
+ result = client.search_domain("example.com")
65
+ print(result)
66
+ ```
67
+
68
+ ## Features
69
+
70
+ - Email search with optional house value and extra info
71
+ - Phone number search with validation and formatting
72
+ - Domain search with comprehensive results
73
+ - Automatic caching of results
74
+ - Rate limiting and retry handling
75
+ - Type hints and comprehensive documentation
76
+
77
+ ## Advanced Usage
78
+
79
+ ### Configuration
80
+
81
+ ```python
82
+ from search_api import SearchAPI, SearchAPIConfig
83
+
84
+ config = SearchAPIConfig(
85
+ api_key="your_api_key",
86
+ cache_ttl=3600, # Cache results for 1 hour
87
+ max_retries=3,
88
+ timeout=30,
89
+ base_url="https://search-api.dev"
90
+ )
91
+
92
+ client = SearchAPI(config=config)
93
+ ```
94
+
95
+ ### Error Handling
96
+
97
+ ```python
98
+ from search_api import SearchAPIError
99
+
100
+ try:
101
+ result = client.search_email("example@domain.com")
102
+ except SearchAPIError as e:
103
+ print(f"Error: {e.message}")
104
+ print(f"Status code: {e.status_code}")
105
+ ```
106
+
107
+ ## API Reference
108
+
109
+ ### SearchAPI
110
+
111
+ Main client class for interacting with the Search API.
112
+
113
+ #### Methods
114
+
115
+ - `search_email(email: str, include_house_value: bool = False, include_extra_info: bool = False) -> Dict`
116
+ - `search_phone(phone: str, include_house_value: bool = False, include_extra_info: bool = False) -> Dict`
117
+ - `search_domain(domain: str) -> Dict`
118
+
119
+ ### SearchAPIConfig
120
+
121
+ Configuration class for customizing client behavior.
122
+
123
+ #### Parameters
124
+
125
+ - `api_key: str` - Your API key
126
+ - `cache_ttl: int` - Cache time-to-live in seconds
127
+ - `max_retries: int` - Maximum number of retry attempts
128
+ - `timeout: int` - Request timeout in seconds
129
+ - `base_url: str` - API base URL
130
+
131
+ ## Contributing
132
+
133
+ Contributions are welcome! Please feel free to submit a Pull Request.
134
+
135
+ ## License
136
+
137
+ This project is licensed under the MIT License - see the LICENSE file for details.
@@ -0,0 +1,102 @@
1
+ # Search API Python Client
2
+
3
+ A Python client library for the Search API, providing easy access to email, phone, and domain search functionality.
4
+ Acquire your API-Key through @ADSearchEngine_bot on Telegram
5
+
6
+ ## Installation
7
+
8
+ ```bash
9
+ pip install search-api
10
+ ```
11
+
12
+ ## Quick Start
13
+
14
+ ```python
15
+ from search_api import SearchAPI
16
+
17
+ # Initialize the client with your API key
18
+ client = SearchAPI(api_key="your_api_key")
19
+
20
+ # Search by email
21
+ result = client.search_email("example@domain.com", include_house_value=True)
22
+ print(result)
23
+
24
+ # Search by phone
25
+ result = client.search_phone("+1234567890", include_extra_info=True)
26
+ print(result)
27
+
28
+ # Search by domain
29
+ result = client.search_domain("example.com")
30
+ print(result)
31
+ ```
32
+
33
+ ## Features
34
+
35
+ - Email search with optional house value and extra info
36
+ - Phone number search with validation and formatting
37
+ - Domain search with comprehensive results
38
+ - Automatic caching of results
39
+ - Rate limiting and retry handling
40
+ - Type hints and comprehensive documentation
41
+
42
+ ## Advanced Usage
43
+
44
+ ### Configuration
45
+
46
+ ```python
47
+ from search_api import SearchAPI, SearchAPIConfig
48
+
49
+ config = SearchAPIConfig(
50
+ api_key="your_api_key",
51
+ cache_ttl=3600, # Cache results for 1 hour
52
+ max_retries=3,
53
+ timeout=30,
54
+ base_url="https://search-api.dev"
55
+ )
56
+
57
+ client = SearchAPI(config=config)
58
+ ```
59
+
60
+ ### Error Handling
61
+
62
+ ```python
63
+ from search_api import SearchAPIError
64
+
65
+ try:
66
+ result = client.search_email("example@domain.com")
67
+ except SearchAPIError as e:
68
+ print(f"Error: {e.message}")
69
+ print(f"Status code: {e.status_code}")
70
+ ```
71
+
72
+ ## API Reference
73
+
74
+ ### SearchAPI
75
+
76
+ Main client class for interacting with the Search API.
77
+
78
+ #### Methods
79
+
80
+ - `search_email(email: str, include_house_value: bool = False, include_extra_info: bool = False) -> Dict`
81
+ - `search_phone(phone: str, include_house_value: bool = False, include_extra_info: bool = False) -> Dict`
82
+ - `search_domain(domain: str) -> Dict`
83
+
84
+ ### SearchAPIConfig
85
+
86
+ Configuration class for customizing client behavior.
87
+
88
+ #### Parameters
89
+
90
+ - `api_key: str` - Your API key
91
+ - `cache_ttl: int` - Cache time-to-live in seconds
92
+ - `max_retries: int` - Maximum number of retry attempts
93
+ - `timeout: int` - Request timeout in seconds
94
+ - `base_url: str` - API base URL
95
+
96
+ ## Contributing
97
+
98
+ Contributions are welcome! Please feel free to submit a Pull Request.
99
+
100
+ ## License
101
+
102
+ This project is licensed under the MIT License - see the LICENSE file for details.
@@ -0,0 +1,22 @@
1
+ from .client import SearchAPI, SearchAPIConfig
2
+ from .exceptions import SearchAPIError
3
+ from .models import (
4
+ EmailSearchResult,
5
+ PhoneSearchResult,
6
+ DomainSearchResult,
7
+ Address,
8
+ PhoneNumber,
9
+ )
10
+
11
+ __version__ = "1.0.0"
12
+
13
+ __all__ = [
14
+ "SearchAPI",
15
+ "SearchAPIConfig",
16
+ "SearchAPIError",
17
+ "EmailSearchResult",
18
+ "PhoneSearchResult",
19
+ "DomainSearchResult",
20
+ "Address",
21
+ "PhoneNumber",
22
+ ]
@@ -0,0 +1,436 @@
1
+ import json
2
+ import re
3
+ from datetime import date
4
+ from decimal import Decimal
5
+ from typing import Dict, List, Optional, Union
6
+ from urllib.parse import urljoin
7
+
8
+ import phonenumbers
9
+ import requests
10
+ from cachetools import TTLCache
11
+ from dateutil.parser import parse
12
+
13
+ from .exceptions import (
14
+ AuthenticationError,
15
+ InsufficientBalanceError,
16
+ RateLimitError,
17
+ SearchAPIError,
18
+ ServerError,
19
+ ValidationError,
20
+ )
21
+ from .models import (
22
+ Address,
23
+ DomainSearchResult,
24
+ EmailSearchResult,
25
+ PhoneNumber,
26
+ PhoneSearchResult,
27
+ SearchAPIConfig,
28
+ )
29
+
30
+ # Major email domains that are not allowed for domain search
31
+ MAJOR_DOMAINS = {
32
+ "gmail.com",
33
+ "yahoo.com",
34
+ "outlook.com",
35
+ "hotmail.com",
36
+ "aol.com",
37
+ "icloud.com",
38
+ "live.com",
39
+ "msn.com",
40
+ "comcast.net",
41
+ "me.com",
42
+ "mac.com",
43
+ "att.net",
44
+ "verizon.net",
45
+ "protonmail.com",
46
+ "zoho.com",
47
+ "yandex.com",
48
+ "mail.com",
49
+ "gmx.com",
50
+ "rocketmail.com",
51
+ "yahoo.co.uk",
52
+ "btinternet.com",
53
+ "bellsouth.net",
54
+ }
55
+
56
+ # Street type mapping
57
+ STREET_TYPE_MAP = {
58
+ "st": "Street",
59
+ "ave": "Avenue",
60
+ "blvd": "Boulevard",
61
+ "rd": "Road",
62
+ "ln": "Lane",
63
+ "dr": "Drive",
64
+ "ct": "Court",
65
+ "ter": "Terrace",
66
+ "pl": "Place",
67
+ "way": "Way",
68
+ "pkwy": "Parkway",
69
+ "cir": "Circle",
70
+ "sq": "Square",
71
+ "hwy": "Highway",
72
+ "bend": "Bend",
73
+ "cove": "Cove",
74
+ }
75
+
76
+ # State abbreviations mapping
77
+ STATE_ABBREVIATIONS = {
78
+ "al": "AL",
79
+ "ak": "AK",
80
+ "az": "AZ",
81
+ "ar": "AR",
82
+ "ca": "CA",
83
+ "co": "CO",
84
+ "ct": "CT",
85
+ "de": "DE",
86
+ "fl": "FL",
87
+ "ga": "GA",
88
+ "hi": "HI",
89
+ "id": "ID",
90
+ "il": "IL",
91
+ "in": "IN",
92
+ "ia": "IA",
93
+ "ks": "KS",
94
+ "ky": "KY",
95
+ "la": "LA",
96
+ "me": "ME",
97
+ "md": "MD",
98
+ "ma": "MA",
99
+ "mi": "MI",
100
+ "mn": "MN",
101
+ "ms": "MS",
102
+ "mo": "MO",
103
+ "mt": "MT",
104
+ "ne": "NE",
105
+ "nv": "NV",
106
+ "nh": "NH",
107
+ "nj": "NJ",
108
+ "nm": "NM",
109
+ "ny": "NY",
110
+ "nc": "NC",
111
+ "nd": "ND",
112
+ "oh": "OH",
113
+ "ok": "OK",
114
+ "or": "OR",
115
+ "pa": "PA",
116
+ "ri": "RI",
117
+ "sc": "SC",
118
+ "sd": "SD",
119
+ "tn": "TN",
120
+ "tx": "TX",
121
+ "ut": "UT",
122
+ "vt": "VT",
123
+ "va": "VA",
124
+ "wa": "WA",
125
+ "wv": "WV",
126
+ "wi": "WI",
127
+ "wy": "WY",
128
+ }
129
+
130
+
131
+ class SearchAPI:
132
+ """Main client for interacting with the Search API."""
133
+
134
+ def __init__(self, api_key: str = None, config: SearchAPIConfig = None):
135
+ """Initialize the Search API client.
136
+
137
+ Args:
138
+ api_key: Your API key
139
+ config: Optional configuration object
140
+ """
141
+ if config is None:
142
+ if api_key is None:
143
+ raise ValueError("Either api_key or config must be provided")
144
+ config = SearchAPIConfig(api_key=api_key)
145
+
146
+ self.config = config
147
+ self.session = requests.Session()
148
+ self.session.headers.update(
149
+ {
150
+ "User-Agent": config.user_agent,
151
+ "Accept": "application/json",
152
+ }
153
+ )
154
+ self.cache = TTLCache(maxsize=1000, ttl=config.cache_ttl)
155
+
156
+ def _make_request(
157
+ self,
158
+ endpoint: str,
159
+ params: Optional[Dict] = None,
160
+ method: str = "GET",
161
+ data: Optional[Dict] = None,
162
+ ) -> Dict:
163
+ """Make a request to the Search API.
164
+
165
+ Args:
166
+ endpoint: API endpoint
167
+ params: Query parameters
168
+ method: HTTP method
169
+ data: Request body data
170
+
171
+ Returns:
172
+ API response as dictionary
173
+
174
+ Raises:
175
+ SearchAPIError: If the request fails
176
+ """
177
+ url = urljoin(self.config.base_url, endpoint)
178
+ params = params or {}
179
+ params["api_key"] = self.config.api_key
180
+
181
+ try:
182
+ response = self.session.request(
183
+ method,
184
+ url,
185
+ params=params,
186
+ json=data,
187
+ timeout=self.config.timeout,
188
+ )
189
+ response.raise_for_status()
190
+ return response.json()
191
+ except requests.exceptions.HTTPError as e:
192
+ if e.response.status_code == 401:
193
+ raise AuthenticationError("Invalid API key", e.response.status_code)
194
+ elif e.response.status_code == 402:
195
+ raise InsufficientBalanceError(
196
+ "Insufficient balance", e.response.status_code
197
+ )
198
+ elif e.response.status_code == 429:
199
+ raise RateLimitError("Rate limit exceeded", e.response.status_code)
200
+ elif e.response.status_code >= 500:
201
+ raise ServerError("Server error", e.response.status_code)
202
+ else:
203
+ raise SearchAPIError(
204
+ f"HTTP error: {e.response.text}",
205
+ e.response.status_code,
206
+ e.response.json() if e.response.text else None,
207
+ )
208
+ except requests.exceptions.RequestException as e:
209
+ raise SearchAPIError(f"Request error: {str(e)}")
210
+
211
+ def _format_address(self, address_str: str) -> str:
212
+ """Format an address string according to standard conventions.
213
+
214
+ Args:
215
+ address_str: Raw address string
216
+
217
+ Returns:
218
+ Formatted address string
219
+ """
220
+ parts = [part.strip() for part in address_str.split(",") if part.strip()]
221
+ formatted_parts = []
222
+
223
+ for part in parts:
224
+ words = part.split()
225
+ for i, word in enumerate(words):
226
+ word_lower = word.lower()
227
+ words[i] = STREET_TYPE_MAP.get(word_lower, word.title())
228
+ formatted_parts.append(" ".join(words))
229
+
230
+ if formatted_parts:
231
+ last_part = formatted_parts[-1].split()
232
+ if len(last_part) > 1 and last_part[-1].isdigit():
233
+ state = last_part[-2].lower()
234
+ if state in STATE_ABBREVIATIONS:
235
+ last_part[-2] = STATE_ABBREVIATIONS[state]
236
+ formatted_parts[-1] = " ".join(last_part)
237
+ elif last_part:
238
+ state = last_part[-1].lower()
239
+ if state in STATE_ABBREVIATIONS:
240
+ last_part[-1] = STATE_ABBREVIATIONS[state]
241
+ formatted_parts[-1] = " ".join(last_part)
242
+
243
+ return ", ".join(formatted_parts)
244
+
245
+ def _parse_address(self, address_data: Union[str, Dict]) -> Address:
246
+ """Parse address data into an Address object.
247
+
248
+ Args:
249
+ address_data: Address data as string or dictionary
250
+
251
+ Returns:
252
+ Address object
253
+ """
254
+ if isinstance(address_data, str):
255
+ return Address(street=self._format_address(address_data))
256
+ else:
257
+ return Address(
258
+ street=address_data.get("street", ""),
259
+ city=address_data.get("city"),
260
+ state=address_data.get("state"),
261
+ postal_code=address_data.get("postal_code"),
262
+ country=address_data.get("country"),
263
+ zestimate=Decimal(str(address_data["zestimate"]))
264
+ if "zestimate" in address_data
265
+ else None,
266
+ )
267
+
268
+ def _parse_phone_number(self, phone_str: str) -> PhoneNumber:
269
+ """Parse and validate a phone number.
270
+
271
+ Args:
272
+ phone_str: Phone number string
273
+
274
+ Returns:
275
+ PhoneNumber object
276
+ """
277
+ try:
278
+ number = phonenumbers.parse(phone_str, "US")
279
+ is_valid = phonenumbers.is_valid_number(number)
280
+ formatted = phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.E164)
281
+ return PhoneNumber(number=formatted, is_valid=is_valid)
282
+ except phonenumbers.NumberParseException:
283
+ return PhoneNumber(number=phone_str, is_valid=False)
284
+
285
+ def search_email(
286
+ self, email: str, include_house_value: bool = False, include_extra_info: bool = False
287
+ ) -> EmailSearchResult:
288
+ """Search for information by email address.
289
+
290
+ Args:
291
+ email: Email address to search
292
+ include_house_value: Whether to include house value information
293
+ include_extra_info: Whether to include extra information
294
+
295
+ Returns:
296
+ EmailSearchResult object
297
+
298
+ Raises:
299
+ ValidationError: If the email is invalid
300
+ SearchAPIError: If the request fails
301
+ """
302
+ if not re.match(r"[^@]+@[^@]+\.[^@]+", email):
303
+ raise ValidationError("Invalid email format")
304
+
305
+ cache_key = f"email_{email}_{include_house_value}_{include_extra_info}"
306
+ if cache_key in self.cache:
307
+ return self.cache[cache_key]
308
+
309
+ params = {
310
+ "email": email,
311
+ "house_value": str(include_house_value).lower(),
312
+ "extra_info": str(include_extra_info).lower(),
313
+ }
314
+
315
+ response = self._make_request("search.php", params)
316
+ if "error" in response:
317
+ raise SearchAPIError(response["error"])
318
+
319
+ result = EmailSearchResult(
320
+ email=email,
321
+ name=response.get("name"),
322
+ dob=parse(response["dob"]).date() if response.get("dob") else None,
323
+ addresses=[
324
+ self._parse_address(addr) for addr in response.get("addresses", [])
325
+ ],
326
+ phone_numbers=[
327
+ self._parse_phone_number(num) for num in response.get("numbers", [])
328
+ ],
329
+ extra_info=response.get("extra_info"),
330
+ )
331
+
332
+ self.cache[cache_key] = result
333
+ return result
334
+
335
+ def search_phone(
336
+ self, phone: str, include_house_value: bool = False, include_extra_info: bool = False
337
+ ) -> PhoneSearchResult:
338
+ """Search for information by phone number.
339
+
340
+ Args:
341
+ phone: Phone number to search
342
+ include_house_value: Whether to include house value information
343
+ include_extra_info: Whether to include extra information
344
+
345
+ Returns:
346
+ PhoneSearchResult object
347
+
348
+ Raises:
349
+ ValidationError: If the phone number is invalid
350
+ SearchAPIError: If the request fails
351
+ """
352
+ phone_number = self._parse_phone_number(phone)
353
+ if not phone_number.is_valid:
354
+ raise ValidationError("Invalid phone number format")
355
+
356
+ cache_key = f"phone_{phone}_{include_house_value}_{include_extra_info}"
357
+ if cache_key in self.cache:
358
+ return self.cache[cache_key]
359
+
360
+ params = {
361
+ "phone": phone,
362
+ "house_value": str(include_house_value).lower(),
363
+ "extra_info": str(include_extra_info).lower(),
364
+ }
365
+
366
+ response = self._make_request("search.php", params)
367
+ if "error" in response:
368
+ raise SearchAPIError(response["error"])
369
+
370
+ result = PhoneSearchResult(
371
+ phone=phone_number,
372
+ name=response.get("name"),
373
+ dob=parse(response["dob"]).date() if response.get("dob") else None,
374
+ addresses=[
375
+ self._parse_address(addr) for addr in response.get("addresses", [])
376
+ ],
377
+ phone_numbers=[
378
+ self._parse_phone_number(num) for num in response.get("numbers", [])
379
+ ],
380
+ extra_info=response.get("extra_info"),
381
+ )
382
+
383
+ self.cache[cache_key] = result
384
+ return result
385
+
386
+ def search_domain(self, domain: str) -> DomainSearchResult:
387
+ """Search for information by domain name.
388
+
389
+ Args:
390
+ domain: Domain name to search
391
+
392
+ Returns:
393
+ DomainSearchResult object
394
+
395
+ Raises:
396
+ ValidationError: If the domain is invalid or is a major domain
397
+ SearchAPIError: If the request fails
398
+ """
399
+ domain = domain.lower().strip()
400
+ if not re.match(r"^[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,}$", domain):
401
+ raise ValidationError("Invalid domain format")
402
+
403
+ if domain in MAJOR_DOMAINS:
404
+ raise ValidationError("Searching major domains is not allowed")
405
+
406
+ cache_key = f"domain_{domain}"
407
+ if cache_key in self.cache:
408
+ return self.cache[cache_key]
409
+
410
+ params = {"domain": domain}
411
+ response = self._make_request("search.php", params)
412
+ if "error" in response:
413
+ raise SearchAPIError(response["error"])
414
+
415
+ results = []
416
+ for item in response.get("results", []):
417
+ email_result = EmailSearchResult(
418
+ email=item["email"],
419
+ name=item.get("name"),
420
+ addresses=[
421
+ self._parse_address(addr) for addr in item.get("addresses", [])
422
+ ],
423
+ phone_numbers=[
424
+ self._parse_phone_number(num) for num in item.get("phone_numbers", [])
425
+ ],
426
+ )
427
+ results.append(email_result)
428
+
429
+ result = DomainSearchResult(
430
+ domain=domain,
431
+ results=results,
432
+ total_results=len(results),
433
+ )
434
+
435
+ self.cache[cache_key] = result
436
+ return result
@@ -0,0 +1,46 @@
1
+ from typing import Optional
2
+
3
+
4
+ class SearchAPIError(Exception):
5
+ """Base exception for all Search API errors."""
6
+
7
+ def __init__(
8
+ self,
9
+ message: str,
10
+ status_code: Optional[int] = None,
11
+ response: Optional[dict] = None,
12
+ ):
13
+ self.message = message
14
+ self.status_code = status_code
15
+ self.response = response
16
+ super().__init__(self.message)
17
+
18
+
19
+ class AuthenticationError(SearchAPIError):
20
+ """Raised when there are authentication issues."""
21
+
22
+ pass
23
+
24
+
25
+ class ValidationError(SearchAPIError):
26
+ """Raised when input validation fails."""
27
+
28
+ pass
29
+
30
+
31
+ class RateLimitError(SearchAPIError):
32
+ """Raised when rate limit is exceeded."""
33
+
34
+ pass
35
+
36
+
37
+ class InsufficientBalanceError(SearchAPIError):
38
+ """Raised when API key has insufficient balance."""
39
+
40
+ pass
41
+
42
+
43
+ class ServerError(SearchAPIError):
44
+ """Raised when the server returns an error."""
45
+
46
+ pass
@@ -0,0 +1,87 @@
1
+ from dataclasses import dataclass, field
2
+ from datetime import date
3
+ from typing import List, Optional, Union
4
+ from decimal import Decimal
5
+
6
+
7
+ @dataclass
8
+ class Address:
9
+ """Represents a physical address with optional Zestimate value."""
10
+
11
+ street: str
12
+ city: Optional[str] = None
13
+ state: Optional[str] = None
14
+ postal_code: Optional[str] = None
15
+ country: Optional[str] = None
16
+ zestimate: Optional[Decimal] = None
17
+
18
+ def __str__(self) -> str:
19
+ parts = [self.street]
20
+ if self.city:
21
+ parts.append(self.city)
22
+ if self.state:
23
+ parts.append(self.state)
24
+ if self.postal_code:
25
+ parts.append(self.postal_code)
26
+ if self.country:
27
+ parts.append(self.country)
28
+ return ", ".join(parts)
29
+
30
+
31
+ @dataclass
32
+ class PhoneNumber:
33
+ """Represents a phone number with validation."""
34
+
35
+ number: str
36
+ country_code: str = "US"
37
+ is_valid: bool = True
38
+
39
+ def __str__(self) -> str:
40
+ return self.number
41
+
42
+
43
+ @dataclass
44
+ class BaseSearchResult:
45
+ """Base class for all search results."""
46
+
47
+ name: Optional[str] = None
48
+ dob: Optional[date] = None
49
+ addresses: List[Address] = field(default_factory=list)
50
+ phone_numbers: List[PhoneNumber] = field(default_factory=list)
51
+
52
+
53
+ @dataclass
54
+ class EmailSearchResult(BaseSearchResult):
55
+ """Result from email search."""
56
+
57
+ email: str
58
+ extra_info: Optional[dict] = None
59
+
60
+
61
+ @dataclass
62
+ class PhoneSearchResult(BaseSearchResult):
63
+ """Result from phone search."""
64
+
65
+ phone: PhoneNumber
66
+ extra_info: Optional[dict] = None
67
+
68
+
69
+ @dataclass
70
+ class DomainSearchResult:
71
+ """Result from domain search."""
72
+
73
+ domain: str
74
+ results: List[EmailSearchResult] = field(default_factory=list)
75
+ total_results: int = 0
76
+
77
+
78
+ @dataclass
79
+ class SearchAPIConfig:
80
+ """Configuration for the Search API client."""
81
+
82
+ api_key: str
83
+ cache_ttl: int = 3600 # 1 hour
84
+ max_retries: int = 3
85
+ timeout: int = 30
86
+ base_url: str = "https://search-api.dev"
87
+ user_agent: str = "SearchAPI-Python/1.0.0"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,32 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="AD-SearchAPI",
5
+ version="1.0.0",
6
+ packages=find_packages(),
7
+ install_requires=[
8
+ "requests>=2.31.0",
9
+ "phonenumbers>=8.13.0",
10
+ "python-dateutil>=2.8.2",
11
+ "cachetools>=5.3.0",
12
+ "typing-extensions>=4.7.0",
13
+ ],
14
+ author="Search API Team",
15
+ author_email="support@search-api.dev",
16
+ description="A Python client library for the Search API",
17
+ long_description=open("README.md").read(),
18
+ long_description_content_type="text/markdown",
19
+ url="https://github.com/AntiChrist-Coder/search_api_library",
20
+ classifiers=[
21
+ "Development Status :: 5 - Production/Stable",
22
+ "Intended Audience :: Developers",
23
+ "License :: OSI Approved :: MIT License",
24
+ "Programming Language :: Python :: 3",
25
+ "Programming Language :: Python :: 3.7",
26
+ "Programming Language :: Python :: 3.8",
27
+ "Programming Language :: Python :: 3.9",
28
+ "Programming Language :: Python :: 3.10",
29
+ "Programming Language :: Python :: 3.11",
30
+ ],
31
+ python_requires=">=3.7",
32
+ )
@@ -0,0 +1,179 @@
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
+ cache_ttl=1800,
49
+ max_retries=5,
50
+ timeout=60,
51
+ base_url="https://custom-api.dev",
52
+ )
53
+ client = SearchAPI(config=config)
54
+ assert client.config == config
55
+
56
+
57
+ def test_init_without_api_key_or_config():
58
+ with pytest.raises(ValueError):
59
+ SearchAPI()
60
+
61
+
62
+ @patch("requests.Session.request")
63
+ def test_search_email_success(mock_request, client, mock_response):
64
+ mock_request.return_value.json.return_value = mock_response
65
+ mock_request.return_value.status_code = 200
66
+
67
+ result = client.search_email("test@example.com")
68
+
69
+ assert result.name == "John Doe"
70
+ assert result.dob == date(1990, 1, 1)
71
+ assert len(result.addresses) == 2
72
+ assert len(result.phone_numbers) == 2
73
+ assert isinstance(result.addresses[0], Address)
74
+ assert isinstance(result.phone_numbers[0], PhoneNumber)
75
+ assert result.addresses[1].zestimate == Decimal("1500000")
76
+
77
+
78
+ @patch("requests.Session.request")
79
+ def test_search_email_invalid_format(client):
80
+ with pytest.raises(ValidationError):
81
+ client.search_email("invalid-email")
82
+
83
+
84
+ @patch("requests.Session.request")
85
+ def test_search_email_api_error(client):
86
+ mock_response = Mock()
87
+ mock_response.status_code = 401
88
+ mock_response.text = "Invalid API key"
89
+ mock_response.json.return_value = {"error": "Invalid API key"}
90
+
91
+ with patch("requests.Session.request", return_value=mock_response):
92
+ with pytest.raises(AuthenticationError):
93
+ client.search_email("test@example.com")
94
+
95
+
96
+ @patch("requests.Session.request")
97
+ def test_search_phone_success(mock_request, client, mock_response):
98
+ mock_request.return_value.json.return_value = mock_response
99
+ mock_request.return_value.status_code = 200
100
+
101
+ result = client.search_phone("+12125551234")
102
+
103
+ assert result.name == "John Doe"
104
+ assert result.dob == date(1990, 1, 1)
105
+ assert len(result.addresses) == 2
106
+ assert len(result.phone_numbers) == 2
107
+ assert isinstance(result.addresses[0], Address)
108
+ assert isinstance(result.phone_numbers[0], PhoneNumber)
109
+ assert result.addresses[1].zestimate == Decimal("1500000")
110
+
111
+
112
+ @patch("requests.Session.request")
113
+ def test_search_phone_invalid_format(client):
114
+ with pytest.raises(ValidationError):
115
+ client.search_phone("invalid-phone")
116
+
117
+
118
+ @patch("requests.Session.request")
119
+ def test_search_domain_success(mock_request, client):
120
+ mock_response = {
121
+ "results": [
122
+ {
123
+ "email": "test1@example.com",
124
+ "name": "John Doe",
125
+ "addresses": ["123 Main St, New York, NY 10001"],
126
+ "phone_numbers": ["+12125551234"],
127
+ },
128
+ {
129
+ "email": "test2@example.com",
130
+ "name": "Jane Smith",
131
+ "addresses": ["456 Park Ave, New York, NY 10022"],
132
+ "phone_numbers": ["+12125556789"],
133
+ },
134
+ ]
135
+ }
136
+ mock_request.return_value.json.return_value = mock_response
137
+ mock_request.return_value.status_code = 200
138
+
139
+ result = client.search_domain("example.com")
140
+
141
+ assert result.domain == "example.com"
142
+ assert result.total_results == 2
143
+ assert len(result.results) == 2
144
+ assert result.results[0].email == "test1@example.com"
145
+ assert result.results[1].email == "test2@example.com"
146
+
147
+
148
+ @patch("requests.Session.request")
149
+ def test_search_domain_major_domain(client):
150
+ with pytest.raises(ValidationError):
151
+ client.search_domain("gmail.com")
152
+
153
+
154
+ @patch("requests.Session.request")
155
+ def test_search_domain_invalid_format(client):
156
+ with pytest.raises(ValidationError):
157
+ client.search_domain("invalid-domain")
158
+
159
+
160
+ def test_format_address(client):
161
+ address = "123 main st, new york, ny 10001"
162
+ formatted = client._format_address(address)
163
+ assert formatted == "123 Main Street, New York, NY 10001"
164
+
165
+
166
+ def test_parse_phone_number(client):
167
+ phone = "+12125551234"
168
+ result = client._parse_phone_number(phone)
169
+ assert isinstance(result, PhoneNumber)
170
+ assert result.number == "+12125551234"
171
+ assert result.is_valid is True
172
+
173
+
174
+ def test_parse_phone_number_invalid(client):
175
+ phone = "invalid-phone"
176
+ result = client._parse_phone_number(phone)
177
+ assert isinstance(result, PhoneNumber)
178
+ assert result.number == "invalid-phone"
179
+ assert result.is_valid is False