adspower 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.
- adspower-1.0.0/LICENSE +21 -0
- adspower-1.0.0/PKG-INFO +108 -0
- adspower-1.0.0/README.md +83 -0
- adspower-1.0.0/adspower/__init__.py +20 -0
- adspower-1.0.0/adspower/adspower.py +498 -0
- adspower-1.0.0/adspower/exceptions.py +93 -0
- adspower-1.0.0/adspower/types.py +68 -0
- adspower-1.0.0/adspower/utils.py +12 -0
- adspower-1.0.0/pyproject.toml +36 -0
adspower-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Alexey
|
|
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.
|
adspower-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: adspower
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: The package for interacting with API of anti-detect browser AdsPower.
|
|
5
|
+
Home-page: https://github.com/blnkoff/adspower
|
|
6
|
+
License: MIT
|
|
7
|
+
Author: Alexey
|
|
8
|
+
Author-email: abelenkov2006@gmail.com
|
|
9
|
+
Requires-Python: >=3.11,<4.0
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: MacOS
|
|
14
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Dist: requests (>=2.31.0,<3.0.0)
|
|
21
|
+
Requires-Dist: selenium (>=4.16.0,<5.0.0)
|
|
22
|
+
Project-URL: Repository, https://github.com/blnkoff/adspower
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# adspower
|
|
26
|
+
|
|
27
|
+
[](https://t.me/crocofactory)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
The package for interacting with API of anti-detect browser AdsPower.
|
|
31
|
+
|
|
32
|
+
- **[Telegram channel](https://t.me/crocofactory)**
|
|
33
|
+
- **[Bug reports](https://github.com/blnkoff/ether-wallet/issues)**
|
|
34
|
+
|
|
35
|
+
adspower's source code is made available under the [MIT License](LICENSE)
|
|
36
|
+
|
|
37
|
+
# Restrictions
|
|
38
|
+
1. During using the package, AdsPower must be opened.
|
|
39
|
+
2. The local API is available only in paid AdsPower subscriptions
|
|
40
|
+
3. AdsPower has frequency control for all APIs, max. access frequency: 1 request/second
|
|
41
|
+
|
|
42
|
+
# Quick start
|
|
43
|
+
|
|
44
|
+
For example, you can use driver by the following way.
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from adspower import AdsPower
|
|
48
|
+
ads_power = AdsPower('ja54rwh')
|
|
49
|
+
with ads_power as driver:
|
|
50
|
+
driver.get('https://github.com/blnkoff/adspower')
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Here is example of usage of creating profiles from proxies
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from adspower import AdsPower, CreateProfileParams, UserProxyConfig
|
|
57
|
+
|
|
58
|
+
def create_profiles(config: dict[str], proxies) -> list[AdsPower]:
|
|
59
|
+
group_name = config['ads_power']['group_name']
|
|
60
|
+
|
|
61
|
+
group_id = AdsPower.query_group(group_name=group_name)[0]['group_id']
|
|
62
|
+
|
|
63
|
+
data_set = []
|
|
64
|
+
for proxy in proxies:
|
|
65
|
+
proxy_config = UserProxyConfig(
|
|
66
|
+
proxy_soft='other',
|
|
67
|
+
proxy_type=proxy.type,
|
|
68
|
+
proxy_host=proxy.host,
|
|
69
|
+
proxy_port=proxy.port,
|
|
70
|
+
proxy_user=proxy.login,
|
|
71
|
+
proxy_password=proxy.password
|
|
72
|
+
)
|
|
73
|
+
data_set.append(CreateProfileParams(group_id=group_id, user_proxy_config=proxy_config))
|
|
74
|
+
|
|
75
|
+
profiles = []
|
|
76
|
+
for data in data_set:
|
|
77
|
+
profiles.append(AdsPower.create_profile(data))
|
|
78
|
+
|
|
79
|
+
return profiles
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Here is example of retrieving profiles from AdsPower
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from adspower import AdsPower
|
|
86
|
+
|
|
87
|
+
def retrieve_profiles(config: dict[str]):
|
|
88
|
+
group_name = config['ads_power']['group_name']
|
|
89
|
+
|
|
90
|
+
group_id = AdsPower.query_group(group_name=group_name)['group_id']
|
|
91
|
+
|
|
92
|
+
response = AdsPower.query_profiles(group_id=group_id)
|
|
93
|
+
|
|
94
|
+
profiles = [AdsPower(profile['user_id']) for profile in response]
|
|
95
|
+
return profiles
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
# Installing adspower
|
|
99
|
+
To install the package from GitHub you can use that:
|
|
100
|
+
```sh
|
|
101
|
+
pip install git+https://github.com/blnkoff/adspower.git
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
To install the package from PyPi you can use that:
|
|
105
|
+
```sh
|
|
106
|
+
pip install adspower
|
|
107
|
+
```
|
|
108
|
+
|
adspower-1.0.0/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# adspower
|
|
2
|
+
|
|
3
|
+
[](https://t.me/crocofactory)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
The package for interacting with API of anti-detect browser AdsPower.
|
|
7
|
+
|
|
8
|
+
- **[Telegram channel](https://t.me/crocofactory)**
|
|
9
|
+
- **[Bug reports](https://github.com/blnkoff/ether-wallet/issues)**
|
|
10
|
+
|
|
11
|
+
adspower's source code is made available under the [MIT License](LICENSE)
|
|
12
|
+
|
|
13
|
+
# Restrictions
|
|
14
|
+
1. During using the package, AdsPower must be opened.
|
|
15
|
+
2. The local API is available only in paid AdsPower subscriptions
|
|
16
|
+
3. AdsPower has frequency control for all APIs, max. access frequency: 1 request/second
|
|
17
|
+
|
|
18
|
+
# Quick start
|
|
19
|
+
|
|
20
|
+
For example, you can use driver by the following way.
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from adspower import AdsPower
|
|
24
|
+
ads_power = AdsPower('ja54rwh')
|
|
25
|
+
with ads_power as driver:
|
|
26
|
+
driver.get('https://github.com/blnkoff/adspower')
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Here is example of usage of creating profiles from proxies
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from adspower import AdsPower, CreateProfileParams, UserProxyConfig
|
|
33
|
+
|
|
34
|
+
def create_profiles(config: dict[str], proxies) -> list[AdsPower]:
|
|
35
|
+
group_name = config['ads_power']['group_name']
|
|
36
|
+
|
|
37
|
+
group_id = AdsPower.query_group(group_name=group_name)[0]['group_id']
|
|
38
|
+
|
|
39
|
+
data_set = []
|
|
40
|
+
for proxy in proxies:
|
|
41
|
+
proxy_config = UserProxyConfig(
|
|
42
|
+
proxy_soft='other',
|
|
43
|
+
proxy_type=proxy.type,
|
|
44
|
+
proxy_host=proxy.host,
|
|
45
|
+
proxy_port=proxy.port,
|
|
46
|
+
proxy_user=proxy.login,
|
|
47
|
+
proxy_password=proxy.password
|
|
48
|
+
)
|
|
49
|
+
data_set.append(CreateProfileParams(group_id=group_id, user_proxy_config=proxy_config))
|
|
50
|
+
|
|
51
|
+
profiles = []
|
|
52
|
+
for data in data_set:
|
|
53
|
+
profiles.append(AdsPower.create_profile(data))
|
|
54
|
+
|
|
55
|
+
return profiles
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Here is example of retrieving profiles from AdsPower
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from adspower import AdsPower
|
|
62
|
+
|
|
63
|
+
def retrieve_profiles(config: dict[str]):
|
|
64
|
+
group_name = config['ads_power']['group_name']
|
|
65
|
+
|
|
66
|
+
group_id = AdsPower.query_group(group_name=group_name)['group_id']
|
|
67
|
+
|
|
68
|
+
response = AdsPower.query_profiles(group_id=group_id)
|
|
69
|
+
|
|
70
|
+
profiles = [AdsPower(profile['user_id']) for profile in response]
|
|
71
|
+
return profiles
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
# Installing adspower
|
|
75
|
+
To install the package from GitHub you can use that:
|
|
76
|
+
```sh
|
|
77
|
+
pip install git+https://github.com/blnkoff/adspower.git
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
To install the package from PyPi you can use that:
|
|
81
|
+
```sh
|
|
82
|
+
pip install adspower
|
|
83
|
+
```
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
adspower
|
|
3
|
+
~~~~~~~~~~~~~~
|
|
4
|
+
The package for interacting with anti-detect browser APIs.
|
|
5
|
+
Author's github - https://github.com/blnkoff
|
|
6
|
+
|
|
7
|
+
Usage example:
|
|
8
|
+
>>> from adspower import AdsPower
|
|
9
|
+
>>> ads_power = AdsPower('ja54rwh')
|
|
10
|
+
>>> with ads_power as driver:
|
|
11
|
+
>>> driver.get('https://github.com/blnkoff/adspower')
|
|
12
|
+
:copyright: (c) 2023 by Alexey
|
|
13
|
+
:license: Apache 2.0, see LICENSE for more details.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
__version__ = "1.0.0"
|
|
17
|
+
|
|
18
|
+
from .adspower import AdsPower
|
|
19
|
+
from .types import (UserProxyConfig, FingerprintConfig, CreateProfileParams, QueryProfilesParams, QueryGroupParams,
|
|
20
|
+
GroupInfo, ProfileInfo)
|
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import requests
|
|
3
|
+
from requests.exceptions import ConnectionError as RequestConnectionError, InvalidURL
|
|
4
|
+
from urllib3.exceptions import MaxRetryError, NewConnectionError
|
|
5
|
+
from .types import *
|
|
6
|
+
from .exceptions import *
|
|
7
|
+
from functools import wraps
|
|
8
|
+
from selenium import webdriver
|
|
9
|
+
from typing import Self, Optional, Any, Type, Iterable
|
|
10
|
+
from selenium.webdriver.chrome.options import Options
|
|
11
|
+
from selenium.webdriver.chrome.service import Service
|
|
12
|
+
from selenium.webdriver.chrome.webdriver import WebDriver
|
|
13
|
+
from .utils import is_typed_dict
|
|
14
|
+
|
|
15
|
+
_request_accessibility = 0
|
|
16
|
+
|
|
17
|
+
__all__ = ['AdsPower']
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AdsPower:
|
|
21
|
+
"""Controls AdsPower overall and the instance of corresponding profile"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, profile_id: Optional[str] = None, port: int = 50325):
|
|
24
|
+
"""
|
|
25
|
+
:param profile_id: ID of existed profile in AdsPower
|
|
26
|
+
:param port: The port of AdsPower's API
|
|
27
|
+
"""
|
|
28
|
+
self._driver = None
|
|
29
|
+
self.__profile_id = profile_id
|
|
30
|
+
self.__api_url = self._validate_api_url('http://local.adspower.net', port)
|
|
31
|
+
|
|
32
|
+
def __enter__(self):
|
|
33
|
+
driver = self.get_driver()
|
|
34
|
+
driver.maximize_window()
|
|
35
|
+
for _ in range(3):
|
|
36
|
+
time.sleep(1.5)
|
|
37
|
+
self.close_tabs()
|
|
38
|
+
return driver
|
|
39
|
+
|
|
40
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
41
|
+
self.quit()
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def driver(self) -> WebDriver | None:
|
|
45
|
+
"""
|
|
46
|
+
:return: The driver instance or None if get_driver() was not called
|
|
47
|
+
"""
|
|
48
|
+
return self._driver
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def api_url(self) -> str:
|
|
52
|
+
"""
|
|
53
|
+
:return: The URL of API of an anti-detect browser
|
|
54
|
+
"""
|
|
55
|
+
return self.__api_url
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def profile_id(self) -> str:
|
|
59
|
+
"""
|
|
60
|
+
Returns ID of existed profile in anti-detect browser
|
|
61
|
+
:return: The ID of existed profile in anti-detect
|
|
62
|
+
"""
|
|
63
|
+
return self.__profile_id
|
|
64
|
+
|
|
65
|
+
@profile_id.setter
|
|
66
|
+
def profile_id(self, value: str):
|
|
67
|
+
self.__profile_id = value
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def _wait_for_delay(func):
|
|
71
|
+
@wraps(func)
|
|
72
|
+
def wrapper(*args, **kwargs) -> Any:
|
|
73
|
+
current_time = time.time()
|
|
74
|
+
global _request_accessibility
|
|
75
|
+
|
|
76
|
+
if _request_accessibility < current_time:
|
|
77
|
+
result = func(*args, **kwargs)
|
|
78
|
+
else:
|
|
79
|
+
time.sleep(_request_accessibility - current_time)
|
|
80
|
+
result = func(*args, **kwargs)
|
|
81
|
+
|
|
82
|
+
_request_accessibility = time.time() + 0.7
|
|
83
|
+
return result
|
|
84
|
+
|
|
85
|
+
return wrapper
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def _validate_response(exception: Type[Exception], request: dict[str, Any], response: dict[str, Any]):
|
|
89
|
+
if response['code'] != 0:
|
|
90
|
+
if response['msg'] == 'Too many request per second, please check':
|
|
91
|
+
raise ExceededQPS
|
|
92
|
+
else:
|
|
93
|
+
raise exception(request, response)
|
|
94
|
+
|
|
95
|
+
@staticmethod
|
|
96
|
+
def _validate_api_url(api_url: str, port: int) -> str:
|
|
97
|
+
url = f'{api_url}:{port}'
|
|
98
|
+
try:
|
|
99
|
+
status_url = url + '/status'
|
|
100
|
+
requests.get(url=status_url)
|
|
101
|
+
except (MaxRetryError, RequestConnectionError, NewConnectionError, ConnectionRefusedError, InvalidURL):
|
|
102
|
+
raise UnavailableAPI(port)
|
|
103
|
+
else:
|
|
104
|
+
return url
|
|
105
|
+
|
|
106
|
+
@_wait_for_delay
|
|
107
|
+
def get_driver(self, profile_id: Optional[str] = None) -> WebDriver:
|
|
108
|
+
"""
|
|
109
|
+
Returns a driver associated with profile_id
|
|
110
|
+
|
|
111
|
+
:raise ProfileNotFound: Raised when profile getting or deleting is failed
|
|
112
|
+
:raise NoProfileFound: Raised when profile ID is not specified
|
|
113
|
+
:return: A WebDriver instance associated with profile_id
|
|
114
|
+
"""
|
|
115
|
+
profile_id = profile_id or self.profile_id
|
|
116
|
+
if not profile_id:
|
|
117
|
+
raise NoProfileIdFound
|
|
118
|
+
|
|
119
|
+
self.profile_id = profile_id
|
|
120
|
+
url = self.api_url + '/api/v1/browser/start'
|
|
121
|
+
|
|
122
|
+
params = {
|
|
123
|
+
'ip_tab': 0,
|
|
124
|
+
'user_id': profile_id
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
response = requests.get(url=url, params=params).json()
|
|
128
|
+
self._validate_response(ProfileNotFound, params, response)
|
|
129
|
+
|
|
130
|
+
debugger_address = response['data']['ws']['selenium']
|
|
131
|
+
chrome_driver = response['data']['webdriver']
|
|
132
|
+
|
|
133
|
+
options = Options()
|
|
134
|
+
options.add_experimental_option('debuggerAddress', debugger_address)
|
|
135
|
+
options.page_load_strategy = 'none'
|
|
136
|
+
options.add_argument('--headless=new')
|
|
137
|
+
|
|
138
|
+
service = Service(executable_path=chrome_driver)
|
|
139
|
+
self._driver = webdriver.Chrome(service=service, options=options)
|
|
140
|
+
return self.driver
|
|
141
|
+
|
|
142
|
+
@classmethod
|
|
143
|
+
@_wait_for_delay
|
|
144
|
+
def create_profile(
|
|
145
|
+
cls,
|
|
146
|
+
data: Optional[CreateProfileParams] = None,
|
|
147
|
+
port: int = 50325,
|
|
148
|
+
*,
|
|
149
|
+
group_id: Optional[int] = None,
|
|
150
|
+
name: Optional[str] = None,
|
|
151
|
+
user_proxy_config: Optional[UserProxyConfig] = None,
|
|
152
|
+
fingerprint_config: Optional[FingerprintConfig] = None
|
|
153
|
+
) -> Self:
|
|
154
|
+
"""
|
|
155
|
+
Returns an instance of class referring to a new profile
|
|
156
|
+
|
|
157
|
+
:param data: Keyword arguments as dictionary to create profile
|
|
158
|
+
:param port: The port of API
|
|
159
|
+
:param name: The name of the account, no more than 100 characters
|
|
160
|
+
:param group_id: The group ID corresponding to the group in which an account is to be created,
|
|
161
|
+
:param user_proxy_config: Dictionary arguments to update a proxy
|
|
162
|
+
:param fingerprint_config: Dictionary arguments to update a fingerprint
|
|
163
|
+
|
|
164
|
+
:raise ProfileCreationError: Raised when profile creation is failed
|
|
165
|
+
:return: An AdsPower instance with the specified profile ID
|
|
166
|
+
|
|
167
|
+
Request parameters
|
|
168
|
+
----------
|
|
169
|
+
{
|
|
170
|
+
'name':
|
|
171
|
+
The name of the account, no more than 100 characters,
|
|
172
|
+
'group_id':
|
|
173
|
+
The group ID corresponding to the group in which an account is to be created,
|
|
174
|
+
'user_proxy_config': {
|
|
175
|
+
'proxy_soft':
|
|
176
|
+
The proxy software. Currently, supports:
|
|
177
|
+
luminati, lumauto, oxylabsatuto, 922S5,ipideaauto,ipfoxyauto,ssh, other, no_proxy,
|
|
178
|
+
Default value - no_proxy,
|
|
179
|
+
'proxy_type':
|
|
180
|
+
The proxy type. Currently, supports:
|
|
181
|
+
http, https, socks5;
|
|
182
|
+
For no_proxy, you may pass the parameter or not,
|
|
183
|
+
'proxy_host':
|
|
184
|
+
Address of the proxy server, users can enter domain name or IP;
|
|
185
|
+
For no_proxy, you may pass the parameter or not,
|
|
186
|
+
'proxy_port':
|
|
187
|
+
Port of the proxy server;
|
|
188
|
+
For no_proxy, you may pass the parameter or not,
|
|
189
|
+
'proxy_user':
|
|
190
|
+
The username of the proxy,
|
|
191
|
+
'proxy_password':
|
|
192
|
+
The password of the proxy
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
'fingerprint_config': {
|
|
196
|
+
'language_switch':
|
|
197
|
+
Configure languages based on IP country. Default value - 0
|
|
198
|
+
0: Off;
|
|
199
|
+
1: On.
|
|
200
|
+
'ua':
|
|
201
|
+
String containing user agent.
|
|
202
|
+
When customizing please make sure that ua format and content meet
|
|
203
|
+
the requirement.
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
"""
|
|
207
|
+
kwargs = locals()
|
|
208
|
+
del kwargs['port']
|
|
209
|
+
del kwargs['data']
|
|
210
|
+
del kwargs['cls']
|
|
211
|
+
|
|
212
|
+
api_url = cls._validate_api_url('http://local.adspower.net', port)
|
|
213
|
+
|
|
214
|
+
url = api_url + '/api/v1/user/create'
|
|
215
|
+
|
|
216
|
+
data = data or kwargs
|
|
217
|
+
if not data.get('group_id'):
|
|
218
|
+
raise NoGroupIdFound
|
|
219
|
+
|
|
220
|
+
if not data.get('user_proxy_config'):
|
|
221
|
+
data['user_proxy_config'] = {'proxy_soft': 'no_proxy'}
|
|
222
|
+
|
|
223
|
+
response = requests.post(url=url, json=data).json()
|
|
224
|
+
cls._validate_response(ProfileCreationError, data, response)
|
|
225
|
+
|
|
226
|
+
return cls(response['data']['id'], port)
|
|
227
|
+
|
|
228
|
+
@classmethod
|
|
229
|
+
@_wait_for_delay
|
|
230
|
+
def delete_profiles(cls, profile_ids: str | Iterable[str], port: int = 50325) -> None:
|
|
231
|
+
"""
|
|
232
|
+
Deletes profiles associated with specified profile_ids in AdsPower
|
|
233
|
+
:param profile_ids: Iterable containing profile_ids or one profile ID
|
|
234
|
+
:param port: The port of AdsPower's API
|
|
235
|
+
|
|
236
|
+
:raise ProfileNotFound: Raised when a profile does not exist
|
|
237
|
+
:return: None
|
|
238
|
+
"""
|
|
239
|
+
if not profile_ids:
|
|
240
|
+
raise NoProfileIdFound
|
|
241
|
+
|
|
242
|
+
api_url = cls._validate_api_url('http://local.adspower.net', port)
|
|
243
|
+
url = api_url + '/api/v1/user/delete'
|
|
244
|
+
|
|
245
|
+
if isinstance(profile_ids, str):
|
|
246
|
+
profile_ids = [profile_ids]
|
|
247
|
+
else:
|
|
248
|
+
profile_ids_ = []
|
|
249
|
+
for profile_id in profile_ids:
|
|
250
|
+
profile_ids_.append(profile_id)
|
|
251
|
+
profile_ids = profile_ids_
|
|
252
|
+
|
|
253
|
+
data = {'user_ids': profile_ids}
|
|
254
|
+
|
|
255
|
+
response = requests.post(url=url, json=data).json()
|
|
256
|
+
cls._validate_response(ProfileNotFound, data, response)
|
|
257
|
+
|
|
258
|
+
@classmethod
|
|
259
|
+
@_wait_for_delay
|
|
260
|
+
def query_group(
|
|
261
|
+
cls,
|
|
262
|
+
params: Optional[QueryGroupParams] = None,
|
|
263
|
+
port: int = 50325,
|
|
264
|
+
*,
|
|
265
|
+
group_name: Optional[str] = None,
|
|
266
|
+
profile_id: Optional[str] = None,
|
|
267
|
+
page_size: Optional[int] = 100
|
|
268
|
+
) -> GroupResponse:
|
|
269
|
+
"""
|
|
270
|
+
Returns dictionaries containing information about the groups by specified parameters. If there are no
|
|
271
|
+
params, excluding page_size, it returns a list of dictionaries, containing information about all groups
|
|
272
|
+
|
|
273
|
+
:param params: Keyword arguments as dictionary to query groups
|
|
274
|
+
:param port: The port of AdsPower's API
|
|
275
|
+
:param group_name: The group name corresponding to the group
|
|
276
|
+
:param profile_id: ID of existed profile in AdsPower
|
|
277
|
+
:param page_size: The maximum length of returning list. Default value - 100
|
|
278
|
+
|
|
279
|
+
:raise GroupQueryError: Raised when group query is failed
|
|
280
|
+
:return: List of dictionaries containing information about corresponding groups
|
|
281
|
+
|
|
282
|
+
Request parameters
|
|
283
|
+
----------
|
|
284
|
+
{
|
|
285
|
+
'group_name':
|
|
286
|
+
The group name corresponding to the group,
|
|
287
|
+
'profile_id':
|
|
288
|
+
ID of existed profile in AdsPower,
|
|
289
|
+
'page_size':
|
|
290
|
+
The maximum length of returning list. Default value - 100
|
|
291
|
+
}
|
|
292
|
+
"""
|
|
293
|
+
kwargs = locals()
|
|
294
|
+
del kwargs['cls']
|
|
295
|
+
del kwargs['params']
|
|
296
|
+
del kwargs['port']
|
|
297
|
+
|
|
298
|
+
api_url = cls._validate_api_url('http://local.adspower.net', port)
|
|
299
|
+
url = api_url + '/api/v1/group/list'
|
|
300
|
+
|
|
301
|
+
params = params or kwargs
|
|
302
|
+
if not params.get('page_size'):
|
|
303
|
+
params['page_size'] = 100
|
|
304
|
+
|
|
305
|
+
if len(params) == 1:
|
|
306
|
+
response = requests.get(url=url, params=params).json()
|
|
307
|
+
cls._validate_response(GroupQueryError, params, response)
|
|
308
|
+
return response['data']['list']
|
|
309
|
+
else:
|
|
310
|
+
params['user_id'] = params.pop('profile_id')
|
|
311
|
+
response = requests.get(url=url, params=params).json()
|
|
312
|
+
cls._validate_response(GroupQueryError, params, response)
|
|
313
|
+
return response['data']['list'][0]
|
|
314
|
+
|
|
315
|
+
@classmethod
|
|
316
|
+
@_wait_for_delay
|
|
317
|
+
def query_profiles(
|
|
318
|
+
cls,
|
|
319
|
+
params: Optional[QueryProfilesParams] = None,
|
|
320
|
+
port: int = 50325,
|
|
321
|
+
*,
|
|
322
|
+
group_id: Optional[int] = None,
|
|
323
|
+
page_size: Optional[int] = 100
|
|
324
|
+
) -> ProfileResponse:
|
|
325
|
+
"""
|
|
326
|
+
Returns a list consisting of dictionaries containing information about the profiles by specified parameters
|
|
327
|
+
|
|
328
|
+
:param params: Keyword arguments as dictionary to query profiles
|
|
329
|
+
:param port: The port of AdsPower's API
|
|
330
|
+
:param group_id: The group ID corresponding to the group in which an account is to be created,
|
|
331
|
+
:param page_size: The maximum length of returning list. Default value - 100
|
|
332
|
+
|
|
333
|
+
:raise ProfileQueryError: Raised when profile query is failed
|
|
334
|
+
:return: list[dict[str, str]]: List of dictionaries containing information about corresponding profiles
|
|
335
|
+
|
|
336
|
+
Request parameters
|
|
337
|
+
----------
|
|
338
|
+
{
|
|
339
|
+
'group_id':
|
|
340
|
+
The group ID corresponding to the group in which an account exists,
|
|
341
|
+
'page_size':
|
|
342
|
+
The maximum length of returning list. Default value - 100
|
|
343
|
+
}
|
|
344
|
+
"""
|
|
345
|
+
kwargs = locals()
|
|
346
|
+
del kwargs['cls']
|
|
347
|
+
del kwargs['params']
|
|
348
|
+
del kwargs['port']
|
|
349
|
+
|
|
350
|
+
api_url = cls._validate_api_url('http://local.adspower.net', port)
|
|
351
|
+
url = api_url + '/api/v1/user/list'
|
|
352
|
+
|
|
353
|
+
params = params or kwargs
|
|
354
|
+
if not params.get('page_size'):
|
|
355
|
+
params['page_size'] = 100
|
|
356
|
+
|
|
357
|
+
response = requests.get(url=url, params=params).json()
|
|
358
|
+
|
|
359
|
+
cls._validate_response(ProfileQueryError, params, response)
|
|
360
|
+
|
|
361
|
+
return response['data']['list']
|
|
362
|
+
|
|
363
|
+
@_wait_for_delay
|
|
364
|
+
def update_proxy(
|
|
365
|
+
self,
|
|
366
|
+
data: Optional[UpdatingProxyParams] = None,
|
|
367
|
+
*,
|
|
368
|
+
profile_id: Optional[str] = None,
|
|
369
|
+
proxy_soft: Optional[str] = None,
|
|
370
|
+
proxy_type: Optional[str] = None,
|
|
371
|
+
proxy_host: Optional[str] = None,
|
|
372
|
+
proxy_user: Optional[str] = None,
|
|
373
|
+
proxy_password: Optional[str] = None
|
|
374
|
+
) -> None:
|
|
375
|
+
"""
|
|
376
|
+
Updates proxy in current profile
|
|
377
|
+
|
|
378
|
+
:param data: Keyword arguments as dictionary to update proxy
|
|
379
|
+
:param profile_id: ID of existed profile in AdsPower
|
|
380
|
+
:param proxy_soft: The proxy software. Currently, supports:
|
|
381
|
+
luminati, lumauto, oxylabsatuto, 922S5,ipideaauto,ipfoxyauto,ssh, other, noproxy,
|
|
382
|
+
By default you should to set 'other'.
|
|
383
|
+
:param proxy_type: The proxy type. Currently, supports:
|
|
384
|
+
http, https, socks5;
|
|
385
|
+
For no_proxy, you may pass the parameter or not
|
|
386
|
+
:param proxy_host: Address of the proxy server, users can enter domain name or IP;
|
|
387
|
+
For no_proxy, you may pass the parameter or not,
|
|
388
|
+
:param proxy_user: The username of the proxy,
|
|
389
|
+
:param proxy_password: The password of the proxy
|
|
390
|
+
|
|
391
|
+
:raise InvalidProxyConfig: Raised when proxy config is invalid
|
|
392
|
+
:raise ProxyUpdateError: Raised when proxy update is failed
|
|
393
|
+
:return: None
|
|
394
|
+
|
|
395
|
+
Request parameters
|
|
396
|
+
----------
|
|
397
|
+
{
|
|
398
|
+
'profile_id':
|
|
399
|
+
ID of existed profile in AdsPower
|
|
400
|
+
'proxy_soft':
|
|
401
|
+
The proxy software. Currently, supports:
|
|
402
|
+
luminati, lumauto, oxylabsatuto, 922S5,ipideaauto,ipfoxyauto,ssh, other, noproxy,
|
|
403
|
+
By default you should to set 'other',
|
|
404
|
+
'proxy_type':
|
|
405
|
+
The proxy type. Currently, supports:
|
|
406
|
+
http, https, socks5;
|
|
407
|
+
For no_proxy, you may pass the parameter or not,
|
|
408
|
+
'proxy_host':
|
|
409
|
+
Address of the proxy server, users can enter domain name or IP;
|
|
410
|
+
For no_proxy, you may pass the parameter or not,
|
|
411
|
+
'proxy_port':
|
|
412
|
+
Port of the proxy server;
|
|
413
|
+
For no_proxy, you may pass the parameter or not,
|
|
414
|
+
'proxy_user':
|
|
415
|
+
The username of the proxy,
|
|
416
|
+
'proxy_password':
|
|
417
|
+
The password of the proxy
|
|
418
|
+
}
|
|
419
|
+
"""
|
|
420
|
+
kwargs = locals()
|
|
421
|
+
del kwargs['data']
|
|
422
|
+
del kwargs['self']
|
|
423
|
+
|
|
424
|
+
url = self.api_url + '/api/v1/user/update'
|
|
425
|
+
|
|
426
|
+
data = data or kwargs
|
|
427
|
+
if not data.get('profile_id'):
|
|
428
|
+
data['profile_id'] = self.profile_id
|
|
429
|
+
|
|
430
|
+
if not is_typed_dict(data, UpdatingProxyParams):
|
|
431
|
+
raise InvalidProxyConfig(data)
|
|
432
|
+
|
|
433
|
+
profile_id = data.pop('profile_id')
|
|
434
|
+
data_to_send = {
|
|
435
|
+
'user_id': profile_id,
|
|
436
|
+
'user_proxy_config': data
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
response = requests.post(url=url, json=data_to_send).json()
|
|
440
|
+
self._validate_response(ProxyUpdateError, data_to_send, response)
|
|
441
|
+
|
|
442
|
+
@_wait_for_delay
|
|
443
|
+
def update_user_agent(self, user_agent: str, profile_id: Optional[str] = None) -> None:
|
|
444
|
+
"""
|
|
445
|
+
Updates user agent in current profile
|
|
446
|
+
|
|
447
|
+
:param profile_id: ID of existed profile in AdsPower
|
|
448
|
+
:param user_agent: String containing user agent.
|
|
449
|
+
When customizing please make sure that ua format and content meet
|
|
450
|
+
the requirement.
|
|
451
|
+
|
|
452
|
+
:raise NoUserAgentFound: Raised when user agent is not specified
|
|
453
|
+
:raise UserAgentUpdateError: Raised when user agent update is failed
|
|
454
|
+
:return: None
|
|
455
|
+
"""
|
|
456
|
+
url = self.api_url + '/api/v1/user/update'
|
|
457
|
+
if not user_agent:
|
|
458
|
+
raise NoUserAgentFound
|
|
459
|
+
|
|
460
|
+
user_agent = user_agent
|
|
461
|
+
profile_id = self.profile_id or profile_id
|
|
462
|
+
data = {
|
|
463
|
+
'user_id': profile_id,
|
|
464
|
+
'fingerprint_config': {'ua': user_agent}
|
|
465
|
+
}
|
|
466
|
+
response = requests.post(url=url, json=data).json()
|
|
467
|
+
|
|
468
|
+
self._validate_response(UserAgentUpdateError, data, response)
|
|
469
|
+
|
|
470
|
+
def close_tabs(self) -> None:
|
|
471
|
+
"""
|
|
472
|
+
Closes all tabs in anti-detect browser
|
|
473
|
+
|
|
474
|
+
:return: None
|
|
475
|
+
"""
|
|
476
|
+
driver = self.driver
|
|
477
|
+
original_window_handle = driver.current_window_handle
|
|
478
|
+
windows = driver.window_handles
|
|
479
|
+
for window in windows:
|
|
480
|
+
if original_window_handle != window:
|
|
481
|
+
driver.switch_to.window(window)
|
|
482
|
+
driver.close()
|
|
483
|
+
driver.switch_to.window(original_window_handle)
|
|
484
|
+
|
|
485
|
+
@_wait_for_delay
|
|
486
|
+
def quit(self) -> None:
|
|
487
|
+
"""
|
|
488
|
+
Closes current profile
|
|
489
|
+
|
|
490
|
+
:raise QuittingProfileError: Quitting profile is failed. Profile can be already closed
|
|
491
|
+
:return: None
|
|
492
|
+
"""
|
|
493
|
+
self.driver.quit()
|
|
494
|
+
profile_id = self.profile_id
|
|
495
|
+
url = self.api_url + '/api/v1/browser/stop'
|
|
496
|
+
params = {'user_id': profile_id}
|
|
497
|
+
response = requests.get(url=url, params=params).json()
|
|
498
|
+
self._validate_response(QuittingProfileError, params, response)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from adspower.types import UpdatingProxyParams
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class NoParamsFound(Exception):
|
|
6
|
+
"""Raised when params are not specified via positional arguments or keyword arguments"""
|
|
7
|
+
def __init__(self):
|
|
8
|
+
super().__init__("Params are not specified via positional arguments or keyword arguments")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ZeroResponse(Exception):
|
|
12
|
+
"""Raised if response is 0"""
|
|
13
|
+
def __init__(self, message: str, request: dict[str, Any], response: dict[str]):
|
|
14
|
+
super().__init__(f"{message}. \nRequest: {request}. \nResponse: {response}")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ProxyUpdateError(ZeroResponse):
|
|
18
|
+
"""Raised when proxy update is failed"""
|
|
19
|
+
def __init__(self, request: dict[str, Any], response: dict[str]):
|
|
20
|
+
super().__init__("The proxy update is failed", request, response)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class UserAgentUpdateError(ZeroResponse):
|
|
24
|
+
"""Raised when user agent update is failed"""
|
|
25
|
+
def __init__(self, request: dict[str, Any], response: dict[str]):
|
|
26
|
+
super().__init__("The user agent update is failed", request, response)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ProfileCreationError(ZeroResponse):
|
|
30
|
+
"""Raised when profile creation is failed"""
|
|
31
|
+
def __init__(self, request: dict[str, Any], response: dict[str]):
|
|
32
|
+
super().__init__("The profile creation is failed", request, response)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class GroupQueryError(ZeroResponse):
|
|
36
|
+
"""Raised when group query is failed"""
|
|
37
|
+
def __init__(self, request: dict[str, Any], response: dict[str]):
|
|
38
|
+
super().__init__("The group query is failed", request, response)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ProfileQueryError(ZeroResponse):
|
|
42
|
+
"""Raised when profile query is failed"""
|
|
43
|
+
def __init__(self, request: dict[str, Any], response: dict[str]):
|
|
44
|
+
super().__init__("The profile query is failed", request, response)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ProfileNotFound(ZeroResponse):
|
|
48
|
+
"""Raised when profile getting or deleting is failed"""
|
|
49
|
+
def __init__(self, request: dict[str, Any], response: dict[str]):
|
|
50
|
+
super().__init__("Profile getting is failed", request, response)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class QuittingProfileError(ZeroResponse):
|
|
54
|
+
"""Raised when quitting profile is failed."""
|
|
55
|
+
def __init__(self, request: dict[str, Any], response: dict[str]):
|
|
56
|
+
super().__init__("Quitting profile is failed. Profile can be already closed", request, response)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class NoGroupIdFound(Exception):
|
|
60
|
+
"""Raised when group id is not specified"""
|
|
61
|
+
def __init__(self):
|
|
62
|
+
super().__init__("The id of a group isn't passed")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class NoProfileIdFound(Exception):
|
|
66
|
+
"""Raised when profile id is not specified"""
|
|
67
|
+
def __init__(self):
|
|
68
|
+
super().__init__("The id of a profile isn't passed")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class NoUserAgentFound(Exception):
|
|
72
|
+
"""Raised when user agent is not specified"""
|
|
73
|
+
def __init__(self):
|
|
74
|
+
super().__init__("The user agent isn't passed")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class InvalidProxyConfig(Exception):
|
|
78
|
+
"""Raised when proxy config is invalid"""
|
|
79
|
+
|
|
80
|
+
def __init__(self, proxy_config: dict[str] | UpdatingProxyParams):
|
|
81
|
+
super().__init__(f"The proxy config is invalid. Config: {proxy_config}")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class ExceededQPS(Exception):
|
|
85
|
+
"""Raised when amount of queries per second is exceeded"""
|
|
86
|
+
def __init__(self):
|
|
87
|
+
super().__init__("Too many request per second, please try later")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class UnavailableAPI(Exception):
|
|
91
|
+
"""Raised when API url specified with invalid port or AdsPower is not opened"""
|
|
92
|
+
def __init__(self, port: int):
|
|
93
|
+
super().__init__(f"Your API url is specified with invalid port or AdsPower is not opened. You provided port {port}")
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from typing import TypedDict, Literal, NotRequired
|
|
2
|
+
|
|
3
|
+
ProxySoft = Literal['luminati', 'lumauto', 'oxylabsatuto', '922S5', 'ipideaauto',
|
|
4
|
+
'ipfoxyauto', 'ssh', 'other', 'no_proxy']
|
|
5
|
+
|
|
6
|
+
ProxyType = Literal['http', 'https', 'socks5']
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class UserProxyConfig(TypedDict):
|
|
10
|
+
proxy_soft: ProxySoft
|
|
11
|
+
proxy_type: ProxyType
|
|
12
|
+
proxy_host: str
|
|
13
|
+
proxy_port: int
|
|
14
|
+
proxy_user: str
|
|
15
|
+
proxy_password: str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class UpdatingProxyParams(TypedDict):
|
|
19
|
+
profile_id: NotRequired[str]
|
|
20
|
+
proxy_config: UserProxyConfig
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FingerprintConfig(TypedDict):
|
|
24
|
+
ua: str
|
|
25
|
+
language_switch: NotRequired[bool]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CreateProfileParams(TypedDict):
|
|
29
|
+
name: NotRequired[str]
|
|
30
|
+
group_id: int
|
|
31
|
+
user_proxy_config: NotRequired[UserProxyConfig]
|
|
32
|
+
fingerprint_config: NotRequired[FingerprintConfig]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class QueryGroupParams(TypedDict):
|
|
36
|
+
group_name: NotRequired[str]
|
|
37
|
+
profile_id: NotRequired[str]
|
|
38
|
+
page_size: NotRequired[int]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class QueryProfilesParams(TypedDict):
|
|
42
|
+
group_id: NotRequired[int]
|
|
43
|
+
page_size: NotRequired[int]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class GroupInfo(TypedDict):
|
|
47
|
+
group_id: int
|
|
48
|
+
group_name: str
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ProfileInfo(TypedDict):
|
|
52
|
+
serial_number: int
|
|
53
|
+
user_id: str
|
|
54
|
+
name: str
|
|
55
|
+
group_id: int
|
|
56
|
+
group_name: str
|
|
57
|
+
domain_name: str
|
|
58
|
+
username: str
|
|
59
|
+
remark: str
|
|
60
|
+
created_time: int
|
|
61
|
+
ip: str
|
|
62
|
+
ip_country: str
|
|
63
|
+
password: str
|
|
64
|
+
last_open_time: int
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
GroupResponse = list[GroupInfo] | GroupInfo
|
|
68
|
+
ProfileResponse = list[ProfileInfo]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from typing import Type, Any, TypedDict
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def is_typed_dict(value: Any, typed_dict: Type[TypedDict]) -> bool:
|
|
5
|
+
typed_dict_keys = typed_dict.__annotations__.keys()
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
value_keys = value.keys()
|
|
9
|
+
except AttributeError:
|
|
10
|
+
return False
|
|
11
|
+
|
|
12
|
+
return typed_dict_keys == value_keys
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = 'adspower'
|
|
3
|
+
version = '1.0.0'
|
|
4
|
+
description = 'The package for interacting with API of anti-detect browser AdsPower.'
|
|
5
|
+
authors = ['Alexey <abelenkov2006@gmail.com>']
|
|
6
|
+
license = 'MIT'
|
|
7
|
+
readme = 'README.md'
|
|
8
|
+
repository = 'https://github.com/blnkoff/adspower'
|
|
9
|
+
homepage = 'https://github.com/blnkoff/adspower'
|
|
10
|
+
classifiers = [
|
|
11
|
+
'Development Status :: 5 - Production/Stable',
|
|
12
|
+
'Intended Audience :: Developers',
|
|
13
|
+
'Topic :: Software Development :: Libraries :: Python Modules',
|
|
14
|
+
'Programming Language :: Python :: 3.11',
|
|
15
|
+
'Programming Language :: Python :: 3 :: Only',
|
|
16
|
+
'License :: OSI Approved :: MIT License',
|
|
17
|
+
'Operating System :: Microsoft :: Windows',
|
|
18
|
+
'Operating System :: MacOS'
|
|
19
|
+
]
|
|
20
|
+
packages = [{ include = 'adspower' }]
|
|
21
|
+
|
|
22
|
+
[tool.poetry.dependencies]
|
|
23
|
+
python = '^3.11'
|
|
24
|
+
requests = "^2.31.0"
|
|
25
|
+
selenium = "^4.16.0"
|
|
26
|
+
|
|
27
|
+
[tool.poetry.group.dev.dependencies]
|
|
28
|
+
pytest = "^7.4.3"
|
|
29
|
+
twine = "^4.0.2"
|
|
30
|
+
build = "^1.0.3"
|
|
31
|
+
sphinx-rtd-theme = "^2.0.0"
|
|
32
|
+
sphinx = "^7.2.6"
|
|
33
|
+
|
|
34
|
+
[build-system]
|
|
35
|
+
requires = ['poetry-core']
|
|
36
|
+
build-backend = 'poetry.core.masonry.api'
|