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.
- ad_searchapi-1.0.0/AD_SearchAPI.egg-info/PKG-INFO +137 -0
- ad_searchapi-1.0.0/AD_SearchAPI.egg-info/SOURCES.txt +13 -0
- ad_searchapi-1.0.0/AD_SearchAPI.egg-info/dependency_links.txt +1 -0
- ad_searchapi-1.0.0/AD_SearchAPI.egg-info/requires.txt +5 -0
- ad_searchapi-1.0.0/AD_SearchAPI.egg-info/top_level.txt +1 -0
- ad_searchapi-1.0.0/LICENSE +21 -0
- ad_searchapi-1.0.0/PKG-INFO +137 -0
- ad_searchapi-1.0.0/README.md +102 -0
- ad_searchapi-1.0.0/search_api/__init__.py +22 -0
- ad_searchapi-1.0.0/search_api/client.py +436 -0
- ad_searchapi-1.0.0/search_api/exceptions.py +46 -0
- ad_searchapi-1.0.0/search_api/models.py +87 -0
- ad_searchapi-1.0.0/setup.cfg +4 -0
- ad_searchapi-1.0.0/setup.py +32 -0
- ad_searchapi-1.0.0/tests/test_client.py +179 -0
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -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,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
|