├── LICENSE
├── README.md
├── adspower
├── __init__.py
├── _api_entity.py
├── _base_category.py
├── _base_group.py
├── _base_http_client.py
├── _base_profile_api.py
├── async_api
│ ├── __init__.py
│ ├── _base_profile.py
│ ├── category.py
│ ├── group.py
│ ├── http_client.py
│ ├── playwright
│ │ ├── __init__.py
│ │ └── profile.py
│ ├── profile_api.py
│ └── selenium
│ │ ├── __init__.py
│ │ └── profile.py
├── exceptions.py
├── sync_api
│ ├── __init__.py
│ ├── _base_profile.py
│ ├── category.py
│ ├── group.py
│ ├── http_client.py
│ ├── playwright
│ │ ├── __init__.py
│ │ └── profile.py
│ ├── profile_api.py
│ └── selenium
│ │ ├── __init__.py
│ │ └── profile.py
├── types.py
└── utils.py
├── branding
└── adspower
│ └── banner.png
├── pyproject.toml
└── tests
├── __init__.py
├── conftest.py
├── test_async_api.py
├── test_sync_api.py
└── utils.py
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Belenkov 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.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # adspower
2 |
3 | 
4 |
5 | [](https://pypi.org/project/adspower/)
6 | [](https://pypi.org/project/adspower/)
7 |
8 |
9 | The package for interacting with API of anti-detect browser [AdsPower](https://www.adspower.com).
10 |
11 | - **[Overview](#quick-start)**
12 | - **[Installing](#installing-adspower)**
13 | - **[Bug reports](https://github.com/CrocoFactory/adspower/issues)**
14 |
15 | The project is made by the **[Croco Factory](https://github.com/CrocoFactory)** team
16 |
17 | adspower's source code is made available under the [MIT License](LICENSE)
18 |
19 | ## Features
20 | - Synchronous and asynchronous interaction with the local API
21 | - Interaction with the most popular libraries for browser automation in Python: Selenium and Playwright
22 |
23 | ## Restrictions
24 | 1. During using the package, AdsPower must be opened.
25 | 2. The local API is available only in paid AdsPower subscriptions
26 | 3. AdsPower has frequency control for all APIs, max. access frequency: 1 request/second
27 |
28 |
29 | ## Quick start
30 |
31 | *Example of interacting with synchronous API.*
32 |
33 | ```python
34 | from adspower.sync_api import Group, ProfileAPI
35 | group = Group.create(name='my_group', remark='The best group ever')
36 |
37 | profile_api = ProfileAPI.create(group=group)
38 | print(f'Profile {profile_api.name} was created in group {group.name}')
39 | ```
40 |
41 | **Use `ProfileAPI` only when** you don't need `Selenium` and `Playwright` interactions.
42 |
43 | Library provides ways to interact the most popular libraries for browser automation in Python: `Selenium` and `Playwright`.
44 | To get a browser, you can use `with` statement:
45 |
46 | - *Selenium*
47 |
48 | ```python
49 | from adspower.sync_api.selenium import Profile, Group
50 | my_group = Group.query(name='my_group')[0]
51 | profile = Profile.create(group=my_group, name='my_profile')
52 |
53 | with profile as browser:
54 | browser.get('https://github.com/blnkoff/adspower')
55 | ```
56 |
57 | - *Playwright*
58 |
59 | ```python
60 | from adspower.async_api.playwright import Profile, Group
61 |
62 | async def main() -> None:
63 | my_group = (await Group.query(name='my_group'))[0]
64 | profile = await Profile.create(group=my_group, name='my_profile')
65 |
66 | async with profile as browser:
67 | page = browser.pages[0]
68 | await page.goto('https://github.com/blnkoff/adspower')
69 | ```
70 |
71 | Both versions support sync and async API.
72 |
73 | Or manually call `get_browser` if you need specify part of behaviour.
74 | ```python
75 | from adspower.sync_api.selenium import Profile, Group
76 |
77 | my_group = Group.query(name='my_group')[0]
78 | profile = Profile.create(group=my_group, name='my_profile')
79 | browser = profile.get_browser(ip_tab=False, headless=True, disable_password_filling=True)
80 | browser.get('https://github.com/blnkoff/adspower')
81 | profile.quit()
82 | ```
83 |
84 | Notice that you must not call quitting methods of `Playwright` library or `Selenium` after `profile.quit()`, since
85 | it calls these methods automatically. An attempt to do so will lead to the error.
86 |
87 | *Example of setting proxy and fingerprint*
88 |
89 | ```python
90 | from adspower.sync_api.playwright import Profile, Group
91 | from adspower import ProxyConfig, FingerprintConfig
92 |
93 | proxy = ProxyConfig(
94 | soft='other',
95 | type='http',
96 | host='xx.xx.x.xx',
97 | port=1000,
98 | user='username',
99 | password='password'
100 | )
101 |
102 | fingerprint = FingerprintConfig(
103 | ua='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.112 Safari/537.36'
104 | )
105 |
106 | group = Group.query(name='my_group')[0]
107 | profile = Profile.create(group=group, proxy_config=proxy, name='my_profile', fingerprint_config=fingerprint)
108 | ```
109 |
110 | There are extension categories, implemented as `Category` class. At the moment, it can`t be created, but can be retrieved.
111 | You can manually create extension category and used it for profile creation using API.
112 |
113 | *Example of querying category*
114 |
115 | ```python
116 | from adspower.sync_api.playwright import Profile, Category, Group
117 |
118 | category = Category.query(name='my_category')[0]
119 | group = Group.query(name='my_group')[0]
120 |
121 | profile = Profile.create(group=group, category=category)
122 | ```
123 |
124 | You can create anonymous profile that is deleted after last statement in context manager.
125 |
126 | *Example of anonymous profile*
127 | ```python
128 | from adspower.async_api.playwright import Profile, Group
129 |
130 | async def main() -> None:
131 | my_group = (await Group.query(name='my_group'))[0]
132 | profile = await Profile.anonymous(group=my_group)
133 |
134 | async with profile as browser:
135 | page = browser.pages[0]
136 | await page.goto('https://www.google.com')
137 | ```
138 |
139 | Each API entity, such as Profile, Group and Category, pretty formatted, can be compared and converted to dict
140 |
141 | *Example 1*
142 |
143 | ```python
144 | from adspower.sync_api.playwright import Category
145 |
146 | category = Category.query(name='my_category')[0]
147 | print(category)
148 | ```
149 |
150 | ```markdown
151 | Category(id=10515; name=my_category)
152 | ```
153 |
154 | *Example 2*
155 |
156 | ```python
157 | from adspower.sync_api.playwright import Profile, Group
158 |
159 | group = Group.query(name='my_group')[0]
160 | profile_created = Profile.create(group=group)
161 |
162 | profile_queried = Profile.query(id_=profile_created.id)
163 | print(profile_queried == profile_created)
164 | ```
165 |
166 | ```python
167 | True
168 | ```
169 |
170 | *Example 3*
171 | ```python
172 | from adspower.sync_api.playwright import Category
173 |
174 | category = Category.query(name='my_category')[0]
175 | print(category.to_dict())
176 | ```
177 |
178 | ```json
179 | {
180 | "id": 10515,
181 | "name": "my_category",
182 | "remark": "category remark"
183 | }
184 | ```
185 |
186 | # Installing adspower
187 | To install the package from PyPi you can use that:
188 |
189 | ```sh
190 | pip install adspower
191 | ```
192 |
193 | You will probably want to use the pacakge with `Selenium` or `Playwright`. You can install it as extra-package:
194 |
195 | ```sh
196 | pip install adspower[playwright]
197 | ```
198 |
199 | ```sh
200 | pip install adspower[selenium]
201 | ```
202 |
--------------------------------------------------------------------------------
/adspower/__init__.py:
--------------------------------------------------------------------------------
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.sync_api.selenium import Profile, Group
9 | >>> my_group = Group.query(name='my_group')[0]
10 | >>> profile = Profile.create(group=my_group, name='my_profile')
11 | >>>
12 | >>> with profile as browser:
13 | >>> browser.get('https://github.com/blnkoff/adspower')
14 | :copyright: (c) 2023 by Alexey
15 | :license: Apache 2.0, see LICENSE for more details.
16 | """
17 |
18 | from adspower.types import FingerprintConfig, ProxyConfig
19 |
--------------------------------------------------------------------------------
/adspower/_api_entity.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 | from typing import Mapping, MutableMapping
3 |
4 |
5 | class _APIEntity(ABC):
6 | @property
7 | @abstractmethod
8 | def id(self) -> int | str:
9 | pass
10 |
11 | @property
12 | @abstractmethod
13 | def name(self) -> str:
14 | pass
15 |
16 | @abstractmethod
17 | def to_dict(self) -> Mapping | MutableMapping:
18 | pass
19 |
20 | def __str__(self):
21 | str_ = f'{self.__class__.__name__}(id={self.id}; name={self.name})'
22 | return str_
23 |
24 | def __eq__(self, other):
25 | return isinstance(other, _APIEntity) and self.to_dict() == other.to_dict()
26 |
--------------------------------------------------------------------------------
/adspower/_base_category.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 | from typing import Self, Optional, Any
3 | from adspower._api_entity import _APIEntity
4 | from adspower.types import CategoryInfo, HandlingTuple
5 |
6 |
7 | class _BaseCategory(_APIEntity, ABC):
8 |
9 | def __init__(self, id_: int, name: str | None, remark: str | None):
10 | self.__remark = remark
11 | self.__name = name
12 | self.__id = id_
13 |
14 | @property
15 | def id(self) -> int:
16 | """
17 | :return: Id of the category
18 | """
19 | return self.__id
20 |
21 | @property
22 | def name(self) -> str | None:
23 | """
24 | :return: Name of the category
25 | """
26 | return self.__name
27 |
28 | @property
29 | def remark(self) -> str | None:
30 | """
31 | :return: Description of the category
32 | """
33 | return self.__remark
34 |
35 | def to_dict(self) -> CategoryInfo:
36 | """
37 | Converts the Category instance to the dictionary containing info about extension category
38 | :return: Dictionary containing info about extension category
39 | """
40 | return CategoryInfo(
41 | id=self.id,
42 | name=self.name,
43 | remark=self.remark
44 | )
45 |
46 | @classmethod
47 | @abstractmethod
48 | def query(cls, id_: Optional[int] = None, page: int = 1, page_size: int = 100) -> list[Self]:
49 | pass
50 |
51 | @classmethod
52 | def _query(cls, id_: Optional[int] = None, name: Optional[str] = None, page: int = 1, page_size: int = 100) -> HandlingTuple:
53 | path = '/api/v1/application/list'
54 | data = {
55 | 'page': page,
56 | 'page_size': page_size,
57 | }
58 |
59 | args = {
60 | 'url': path,
61 | 'params': data,
62 | 'error_msg': 'Querying categories is failed'
63 | }
64 |
65 | def handler(response: dict[str, Any]) -> list[Self]:
66 | categories = []
67 | for category_info in response['list']:
68 | if (not id_ or category_info['id'] == id_) and (not name or category_info['name'] == name):
69 | categories.append(cls(int(category_info['id']), category_info['name'], category_info['remark']))
70 | if id_:
71 | break
72 |
73 | return categories
74 |
75 | return args, handler
76 |
--------------------------------------------------------------------------------
/adspower/_base_group.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 | from typing import Optional, Any
3 | from typing import Self
4 | from adspower.types import GroupInfo
5 | from adspower._api_entity import _APIEntity
6 | from adspower.types import HandlingTuple
7 | from adspower.utils import _convert_json
8 |
9 |
10 | class _BaseGroup(_APIEntity, ABC):
11 | def __init__(self, id_: int, name: str, remark: str | None) -> None:
12 | self.__id = id_
13 | self.__name = name
14 | self.__remark = remark
15 |
16 | def to_dict(self) -> GroupInfo:
17 | """
18 | Converts the Group instance to the dictionary containing info about group
19 | :return: Dictionary containing info about group
20 | """
21 | return GroupInfo(
22 | group_id=self.id,
23 | group_name=self.name,
24 | remark=self.remark
25 | )
26 |
27 | @classmethod
28 | @abstractmethod
29 | def create(cls, group_name: str, remark: Optional[str] = None) -> Self:
30 | pass
31 |
32 | @classmethod
33 | def _create(cls, group_name: str, remark: Optional[str] = None) -> HandlingTuple:
34 | path = '/api/v1/group/create'
35 |
36 | data = {'group_name': group_name, 'remark': remark}
37 |
38 | args = {
39 | 'url': path,
40 | 'json': data,
41 | 'error_msg': 'Creating group is failed'
42 | }
43 |
44 | def handler(response: dict[str, Any]) -> Self:
45 | response = _convert_json(response)
46 | return cls(id_=response['group_id'], name=response['group_name'], remark=response['remark'])
47 |
48 | return args, handler
49 |
50 | @classmethod
51 | @abstractmethod
52 | def query(
53 | cls,
54 | name: Optional[str] = None,
55 | profile_id: Optional[str] = None,
56 | page_size: Optional[int] = 100,
57 | ) -> list[Self]:
58 | pass
59 |
60 | @classmethod
61 | def _query(
62 | cls,
63 | name: Optional[str] = None,
64 | profile_id: Optional[str] = None,
65 | page_size: Optional[int] = 100,
66 | ) -> HandlingTuple:
67 | path = '/api/v1/group/list'
68 |
69 | params = {
70 | 'group_name': name,
71 | 'profile_id': profile_id,
72 | 'page_size': page_size,
73 | }
74 |
75 | args = {
76 | 'url': path,
77 | 'params': params,
78 | 'error_msg': 'The group query is failed'
79 | }
80 |
81 | def handler(response: dict[str, Any]) -> list[Self]:
82 | groups = []
83 | for info in response['list']:
84 | info = _convert_json(info)
85 | groups.append(cls(id_=int(info['group_id']), name=info['group_name'], remark=info['remark']))
86 |
87 | return groups
88 |
89 | return args, handler
90 |
91 | @property
92 | def id(self) -> int:
93 | """
94 | :return: Id of the group
95 | """
96 | return self.__id
97 |
98 | @property
99 | def name(self) -> str:
100 | """
101 | :return: Name of the group
102 | """
103 | return self.__name
104 |
105 | @property
106 | def remark(self) -> str | None:
107 | """
108 | :return: Description of the group
109 | """
110 | return self.__remark
111 |
112 | @abstractmethod
113 | def update(self, name: Optional[str] = None, remark: Optional[str] = None) -> None:
114 | pass
115 |
116 | def _update(self, name: Optional[str] = None, remark: Optional[str] = None) -> HandlingTuple:
117 | path = '/api/v1/group/update'
118 |
119 | if not name:
120 | name = self.name
121 |
122 | data = {"group_id": self.id, "group_name": name, "remark": remark}
123 | args = {
124 | 'url': path,
125 | 'json': data,
126 | 'error_msg': 'Updating group is failed'
127 | }
128 |
129 | def handler() -> None:
130 | self.__name = name if name else self.__name
131 | self.__remark = remark if remark else self.__remark
132 |
133 | return args, handler
134 |
--------------------------------------------------------------------------------
/adspower/_base_http_client.py:
--------------------------------------------------------------------------------
1 | import time
2 | from abc import ABC, abstractmethod
3 | from typing import Any, Optional, Callable, ClassVar, Union
4 | from httpx import USE_CLIENT_DEFAULT, Response
5 | from httpx._client import UseClientDefault
6 | from httpx._types import (URLTypes, RequestContent, RequestData, RequestFiles, QueryParamTypes, HeaderTypes,
7 | CookieTypes,
8 | AuthTypes, TimeoutTypes, RequestExtensions)
9 | from adspower.exceptions import InvalidPortError, InternalAPIError, ExceededQPSError, APIRefusedError, ZeroResponseError
10 |
11 |
12 | class _BaseHTTPClient(ABC):
13 | _request_availability = 0
14 | _delay: ClassVar[float] = 0.9
15 | _timeout = 5.0
16 |
17 | def __init__(self, port: int = 50325):
18 | self._port = port
19 | self._api_url = f'http://local.adspower.net:{self._port}'
20 |
21 | @property
22 | def api_url(self) -> str:
23 | return self._api_url
24 |
25 | @staticmethod
26 | def _validate_response(response: Response, error_msg: str) -> None:
27 | response.raise_for_status()
28 | request = response.request
29 | response_json = response.json()
30 |
31 | if response_json.get('message'):
32 | raise InternalAPIError(request=request, response=response_json)
33 |
34 | if response_json['code'] != 0:
35 | if 'Too many request per second, please check' in response_json['msg']:
36 | raise ExceededQPSError
37 | elif 'This feature is only available in paid subscriptions' in response_json['msg']:
38 | raise APIRefusedError
39 | else:
40 | raise ZeroResponseError(error_msg, request, response_json)
41 |
42 | @classmethod
43 | def set_delay(cls, value: float) -> None:
44 | """
45 | Sets the delay between requests
46 | :param value: Delay in seconds
47 | :return: None
48 | """
49 | if isinstance(value, Union[float, int]):
50 | cls._delay = value
51 | else:
52 | raise TypeError('Delay must be a float')
53 |
54 | @classmethod
55 | def set_timeout(cls, value: float) -> None:
56 | """
57 | Sets the timeout of the request
58 | :param value: Timeout in seconds
59 | :return: None
60 | """
61 | if isinstance(value, Union[float, int]):
62 | cls._timeout = value
63 | else:
64 | raise TypeError('Timeout must be a float')
65 |
66 | @classmethod
67 | def set_port(cls, value: int) -> None:
68 | """
69 | Sets the port of the client. Use it only when your Local API has non-default port.
70 | :param value: Port to be set
71 | :return: None
72 | """
73 | if 1 <= value <= 65535:
74 | cls._port = value
75 | cls._api_url = f'http://local.adspower.net:{value}'
76 | else:
77 | raise InvalidPortError(value)
78 |
79 | @classmethod
80 | def available(cls) -> bool:
81 | """
82 | Checks if the client is available.
83 | :return: True if client is not locked due to delay, False otherwise
84 | """
85 | current_time = time.time()
86 | return cls._request_availability < current_time
87 |
88 | @staticmethod
89 | @abstractmethod
90 | def _delay_request(func: Callable) -> Callable:
91 | pass
92 |
93 | @staticmethod
94 | @abstractmethod
95 | def _handle_request(func: Callable) -> Callable:
96 | pass
97 |
98 | @property
99 | def port(self) -> int:
100 | return self._port
101 |
102 | @_handle_request
103 | @_delay_request
104 | @abstractmethod
105 | def post(
106 | self,
107 | url: URLTypes,
108 | *,
109 | error_msg: str,
110 | content: Optional[RequestContent] = None,
111 | data: Optional[RequestData] = None,
112 | files: Optional[RequestFiles] = None,
113 | json: Optional[Any] = None,
114 | params: Optional[QueryParamTypes] = None,
115 | headers: Optional[HeaderTypes] = None,
116 | cookies: Optional[CookieTypes] = None,
117 | auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
118 | follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
119 | timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
120 | extensions: Optional[RequestExtensions] = None,
121 | ) -> Response:
122 | pass
123 |
124 | @_handle_request
125 | @_delay_request
126 | @abstractmethod
127 | def get(
128 | self,
129 | url: URLTypes,
130 | *,
131 | error_msg: str,
132 | params: QueryParamTypes | None = None,
133 | headers: HeaderTypes | None = None,
134 | cookies: CookieTypes | None = None,
135 | auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
136 | follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
137 | timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
138 | extensions: RequestExtensions | None = None,
139 | ) -> Response:
140 | pass
141 |
--------------------------------------------------------------------------------
/adspower/_base_profile_api.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from typing import Optional, Self, Any
3 | from abc import ABC, abstractmethod
4 | from adspower._api_entity import _APIEntity
5 | from ._base_group import _BaseGroup as Group
6 | from ._base_category import _BaseCategory as Category
7 | from adspower.types import (ProxyConfig, Cookies, FingerprintConfig, RepeatConfigType, ProfileInfo,
8 | UserSort, IpChecker, HandlingTuple)
9 | from .exceptions import InvalidProxyConfig
10 |
11 |
12 | class _BaseProfileAPI(_APIEntity, ABC):
13 |
14 | def __init__(
15 | self,
16 | id_: str,
17 | serial_number: int,
18 | name: str | None,
19 | group: Group,
20 | domain_name: str | None,
21 | username: str | None,
22 | remark: str | None,
23 | created_time: datetime,
24 | category: Category | None,
25 | ip: str | None,
26 | ip_country: str | None,
27 | ip_checker: IpChecker,
28 | fakey: str | None,
29 | password: str | None,
30 | last_open_time: datetime | None
31 | ):
32 | self.__id = id_
33 |
34 | self._serial_number = serial_number
35 | self._name = name
36 | self._group = group
37 | self._domain_name = domain_name
38 | self._username = username
39 | self._remark = remark
40 | self._created_time = created_time
41 | self._ip = ip
42 | self._ip_country = ip_country
43 | self._password = password
44 | self._ip_checker = ip_checker
45 | self._category = category
46 | self._fakey = fakey
47 | self._last_open_time = last_open_time
48 |
49 | self._browser = None
50 |
51 | @staticmethod
52 | @abstractmethod
53 | def _get_init_args(response: dict[str, Any]) -> dict[str, Any]:
54 | pass
55 |
56 | @classmethod
57 | @abstractmethod
58 | def create(
59 | cls,
60 | group: Group,
61 | name: Optional[str] = None,
62 | domain_name: Optional[str] = None,
63 | open_urls: Optional[list[str]] = None,
64 | repeat_config: Optional[RepeatConfigType] = None,
65 | username: Optional[str] = None,
66 | password: Optional[str] = None,
67 | fakey: Optional[str] = None,
68 | cookies: Optional[Cookies] = None,
69 | ignore_cookie_error: bool = False,
70 | ip: Optional[str] = None,
71 | ip_country: Optional[str] = None,
72 | region: Optional[str] = None,
73 | city: Optional[str] = None,
74 | remark: Optional[str] = None,
75 | ip_checker: Optional[IpChecker] = None,
76 | category: Optional[Category] = None,
77 | proxy_config: Optional[ProxyConfig] = None,
78 | fingerprint_config: Optional[FingerprintConfig] = None
79 | ) -> Self:
80 | pass
81 |
82 | @classmethod
83 | def _create(
84 | cls,
85 | group: Group,
86 | name: Optional[str] = None,
87 | domain_name: Optional[str] = None,
88 | open_urls: Optional[list[str]] = None,
89 | repeat_config: Optional[RepeatConfigType] = None,
90 | username: Optional[str] = None,
91 | password: Optional[str] = None,
92 | fakey: Optional[str] = None,
93 | cookies: Optional[Cookies] = None,
94 | ignore_cookie_error: bool = False,
95 | ip: Optional[str] = None,
96 | ip_country: Optional[str] = None,
97 | region: Optional[str] = None,
98 | city: Optional[str] = None,
99 | remark: Optional[str] = None,
100 | ip_checker: Optional[IpChecker] = None,
101 | category: Optional[Category] = None,
102 | proxy_config: Optional[ProxyConfig] = None,
103 | fingerprint_config: Optional[FingerprintConfig] = None
104 | ) -> HandlingTuple:
105 | path = '/api/v1/user/create'
106 |
107 | if not proxy_config:
108 | proxy_config = {'proxy_soft': 'no_proxy'}
109 | else:
110 | proxy_config = cls.__parse_proxy_config(proxy_config)
111 |
112 | data = {
113 | 'group_id': group.id,
114 | 'name': name,
115 | 'domain_name': domain_name,
116 | 'open_urls': open_urls,
117 | 'username': username,
118 | 'repeat_config': repeat_config,
119 | 'password': password,
120 | 'fakey': fakey,
121 | 'cookie': cookies,
122 | 'ignore_cookie_error': ignore_cookie_error,
123 | 'ip': ip,
124 | 'country': ip_country,
125 | 'region': region,
126 | 'city': city,
127 | 'remark': remark,
128 | 'ip_checker': ip_checker,
129 | 'sys_app_cate_id': category.id if category else 0,
130 | 'user_proxy_config': proxy_config,
131 | 'fingerprint_config': fingerprint_config
132 | }
133 |
134 | args = {
135 | 'url': path,
136 | 'json': data,
137 | 'error_msg': 'The profile creation is failed'
138 | }
139 |
140 | def handler(response: dict[str, Any]) -> list[Self]:
141 | return cls.query(group=group, id_=response['id'])
142 |
143 | return args, handler
144 |
145 | @classmethod
146 | @abstractmethod
147 | def query(
148 | cls,
149 | group: Optional[Group] = None,
150 | id_: Optional[str] = None,
151 | serial_number: Optional[int] = None,
152 | user_sort: Optional[UserSort] = None,
153 | page: int = 1,
154 | page_size: int = 100,
155 | ) -> list[Self]:
156 | pass
157 |
158 | @classmethod
159 | def _query(
160 | cls,
161 | group: Optional[Group] = None,
162 | id_: Optional[str] = None,
163 | name: Optional[str] = None,
164 | serial_number: Optional[int] = None,
165 | user_sort: Optional[UserSort] = None,
166 | page: int = 1,
167 | page_size: int = 100,
168 | ) -> HandlingTuple:
169 | path = '/api/v1/user/list'
170 |
171 | data = {
172 | 'group_id': group.id if group else None,
173 | 'user_id': id_,
174 | 'serial_number': serial_number,
175 | 'user_sort': user_sort,
176 | 'page_size': page_size,
177 | 'page': page
178 | }
179 |
180 | args = {
181 | 'url': path,
182 | 'params': data,
183 | 'error_msg': 'Querying the profile is failed'
184 | }
185 |
186 | def handler(response: dict[str, Any]) -> list[Self]:
187 | profiles = []
188 | for profile_info in response['list']:
189 | profile_info = cls._get_init_args(profile_info)
190 |
191 | if name is None or profile_info['name'] == name:
192 | profiles.append(cls(**profile_info))
193 | return profiles
194 |
195 | return args, handler
196 |
197 | @classmethod
198 | @abstractmethod
199 | def delete_cache(
200 | cls
201 | ) -> None:
202 | pass
203 |
204 | @classmethod
205 | def _delete_cache(
206 | cls
207 | ) -> HandlingTuple:
208 | path = '/api/v1/user/delete-cache'
209 | args = {
210 | 'url': path,
211 | 'error_msg': 'Deleting cache is failed'
212 | }
213 |
214 | def handler() -> None:
215 | pass
216 |
217 | return args, handler
218 |
219 | @property
220 | def id(self) -> str:
221 | """
222 | :return: Id of the profile
223 | """
224 | return self.__id
225 |
226 | @property
227 | def serial_number(self) -> int:
228 | """
229 | :return: Serial number of the profile
230 | """
231 | return self._serial_number
232 |
233 | @property
234 | def name(self) -> str | None:
235 | """
236 | :return: Name of the profile
237 | """
238 | return self._name
239 |
240 | @property
241 | def group(self) -> Group:
242 | """
243 | :return: Group where the profile is located
244 | """
245 | return self._group
246 |
247 | @property
248 | def domain_name(self) -> str | None:
249 | """
250 | :return: Domain name, such as facebook.com, amazon.com... Will open when getting the browser
251 | """
252 | return self._domain_name
253 |
254 | @property
255 | def username(self) -> str | None:
256 | """
257 | :return: Username for the domain name (e.g. facebook.com, amazon)
258 | """
259 | return self._username
260 |
261 | @property
262 | def remark(self) -> str | None:
263 | """
264 | :return: Description of the profile
265 | """
266 | return self._remark
267 |
268 | @property
269 | def created_time(self) -> datetime:
270 | """
271 | :return: Creation time of the profile
272 | """
273 | return self._created_time
274 |
275 | @property
276 | def ip(self) -> str | None:
277 | """
278 | :return: Proxy IP used for an account to log in. None when proxy software is not lumauto or oxylabs.
279 | """
280 | return self._ip
281 |
282 | @property
283 | def ip_country(self) -> str | None:
284 | """
285 | :return: Country of the proxy ip
286 | """
287 | return self._ip_country
288 |
289 | @property
290 | def ip_checker(self) -> IpChecker:
291 | """
292 | :return: IP checker for the profile. It can be 'ip2location' or 'ipapi'
293 | """
294 | return self._ip_checker
295 |
296 | @property
297 | def password(self) -> str | None:
298 | """
299 | :return: Password for the domain name (e.g. facebook.com, amazon)
300 | """
301 | return self._password
302 |
303 | @property
304 | def last_open_time(self) -> datetime | None:
305 | """
306 | :return: Last open time of the profile
307 | """
308 | return self._last_open_time
309 |
310 | @property
311 | def category(self) -> Category | None:
312 | """
313 | :return: Extension category of the profile
314 | """
315 | return self._category
316 |
317 | @property
318 | def fakey(self) -> str | None:
319 | """
320 | :return: 2FA-key for the domain name (e.g. facebook.com, amazon).
321 | This applies to online 2FA code generator, which works similarly to authenticators.
322 | """
323 | return self._fakey
324 |
325 | def to_dict(self) -> ProfileInfo:
326 | """
327 | Converts the Profile/ProfileAPI instance to the dictionary containing info about profile
328 | :return: Dictionary containing info about profile
329 | """
330 | return ProfileInfo(
331 | profile_id=self.id,
332 | serial_number=self.serial_number,
333 | name=self.name,
334 | group_id=self.group.id,
335 | group_name=self.group.name,
336 | domain_name=self.domain_name,
337 | username=self.username,
338 | remark=self.remark,
339 | category_id=category.id if (category := self.category) else None,
340 | created_time=int(self.created_time.timestamp()),
341 | ip=self.ip,
342 | ip_country=self.ip_country,
343 | ip_checker=self.ip_checker,
344 | fakey=self.fakey,
345 | password=self.password
346 | )
347 |
348 | @staticmethod
349 | def __parse_proxy_config(proxy_config: ProxyConfig | None) -> dict[str, Any] | None:
350 | if proxy_config:
351 | try:
352 | parsed_proxy = {
353 | 'proxy_soft': proxy_config['soft'],
354 | 'proxy_type': proxy_config['type'],
355 | 'proxy_host': proxy_config['host'],
356 | 'proxy_port': str(proxy_config['port']),
357 | 'proxy_user': proxy_config['user'],
358 | 'proxy_password': proxy_config['password']
359 | }
360 | return parsed_proxy
361 | except (KeyError, TypeError):
362 | raise InvalidProxyConfig(proxy_config)
363 | else:
364 | return None
365 |
366 | @abstractmethod
367 | def _get_browser(
368 | self,
369 | ip_tab: bool = True,
370 | new_first_tab: bool = True,
371 | launch_args: Optional[list[str]] = None,
372 | headless: bool = False,
373 | disable_password_filling: bool = False,
374 | clear_cache_after_closing: bool = False,
375 | enable_password_saving: bool = False
376 | ) -> HandlingTuple:
377 | profile_id = self.id
378 | path = '/api/v1/browser/start'
379 |
380 | params = {
381 | 'user_id': profile_id,
382 | 'ip_tab': int(ip_tab),
383 | 'new_first_tab': int(new_first_tab),
384 | 'launch_args': launch_args,
385 | 'headless': int(headless),
386 | 'disable_password_filling': int(disable_password_filling),
387 | 'clear_cache_after_closing': int(clear_cache_after_closing),
388 | 'enable_password_saving': int(enable_password_saving)
389 | }
390 |
391 | args = {
392 | 'url': path,
393 | 'params': params,
394 | 'error_msg': 'Getting browser is failed'
395 | }
396 |
397 | def handler() -> None:
398 | self._profile_id = profile_id
399 | self.__last_open_time = datetime.now()
400 |
401 | return args, handler
402 |
403 | def _update(
404 | self,
405 | name: Optional[str] = None,
406 | domain_name: Optional[str] = None,
407 | open_urls: Optional[list[str]] = None,
408 | username: Optional[str] = None,
409 | password: Optional[str] = None,
410 | fakey: Optional[str] = None,
411 | cookies: Optional[Cookies] = None,
412 | ignore_cookie_error: bool = False,
413 | ip: Optional[str] = None,
414 | ip_country: Optional[str] = None,
415 | region: Optional[str] = None,
416 | city: Optional[str] = None,
417 | remark: Optional[str] = None,
418 | category: Optional[Category] = None,
419 | proxy_config: Optional[ProxyConfig] = None,
420 | fingerprint_config: Optional[FingerprintConfig] = None
421 | ) -> HandlingTuple:
422 | path = '/api/v1/user/update'
423 | proxy_config = self.__parse_proxy_config(proxy_config)
424 |
425 | data = {
426 | 'user_id': self.id,
427 | 'name': name,
428 | 'domain_name': domain_name,
429 | 'open_urls': open_urls,
430 | 'username': username,
431 | 'password': password,
432 | 'fakey': fakey,
433 | 'cookie': cookies,
434 | 'ignore_cookie_error': int(ignore_cookie_error),
435 | 'ip': ip,
436 | 'country': ip_country,
437 | 'region': region,
438 | 'sys_app_cate_id': category.id if category else None,
439 | 'remark': remark,
440 | 'city': city,
441 | 'user_proxy_config': proxy_config,
442 | 'fingerprint_config': fingerprint_config
443 | }
444 |
445 | args = {
446 | 'url': path,
447 | 'json': data,
448 | 'error_msg': 'Updating profile is failed'
449 | }
450 |
451 | def handler() -> None:
452 | data.pop('city')
453 | data.pop('user_id')
454 | data.pop('user_proxy_config')
455 | data.pop('fingerprint_config')
456 | data.pop('ignore_cookie_error')
457 |
458 | data['ip_country'] = data.pop('country')
459 | data['category'] = Category.query(id_) if (id_ := data.pop('sys_app_cate_id')) else self.category
460 |
461 | for key, value in data.items():
462 | if value is not None:
463 | self.__setattr__(f'_{key}', value)
464 |
465 | return args, handler
466 |
467 | @abstractmethod
468 | def update(
469 | self,
470 | name: Optional[str] = None,
471 | domain_name: Optional[str] = None,
472 | open_urls: Optional[list[str]] = None,
473 | username: Optional[str] = None,
474 | password: Optional[str] = None,
475 | fakey: Optional[str] = None,
476 | cookies: Optional[Cookies] = None,
477 | ignore_cookie_error: bool = False,
478 | ip: Optional[str] = None,
479 | ip_country: Optional[str] = None,
480 | region: Optional[str] = None,
481 | city: Optional[str] = None,
482 | remark: Optional[str] = None,
483 | category: Optional[Category] = None,
484 | proxy_config: Optional[ProxyConfig] = None,
485 | fingerprint_config: Optional[FingerprintConfig] = None
486 | ) -> None:
487 | pass
488 |
489 | def _move(self, group: Group) -> HandlingTuple:
490 | path = '/api/v1/user/regroup'
491 |
492 | data = {
493 | 'user_ids': [self.id],
494 | 'group_id': group.id
495 | }
496 |
497 | args = {
498 | 'url': path,
499 | 'json': data,
500 | 'error_msg': f'Moving profile is failed. Profile ID: {self.id}. Group ID: {group.id}'
501 | }
502 |
503 | def handler() -> None:
504 | self._group = group
505 |
506 | return args, handler
507 |
508 | @abstractmethod
509 | def move(self, group: Group) -> None:
510 | pass
511 |
512 | def _active(self) -> HandlingTuple:
513 | path = '/api/v1/browser/active'
514 |
515 | data = {
516 | 'user_id': self.id
517 | }
518 |
519 | args = {
520 | 'url': path,
521 | 'json': data,
522 | 'error_msg': 'Checking browser for activity is failed'
523 | }
524 |
525 | def handler(response: dict[str, Any]) -> bool:
526 | active = response['status'] == 'Active'
527 | return active
528 |
529 | return args, handler
530 |
531 | @abstractmethod
532 | def active(self) -> bool:
533 | pass
534 |
535 | def _delete(self) -> HandlingTuple:
536 | path = '/api/v1/user/delete'
537 |
538 | data = {'user_ids': [self.id]}
539 |
540 | args = {
541 | 'url': path,
542 | 'json': data,
543 | 'error_msg': 'The profile deletion is failed'
544 | }
545 |
546 | def handler() -> None: pass
547 |
548 | return args, handler
549 |
550 | @abstractmethod
551 | def delete(self) -> None:
552 | pass
553 |
554 | @abstractmethod
555 | def _quit(self) -> HandlingTuple:
556 | path = '/api/v1/browser/stop'
557 | profile_id = self.id
558 |
559 | params = {'user_id': profile_id}
560 | args = {
561 | 'url': path,
562 | 'params': params,
563 | 'error_msg': 'Quitting profile is failed. Profile can be already closed'
564 | }
565 |
566 | def handler() -> None: pass
567 |
568 | return args, handler
569 |
--------------------------------------------------------------------------------
/adspower/async_api/__init__.py:
--------------------------------------------------------------------------------
1 | from .group import Group
2 | from .category import Category
3 | from .http_client import HTTPClient
4 | from .profile_api import ProfileAPI
--------------------------------------------------------------------------------
/adspower/async_api/_base_profile.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 | from typing import Optional, AsyncContextManager
3 | from adspower.async_api import ProfileAPI, Group
4 | from contextlib import asynccontextmanager
5 |
6 |
7 | class _BaseProfile(ProfileAPI, ABC):
8 | async def __aenter__(self):
9 | browser = await self.get_browser()
10 | return browser
11 |
12 | async def __aexit__(self, exc_type, exc_val, exc_tb):
13 | await self.quit()
14 |
15 | @classmethod
16 | @asynccontextmanager
17 | async def anonymous(cls, group: Group) -> AsyncContextManager:
18 | profile = await cls.create(group=group)
19 | try:
20 | yield await profile.get_browser()
21 | finally:
22 | await profile.quit()
23 | await profile.delete()
24 |
25 | @property
26 | @abstractmethod
27 | def browser(self):
28 | pass
29 |
30 | @abstractmethod
31 | async def get_browser(
32 | self,
33 | ip_tab: bool = False,
34 | new_first_tab: bool = True,
35 | launch_args: Optional[list[str]] = None,
36 | headless: bool = False,
37 | disable_password_filling: bool = False,
38 | clear_cache_after_closing: bool = False,
39 | enable_password_saving: bool = False,
40 | close_tabs: bool = True,
41 | ):
42 | pass
43 |
44 | @abstractmethod
45 | def close_tabs(self) -> None:
46 | pass
47 |
48 | @abstractmethod
49 | async def quit(self) -> None:
50 | pass
51 |
--------------------------------------------------------------------------------
/adspower/async_api/category.py:
--------------------------------------------------------------------------------
1 | from typing import Self, Optional, ClassVar
2 | from adspower._base_category import _BaseCategory
3 | from .http_client import HTTPClient
4 |
5 |
6 | class Category(_BaseCategory):
7 | _client: ClassVar[type[HTTPClient]] = HTTPClient
8 |
9 | def __init__(self, id_: int, name: str | None, remark: str | None):
10 | """
11 | The class interacting with extension categories. You can use an extension category to specify extensions for
12 | profiles
13 | :param id_: Id of the extension category
14 | :param name: Name of the extension category
15 | :param remark: Description of the extension category
16 | """
17 | super().__init__(id_, name, remark)
18 |
19 | @classmethod
20 | async def query(cls, id_: Optional[int] = None, name: Optional[str] = None, page: int = 1, page_size: int = 100) -> list[Self]:
21 | """
22 | Query the list of extension categories.
23 | :param id_: Id of the extension category
24 | :param name: Name of the extension category
25 | :param page: Number of page in returning list. Default value - 1.
26 | Numbers of elements in returning list is equal to the range(page, page + page_size)
27 | :param page_size: Maximum length of returning list. Default value - 100
28 |
29 | :return: List of categories
30 | """
31 | http_client = cls._client
32 | args, handler = cls._query(id_, name, page, page_size)
33 |
34 | async with http_client() as client:
35 | response = (await client.get(**args)).json()['data']
36 |
37 | return handler(response)
38 |
--------------------------------------------------------------------------------
/adspower/async_api/group.py:
--------------------------------------------------------------------------------
1 | from typing import ClassVar, Optional
2 | from typing import Self
3 | from adspower._base_group import _BaseGroup
4 | from .http_client import HTTPClient
5 |
6 |
7 | class Group(_BaseGroup):
8 | _client: ClassVar[type[HTTPClient]] = HTTPClient
9 |
10 | def __init__(self, id_: int, name: str, remark: str | None) -> None:
11 | """
12 | The class interacting with groups. You can use groups to combine profiles
13 | :param id_: Id of the group
14 | :param name: Name of the group
15 | :param remark: Description of the group
16 | """
17 | super().__init__(id_, name, remark)
18 |
19 | @classmethod
20 | async def create(cls, name: str, remark: Optional[str] = None) -> Self:
21 | """
22 | Create a new group
23 |
24 | :param name: Name of the group
25 | :param remark: Description of the group
26 | :return: Instance of the Group
27 | """
28 | http_client = cls._client
29 | args, handler = cls._create(name, remark)
30 | async with http_client() as client:
31 | response = (await client.post(**args)).json()['data']
32 |
33 | return handler(response)
34 |
35 | @classmethod
36 | async def query(
37 | cls,
38 | name: Optional[str] = None,
39 | profile_id: Optional[str] = None,
40 | page_size: Optional[int] = 100,
41 | ) -> list[Self]:
42 | """
43 | Query the list of groups.
44 |
45 | :param name: Name of the group
46 | :param profile_id: ID of existing profile in AdsPower
47 | :param page_size: Maximum length of returning list. Default value - 100
48 |
49 | :return: List of groups
50 | """
51 | http_client = cls._client
52 | args, handler = cls._query(name, profile_id, page_size)
53 |
54 | async with http_client() as client:
55 | response = (await client.get(**args)).json()['data']
56 |
57 | return handler(response)
58 |
59 | async def update(self, name: Optional[str] = None, remark: Optional[str] = None) -> None:
60 | """
61 | Updates a group name or description for the existing group
62 |
63 | :param name: Name of the group
64 | :param remark: Description of the group
65 | :return: None
66 | """
67 | http_client = self._client
68 | args, handler = self._update(name, remark)
69 |
70 | async with http_client() as client:
71 | (await client.post(**args))
72 |
73 | handler()
74 |
--------------------------------------------------------------------------------
/adspower/async_api/http_client.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import time
3 | from functools import wraps
4 | from typing import Any, Optional, Callable, Awaitable
5 | from httpx import AsyncClient, USE_CLIENT_DEFAULT, Response, ConnectError, InvalidURL
6 | from httpx._client import UseClientDefault
7 | from httpx._types import (URLTypes, RequestContent, RequestData, RequestFiles, QueryParamTypes, HeaderTypes,
8 | CookieTypes,
9 | AuthTypes, TimeoutTypes, RequestExtensions)
10 | from urllib3.exceptions import MaxRetryError, NewConnectionError
11 | from adspower.exceptions import UnavailableAPIError
12 | from adspower._base_http_client import _BaseHTTPClient
13 |
14 |
15 | class HTTPClient(AsyncClient, _BaseHTTPClient):
16 | def __init__(self):
17 | """
18 | HTTPClient is a wrapper around httpx's AsyncClient to make it easier to perform requests against Local API.
19 | You can customize internal behaviour of the package using HTTPClient`s methods, such as `set_timeout`, `set_port`,
20 | `set_delay` and get information about client availability using `available`
21 | """
22 | port = 50325
23 | _BaseHTTPClient.__init__(self, port)
24 | AsyncClient.__init__(self, base_url=self._api_url, timeout=self._timeout)
25 |
26 | @staticmethod
27 | def _delay_request(func: Callable[..., Awaitable[Response]]):
28 | @wraps(func)
29 | async def wrapper(*args, **kwargs) -> Any:
30 | current_time = time.time()
31 |
32 | if _BaseHTTPClient.available():
33 | result = await func(*args, **kwargs)
34 | _BaseHTTPClient._request_availability = time.time() + HTTPClient._delay
35 | else:
36 | await asyncio.sleep(HTTPClient._request_availability - current_time)
37 | _BaseHTTPClient._request_availability = time.time() + HTTPClient._delay
38 | result = await func(*args, **kwargs)
39 |
40 | return result
41 |
42 | return wrapper
43 |
44 | @staticmethod
45 | def _handle_request(func: Callable[..., Awaitable[Response]]):
46 | @wraps(func)
47 | async def wrapper(
48 | self: "HTTPClient",
49 | *args,
50 | **kwargs,
51 | ) -> Response:
52 | try:
53 | await super().get('/status')
54 | except (MaxRetryError, ConnectError, NewConnectionError, ConnectionRefusedError, InvalidURL):
55 | raise UnavailableAPIError(self._port)
56 | else:
57 | response = await func(self, *args, **kwargs)
58 | HTTPClient._validate_response(response, kwargs['error_msg'])
59 | return response
60 |
61 | return wrapper
62 |
63 | @_handle_request
64 | @_delay_request
65 | async def post(
66 | self,
67 | url: URLTypes,
68 | *,
69 | error_msg: str,
70 | content: Optional[RequestContent] = None,
71 | data: Optional[RequestData] = None,
72 | files: Optional[RequestFiles] = None,
73 | json: Optional[Any] = None,
74 | params: Optional[QueryParamTypes] = None,
75 | headers: Optional[HeaderTypes] = None,
76 | cookies: Optional[CookieTypes] = None,
77 | auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
78 | follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
79 | timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
80 | extensions: Optional[RequestExtensions] = None,
81 | ) -> Response:
82 | return await super().post(
83 | url=url,
84 | content=content,
85 | data=data,
86 | files=files,
87 | json=json,
88 | params=params,
89 | headers=headers,
90 | cookies=cookies,
91 | auth=auth,
92 | follow_redirects=follow_redirects,
93 | timeout=timeout,
94 | extensions=extensions
95 | )
96 |
97 | @_handle_request
98 | @_delay_request
99 | async def get(
100 | self,
101 | url: URLTypes,
102 | *,
103 | error_msg: str,
104 | params: QueryParamTypes | None = None,
105 | headers: HeaderTypes | None = None,
106 | cookies: CookieTypes | None = None,
107 | auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
108 | follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
109 | timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
110 | extensions: RequestExtensions | None = None,
111 | ) -> Response:
112 | return await super().get(
113 | url=url,
114 | params=params,
115 | headers=headers,
116 | cookies=cookies,
117 | auth=auth,
118 | follow_redirects=follow_redirects,
119 | timeout=timeout,
120 | extensions=extensions
121 | )
122 |
--------------------------------------------------------------------------------
/adspower/async_api/playwright/__init__.py:
--------------------------------------------------------------------------------
1 | from .profile import Profile, Group, Category
2 |
--------------------------------------------------------------------------------
/adspower/async_api/playwright/profile.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from typing import Optional, AsyncContextManager
3 | from adspower.async_api._base_profile import _BaseProfile
4 | from adspower.async_api.group import Group
5 | from adspower.async_api.category import Category
6 | from playwright.async_api import async_playwright, BrowserContext, Playwright, Browser
7 | from adspower.types import IpChecker
8 |
9 |
10 | class Profile(_BaseProfile):
11 | def __init__(
12 | self,
13 | id_: str,
14 | serial_number: int,
15 | name: str | None,
16 | group: Group,
17 | domain_name: str | None,
18 | username: str | None,
19 | remark: str | None,
20 | created_time: datetime,
21 | category: Category | None,
22 | ip: str | None,
23 | ip_country: str | None,
24 | ip_checker: IpChecker,
25 | fakey: str | None,
26 | password: str | None,
27 | last_open_time: datetime | None
28 | ):
29 | """
30 | The class interacting with profile management.
31 |
32 | :param id_: Profile id
33 | :param serial_number: Serial number of the profile
34 | :param name: Name of the profile
35 | :param group: Group where the profile is located
36 | :param domain_name: Domain name, such as facebook.com, amazon.com... Will open when getting the browser.
37 | :param username: If username duplication is allowed, leave here empty.
38 | :param remark: Description of the profile
39 | :param created_time: Creation time of the profile
40 | :param category: Extension category for the profile
41 | :param ip: Proxy IP used for an account to log in. Fill in when proxy software is lumauto or oxylabs.
42 | :param ip_country: Country or region your lumauto and oxylabs account belongs to. Without lumauto and oxylabs IP please enter country.
43 | :param ip_checker: IP checker for the profile. Choose from ['ip2location', 'ipapi']
44 | :param fakey: 2FA-key. This applies to online 2FA code generator, which works similarly to authenticators.
45 | :param password: If password duplication is allowed, leave here empty.
46 | :param last_open_time: Last open time of the profile
47 | """
48 | super().__init__(
49 | id_,
50 | serial_number,
51 | name,
52 | group,
53 | domain_name,
54 | username,
55 | remark,
56 | created_time,
57 | category,
58 | ip,
59 | ip_country,
60 | ip_checker,
61 | fakey,
62 | password,
63 | last_open_time,
64 | )
65 | self.__playwright: Playwright | None = None
66 | self.__browser_app: Browser | None = None
67 |
68 | @classmethod
69 | async def anonymous(cls, group: Group) -> AsyncContextManager[BrowserContext]:
70 | return super().anonymous(group)
71 |
72 | async def __aenter__(self) -> BrowserContext:
73 | return await super().__aenter__()
74 |
75 | @property
76 | def browser(self) -> BrowserContext:
77 | """
78 | :return: WebDriver connected to the profile if it's open, None otherwise
79 | """
80 | return self._browser
81 |
82 | async def get_browser(
83 | self,
84 | ip_tab: bool = True,
85 | new_first_tab: bool = True,
86 | launch_args: Optional[list[str]] = None,
87 | headless: bool = False,
88 | disable_password_filling: bool = False,
89 | clear_cache_after_closing: bool = False,
90 | enable_password_saving: bool = False,
91 | close_tabs: bool = True
92 | ) -> BrowserContext:
93 | """
94 | Get a BrowserContext connected to the profile
95 | :param ip_tab: Whether to open the ip detection page
96 | :param new_first_tab: Whether to use the new version of the ip detection page
97 | :param launch_args: Browser startup parameters. eg: --blink-settings=imagesEnabled=false:
98 | Prohibit image loading. --disable-notifications: Disable notifications
99 | :param headless: Whether to start the headless browser
100 | :param disable_password_filling: Whether to disable the function of filling password
101 | :param clear_cache_after_closing: Whether to delete the cache after closing the browser
102 | :param enable_password_saving: Whether to allow password saving
103 | :param close_tabs: Whether to close all startup tabs
104 | :return: BrowserContext instance
105 | """
106 | response = await self._get_browser(
107 | ip_tab,
108 | new_first_tab,
109 | launch_args,
110 | headless,
111 | disable_password_filling,
112 | clear_cache_after_closing,
113 | enable_password_saving,
114 | )
115 |
116 | playwright = self.__playwright = (await async_playwright().start())
117 | browser_app = await playwright.chromium.connect_over_cdp(f'http://localhost:{response["debug_port"]}')
118 | self.__browser_app = browser_app
119 | browser = self._browser = browser_app.contexts[0]
120 |
121 | if close_tabs:
122 | await self.close_tabs()
123 |
124 | return browser
125 |
126 | async def close_tabs(self) -> None:
127 | """
128 | Closes all tabs, exclude current tab
129 | :return: None
130 | """
131 | browser = self._browser
132 | new_page = await browser.new_page()
133 |
134 | for page in browser.pages:
135 | if not (page is new_page):
136 | await page.close()
137 |
138 | async def quit(self) -> None:
139 | """
140 | Quit the browser
141 | :return: None
142 | """
143 | await self._quit()
144 | await self._browser.close()
145 | await self.__browser_app.close()
146 | await self.__playwright.stop()
147 |
148 | self._browser = None
149 | self.__playwright = None
150 | self.__browser_app = None
151 |
--------------------------------------------------------------------------------
/adspower/async_api/profile_api.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from typing import Optional, ClassVar, Self, Any
3 | from abc import ABC, abstractmethod
4 | from .category import Category
5 | from .http_client import HTTPClient
6 | from .group import Group
7 | from adspower._base_profile_api import _BaseProfileAPI
8 | from adspower.types import (ProxyConfig, Cookies, FingerprintConfig, RepeatConfigType, BrowserResponse,
9 | UserSort, IpChecker)
10 | from adspower.utils import _convert_json
11 |
12 |
13 | class ProfileAPI(_BaseProfileAPI, ABC):
14 | _client: ClassVar[type[HTTPClient]] = HTTPClient
15 |
16 | def __init__(
17 | self,
18 | id_: str,
19 | serial_number: int,
20 | name: str | None,
21 | group: Group,
22 | domain_name: str | None,
23 | username: str | None,
24 | remark: str | None,
25 | created_time: datetime,
26 | category: Category | None,
27 | ip: str | None,
28 | ip_country: str | None,
29 | ip_checker: IpChecker,
30 | fakey: str | None,
31 | password: str | None,
32 | last_open_time: datetime | None
33 | ):
34 | """
35 | The class interacting with profile management. It doesn't interact with getting a browser to use it with Selenium or
36 | Playwright. If you want to interact with that, you need to use `Profile` class.
37 |
38 | :param id_: Profile id
39 | :param serial_number: Serial number of the profile
40 | :param name: Name of the profile
41 | :param group: Group where the profile is located
42 | :param domain_name: Domain name, such as facebook.com, amazon.com... Will open when getting the browser.
43 | :param username: Username for the domain name (e.g. facebook.com, amazon)
44 | :param remark: Description of the profile
45 | :param created_time: Creation time of the profile
46 | :param category: Extension category for the profile
47 | :param ip: Proxy IP used for an account to log in. Fill in when proxy software is lumauto or oxylabs.
48 | :param ip_country: Country or region your lumauto and oxylabs account belongs to. Without lumauto and oxylabs IP please enter country.
49 | :param ip_checker: IP checker for the profile. Choose from ['ip2location', 'ipapi']
50 | :param fakey: 2FA-key for the domain name (e.g. facebook.com, amazon).
51 | This applies to online 2FA code generator, which works similarly to authenticators.
52 | :param password: Password for the domain name (e.g. facebook.com, amazon)
53 | :param last_open_time: Last open time of the profile
54 | """
55 | _BaseProfileAPI.__init__(
56 | self,
57 | id_,
58 | serial_number,
59 | name, group,
60 | domain_name,
61 | username,
62 | remark,
63 | created_time,
64 | category,
65 | ip,
66 | ip_country,
67 | ip_checker,
68 | fakey,
69 | password,
70 | last_open_time
71 | )
72 |
73 | @staticmethod
74 | def _get_init_args(response: dict[str, Any]) -> dict[str, Any]:
75 | response = _convert_json(response)
76 |
77 | response['id_'] = response.pop('user_id')
78 | response['category'] = Category(id_=id_, name=None, remark=None) if (id_ := response.pop('sys_app_cate_id')) else None
79 |
80 | last_open_time = response['last_open_time']
81 | response['created_time'] = datetime.fromtimestamp(response['created_time'])
82 | response['last_open_time'] = datetime.fromtimestamp(last_open_time) if last_open_time else None
83 | response['ip_checker'] = response.pop('ipchecker')
84 |
85 | response['group'] = Group(id_=response.pop('group_id'), name=response.pop('group_name'), remark=None)
86 |
87 | response.pop('fbcc_proxy_acc_id')
88 |
89 | return response
90 |
91 | @classmethod
92 | async def create(
93 | cls,
94 | group: Group,
95 | name: Optional[str] = None,
96 | domain_name: Optional[str] = None,
97 | open_urls: Optional[list[str]] = None,
98 | repeat_config: Optional[RepeatConfigType] = None,
99 | username: Optional[str] = None,
100 | password: Optional[str] = None,
101 | fakey: Optional[str] = None,
102 | cookies: Optional[Cookies] = None,
103 | ignore_cookie_error: bool = False,
104 | ip: Optional[str] = None,
105 | ip_country: Optional[str] = None,
106 | region: Optional[str] = None,
107 | city: Optional[str] = None,
108 | remark: Optional[str] = None,
109 | ip_checker: Optional[IpChecker] = None,
110 | category: Optional[Category] = None,
111 | proxy_config: Optional[ProxyConfig] = None,
112 | fingerprint_config: Optional[FingerprintConfig] = None
113 | ) -> Self:
114 | """
115 | Create a new profile
116 |
117 | :param name: Name of the profile
118 | :param group: Group where the profile is located
119 | :param domain_name: Domain name, such as facebook.com, amazon.com... Will open when getting the browser.
120 | :param open_urls: Other urls when opening browser. If leaving it empty, will open the domain name url.
121 | :param repeat_config: Account deduplication. Default setting: Allow duplication.
122 | 0: Allow duplication;
123 | 2: Deduplication based on the account name/password;
124 | 3: Deduplication based on cookie;
125 | 4: Deduplication based on c_user (c_user is a specific tag for Facebook)
126 | :param username: Username for the domain name (e.g. facebook.com, amazon)
127 | :param remark: Description of the profile
128 | :param category: Extension category for the profile
129 | :param ip: Proxy IP used for an account to log in. Fill in when proxy software is lumauto or oxylabs.
130 | :param ip_country: Country or region your lumauto and oxylabs account belongs to.Without lumauto and oxylabs IP please enter country.
131 | :param region: State or province where account logged in.
132 | :param city: City where account logged in.
133 | :param ip_checker: IP checker for the profile. Choose from ['ip2location', 'ipapi']
134 | :param cookies: Cookies to be set when opening browser
135 | :param ignore_cookie_error: 0:When the cookie verification fails, the cookie format is incorrectly returned directly
136 | 1:When the cookie verification fails, filter out the data in the wrong format and keep the cookie in the correct format
137 | Only supports netspace
138 | :param fakey: 2FA-key for the domain name (e.g. facebook.com, amazon).
139 | This applies to online 2FA code generator, which works similarly to authenticators.
140 | :param password: Password for the domain name (e.g. facebook.com, amazon)
141 | :param proxy_config: Dictionary containing proxy info
142 | :param fingerprint_config: Dictionary containing fingerprint info
143 |
144 | :return: A Profile instance
145 | """
146 | http_client = cls._client
147 | args, handler = cls._create(
148 | group,
149 | name,
150 | domain_name,
151 | open_urls,
152 | repeat_config,
153 | username,
154 | password,
155 | fakey,
156 | cookies,
157 | ignore_cookie_error,
158 | ip,
159 | ip_country,
160 | region,
161 | city,
162 | remark,
163 | ip_checker,
164 | category,
165 | proxy_config,
166 | fingerprint_config
167 | )
168 |
169 | async with http_client() as client:
170 | response = (await client.post(**args)).json()['data']
171 |
172 | return (await handler(response))[0]
173 |
174 | @classmethod
175 | async def query(
176 | cls,
177 | group: Optional[Group] = None,
178 | id_: Optional[str] = None,
179 | name: Optional[str] = None,
180 | serial_number: Optional[int] = None,
181 | user_sort: Optional[UserSort] = None,
182 | page: int = 1,
183 | page_size: int = 100,
184 | ) -> list[Self]:
185 | """
186 | Query the list of profiles
187 | :param group: Group where profiles are located
188 | :param id_: Id of the profile
189 | :param name: Name of the profile
190 | :param serial_number: Serial number of the profile
191 | :param user_sort: User sorting. Can be sorted by the specified type, supporting
192 | three fields serial_number, last_open_time, created_time, and two values asc and desc.
193 | :param page: Number of page in returning list. Default value - 1.
194 | Numbers of elements in returning list is equal to the range(page, page + page_size)
195 | :param page_size: Maximum length of returning list. Default value - 100
196 | :return: List of profiles
197 | """
198 | http_client = cls._client
199 | args, handler = cls._query(
200 | group,
201 | id_,
202 | name,
203 | serial_number,
204 | user_sort,
205 | page,
206 | page_size
207 | )
208 |
209 | async with http_client() as client:
210 | response = (await client.get(**args)).json()['data']
211 |
212 | return handler(response)
213 |
214 | @classmethod
215 | async def delete_cache(cls) -> None:
216 | """
217 | Deletes cache of all profiles
218 | :return: None
219 | """
220 | http_client = cls._client
221 | args, _ = cls._delete_cache()
222 |
223 | async with http_client() as client:
224 | (await client.post(**args))
225 |
226 | async def _get_browser(
227 | self,
228 | ip_tab: bool = True,
229 | new_first_tab: bool = True,
230 | launch_args: Optional[list[str]] = None,
231 | headless: bool = False,
232 | disable_password_filling: bool = False,
233 | clear_cache_after_closing: bool = False,
234 | enable_password_saving: bool = False
235 | ) -> BrowserResponse:
236 | http_client = self._client
237 |
238 | args, handler = super()._get_browser(
239 | ip_tab,
240 | new_first_tab,
241 | launch_args,
242 | headless,
243 | disable_password_filling,
244 | clear_cache_after_closing,
245 | enable_password_saving
246 | )
247 | async with http_client() as client:
248 | response = (await client.get(**args)).json()['data']
249 |
250 | handler()
251 | return response
252 |
253 | async def update(
254 | self,
255 | name: Optional[str] = None,
256 | domain_name: Optional[str] = None,
257 | open_urls: Optional[list[str]] = None,
258 | username: Optional[str] = None,
259 | password: Optional[str] = None,
260 | fakey: Optional[str] = None,
261 | cookies: Optional[Cookies] = None,
262 | ignore_cookie_error: bool = False,
263 | ip: Optional[str] = None,
264 | ip_country: Optional[str] = None,
265 | region: Optional[str] = None,
266 | city: Optional[str] = None,
267 | remark: Optional[str] = None,
268 | category: Optional[Category] = None,
269 | proxy_config: Optional[ProxyConfig] = None,
270 | fingerprint_config: Optional[FingerprintConfig] = None
271 | ) -> None:
272 | """
273 | Update the profile
274 |
275 | :param name: Name of the profile
276 | :param domain_name: Domain name, such as facebook.com, amazon.com... Will open when getting the browser.
277 | :param open_urls: Other urls when opening browser. If leaving it empty, will open the domain name url.
278 | :param username: If username duplication is allowed, leave here empty.
279 | :param remark: Description of the profile
280 | :param category: Extension category for the profile
281 | :param ip: Proxy IP used for an account to log in. Fill in when proxy software is lumauto or oxylabs.
282 | :param ip_country: Country or region your lumauto and oxylabs account belongs to.Without lumauto and oxylabs IP please enter country.
283 | :param region: State or province where account logged in.
284 | :param city: City where account logged in.
285 | :param fakey: 2FA-key. This applies to online 2FA code generator, which works similarly to authenticators.
286 | :param cookies: Cookies to be set when opening browser
287 | :param ignore_cookie_error: 0:When the cookie verification fails, the cookie format is incorrectly returned directly
288 | 1:When the cookie verification fails, filter out the data in the wrong format and keep the cookie in the correct format
289 | Only supports netspace
290 | :param password: If password duplication is allowed, leave here empty.
291 | :param proxy_config: Dictionary containing proxy info
292 | :param fingerprint_config: Dictionary containing fingerprint info
293 | """
294 | http_client = self._client
295 | args, handler = self._update(
296 | name,
297 | domain_name,
298 | open_urls,
299 | username,
300 | password,
301 | fakey,
302 | cookies,
303 | ignore_cookie_error,
304 | ip,
305 | ip_country,
306 | region,
307 | city,
308 | remark,
309 | category,
310 | proxy_config,
311 | fingerprint_config
312 | )
313 |
314 | async with http_client() as client:
315 | (await client.post(**args))
316 |
317 | handler()
318 |
319 | async def move(self, group: Group) -> None:
320 | """
321 | Move profile from one group to another
322 | :param group: Group to which an account is to be moved,
323 | :return: None
324 | """
325 | http_client = self._client
326 | args, handler = self._move(group)
327 |
328 | async with http_client() as client:
329 | (await client.post(**args))
330 |
331 | handler()
332 |
333 | async def active(self) -> bool:
334 | """
335 | Return whether a browser is active
336 | :return: True if browser is active, False otherwise
337 | """
338 | args, handler = self._active()
339 | http_client = self._client
340 |
341 | async with http_client() as client:
342 | response = (await client.get(**args)).json()['data']
343 |
344 | return handler(response)
345 |
346 | async def delete(self) -> None:
347 | """
348 | Delete profile
349 | :return: None
350 | """
351 | http_client = self._client
352 | args, _ = self._delete()
353 |
354 | async with http_client() as client:
355 | (await client.post(**args))
356 |
357 | @abstractmethod
358 | async def close_tabs(self) -> None:
359 | """
360 | Closes tabs in browser
361 | :return: None
362 | """
363 | pass
364 |
365 | async def _quit(self) -> None:
366 | http_client = self._client
367 | args, _ = super()._quit()
368 |
369 | async with http_client() as client:
370 | (await client.get(**args))
371 |
372 | @abstractmethod
373 | async def quit(self) -> None:
374 | """
375 | Closes current profile
376 |
377 | :raise QuittingProfileError: Quitting profile is failed. Profile can be already closed
378 | :return: None
379 | """
380 | pass
381 |
--------------------------------------------------------------------------------
/adspower/async_api/selenium/__init__.py:
--------------------------------------------------------------------------------
1 | from .profile import Profile, Group, Category
2 |
--------------------------------------------------------------------------------
/adspower/async_api/selenium/profile.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from typing import Optional, AsyncContextManager
3 | from selenium.webdriver.chrome.options import Options
4 | from selenium.webdriver.chrome.service import Service
5 | from selenium.webdriver.chrome.webdriver import WebDriver
6 | from adspower.async_api._base_profile import _BaseProfile
7 | from adspower.async_api.group import Group
8 | from adspower.async_api.category import Category
9 | from adspower.types import IpChecker
10 |
11 |
12 | class Profile(_BaseProfile):
13 | def __init__(
14 | self,
15 | id_: str,
16 | serial_number: int,
17 | name: str | None,
18 | group: Group,
19 | domain_name: str | None,
20 | username: str | None,
21 | remark: str | None,
22 | created_time: datetime,
23 | category: Category | None,
24 | ip: str | None,
25 | ip_country: str | None,
26 | ip_checker: IpChecker,
27 | fakey: str | None,
28 | password: str | None,
29 | last_open_time: datetime | None
30 | ):
31 | """
32 | The class interacting with profile management.
33 |
34 | :param id_: Profile id
35 | :param serial_number: Serial number of the profile
36 | :param name: Name of the profile
37 | :param group: Group where the profile is located
38 | :param domain_name: Domain name, such as facebook.com, amazon.com... Will open when getting the browser.
39 | :param username: If username duplication is allowed, leave here empty.
40 | :param remark: Description of the profile
41 | :param created_time: Creation time of the profile
42 | :param category: Extension category for the profile
43 | :param ip: Proxy IP used for an account to log in. Fill in when proxy software is lumauto or oxylabs.
44 | :param ip_country: Country or region your lumauto and oxylabs account belongs to. Without lumauto and oxylabs IP please enter country.
45 | :param ip_checker: IP checker for the profile. Choose from ['ip2location', 'ipapi']
46 | :param fakey: 2FA-key. This applies to online 2FA code generator, which works similarly to authenticators.
47 | :param password: If password duplication is allowed, leave here empty.
48 | :param last_open_time: Last open time of the profile
49 | """
50 | super().__init__(
51 | id_,
52 | serial_number,
53 | name,
54 | group,
55 | domain_name,
56 | username,
57 | remark,
58 | created_time,
59 | category,
60 | ip,
61 | ip_country,
62 | ip_checker,
63 | fakey,
64 | password,
65 | last_open_time,
66 | )
67 |
68 | @classmethod
69 | async def anonymous(cls, group: Group) -> AsyncContextManager[WebDriver]:
70 | return super().anonymous(group)
71 |
72 | async def __aenter__(self) -> WebDriver:
73 | return await super().__aenter__()
74 |
75 | @property
76 | def browser(self) -> WebDriver:
77 | """
78 | :return: WebDriver connected to the profile if it's open, None otherwise
79 | """
80 | return self._browser
81 |
82 | async def get_browser(
83 | self,
84 | ip_tab: bool = True,
85 | new_first_tab: bool = True,
86 | launch_args: Optional[list[str]] = None,
87 | headless: bool = False,
88 | disable_password_filling: bool = False,
89 | clear_cache_after_closing: bool = False,
90 | enable_password_saving: bool = False,
91 | close_tabs: bool = True,
92 | start_maximized: bool = True,
93 | options: Options = Options()
94 | ) -> WebDriver:
95 | """
96 | Get a WebDriver connected to the profile
97 | :param ip_tab: Whether to open the ip detection page
98 | :param new_first_tab: Whether to use the new version of the ip detection page
99 | :param launch_args: Browser startup parameters. eg: --blink-settings=imagesEnabled=false:
100 | Prohibit image loading. --disable-notifications: Disable notifications
101 | :param headless: Whether to start the headless browser
102 | :param disable_password_filling: Whether to disable the function of filling password
103 | :param clear_cache_after_closing: Whether to delete the cache after closing the browser
104 | :param enable_password_saving: Whether to allow password saving
105 | :param close_tabs: Whether to close all startup tabs
106 | :param start_maximized: Whether to enable maximized window size at start
107 | :param options: Options to pass to the WebDriver constructor
108 | :return: WebDriver instance
109 | """
110 | response = await self._get_browser(
111 | ip_tab,
112 | new_first_tab,
113 | launch_args,
114 | headless,
115 | disable_password_filling,
116 | clear_cache_after_closing,
117 | enable_password_saving,
118 | )
119 |
120 | debugger_address = response['ws']['selenium']
121 | chrome_driver = response['webdriver']
122 |
123 | options.add_experimental_option('debuggerAddress', debugger_address)
124 | options.page_load_strategy = 'none'
125 |
126 | if not headless:
127 | options.add_argument('--headless=new')
128 |
129 | service = Service(executable_path=chrome_driver)
130 | browser = self._browser = WebDriver(service=service, options=options)
131 |
132 | if start_maximized:
133 | browser.maximize_window()
134 |
135 | if close_tabs:
136 | self.close_tabs()
137 |
138 | return browser
139 |
140 | def close_tabs(self) -> None:
141 | """
142 | Closes all tabs, exclude current tab
143 | :return: None
144 | """
145 | browser = self._browser
146 | original_window_handle = browser.current_window_handle
147 |
148 | windows = browser.window_handles
149 | for window in windows:
150 | if original_window_handle != window:
151 | browser.switch_to.window(window)
152 | browser.close()
153 |
154 | browser.switch_to.window(original_window_handle)
155 |
156 | async def quit(self) -> None:
157 | """
158 | Quit the browser
159 | :return: None
160 | """
161 | await self._quit()
162 | self._browser.quit()
163 | self._browser = None
164 |
--------------------------------------------------------------------------------
/adspower/exceptions.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 | from httpx import ConnectError, RequestError, Request
3 |
4 | from adspower import ProxyConfig
5 |
6 |
7 | class ZeroResponseError(RequestError):
8 | """Raised if response code is 0"""
9 |
10 | def __init__(self, message: str, request: Request, response: dict[str, Any]):
11 | super().__init__(f"{message}. Response: {response}", request=request)
12 | self.__response = response
13 |
14 | @property
15 | def response(self) -> dict[str, Any]:
16 | return self.__response
17 |
18 |
19 | class ExceededQPSError(ConnectionRefusedError):
20 | """Raised when amount of queries per second is exceeded"""
21 |
22 | def __init__(self):
23 | super().__init__("Too many request per second, please try later")
24 |
25 |
26 | class InternalAPIError(RequestError):
27 | """Raised when an internal API error is encountered"""
28 |
29 | def __init__(self, request: Request, response: dict[str, Any]):
30 | super().__init__(f'The internal API error is encountered. Response: {response}', request=request)
31 | self.__response = response
32 |
33 | @property
34 | def response(self) -> dict[str, Any]:
35 | return self.__response
36 |
37 |
38 | class UnavailableAPIError(ConnectError):
39 | """Raised when API url specified with invalid port or AdsPower is not opened"""
40 |
41 | def __init__(self, port: int):
42 | super().__init__(f"API url is specified with invalid port or AdsPower is not opened. Port is {port}")
43 | self.__port = port
44 |
45 | @property
46 | def port(self) -> Any:
47 | return self.__port
48 |
49 |
50 | class APIRefusedError(ConnectionRefusedError):
51 | """Raised when user have no paid subscription to use API"""
52 |
53 | def __init__(self):
54 | super().__init__("Local API is only available in paid subscriptions")
55 |
56 |
57 | class InvalidPortError(TypeError):
58 | """Raised when port is not represented as an integer between 1 and 65535"""
59 |
60 | def __init__(self, port: Any):
61 | super().__init__(f"Port must be a number between 1 and 65535. You provided port {port}")
62 | self.__port = port
63 |
64 | @property
65 | def port(self) -> Any:
66 | return self.__port
67 |
68 |
69 | class InvalidProxyConfig(Exception):
70 | """Raised when proxy config is invalid"""
71 |
72 | def __init__(self, proxy_config: ProxyConfig):
73 | super().__init__(f"The proxy config is invalid. Config: {proxy_config}")
74 |
--------------------------------------------------------------------------------
/adspower/sync_api/__init__.py:
--------------------------------------------------------------------------------
1 | from .group import Group
2 | from .category import Category
3 | from .http_client import HTTPClient
4 | from .profile_api import ProfileAPI
5 |
--------------------------------------------------------------------------------
/adspower/sync_api/_base_profile.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 | from contextlib import contextmanager
3 | from typing import Optional, ContextManager
4 | from adspower.sync_api import ProfileAPI, Group
5 |
6 |
7 | class _BaseProfile(ProfileAPI, ABC):
8 | def __enter__(self):
9 | browser = self.get_browser()
10 | return browser
11 |
12 | def __exit__(self, exc_type, exc_val, exc_tb):
13 | self.quit()
14 |
15 | @classmethod
16 | @contextmanager
17 | def anonymous(cls, group: Group) -> ContextManager:
18 | profile = cls.create(group=group)
19 | try:
20 | yield profile.get_browser()
21 | finally:
22 | profile.quit()
23 | profile.delete()
24 |
25 | @property
26 | @abstractmethod
27 | def browser(self):
28 | pass
29 |
30 | @abstractmethod
31 | def get_browser(
32 | self,
33 | ip_tab: bool = False,
34 | new_first_tab: bool = True,
35 | launch_args: Optional[list[str]] = None,
36 | headless: bool = False,
37 | disable_password_filling: bool = False,
38 | clear_cache_after_closing: bool = False,
39 | enable_password_saving: bool = False,
40 | close_tabs: bool = True
41 | ):
42 | pass
43 |
44 | @abstractmethod
45 | def close_tabs(self) -> None:
46 | pass
47 |
48 | @abstractmethod
49 | def quit(self) -> None:
50 | pass
51 |
--------------------------------------------------------------------------------
/adspower/sync_api/category.py:
--------------------------------------------------------------------------------
1 | from typing import Self, Optional, ClassVar
2 | from adspower._base_category import _BaseCategory
3 | from .http_client import HTTPClient
4 |
5 |
6 | class Category(_BaseCategory):
7 | _client: ClassVar[type[HTTPClient]] = HTTPClient
8 |
9 | def __init__(self, id_: int, name: str | None, remark: str | None):
10 | """
11 | The class interacting with extension categories. You can use an extension category to specify extensions for
12 | profiles
13 | :param id_: Id of the extension category
14 | :param name: Name of the extension category
15 | :param remark: Description of the extension category
16 | """
17 | super().__init__(id_, name, remark)
18 |
19 | @classmethod
20 | def query(cls, id_: Optional[int] = None, name: Optional[str] = None, page: int = 1, page_size: int = 100) -> list[Self]:
21 | """
22 | Query the list of extension categories.
23 | :param id_: Id of the extension category
24 | :param name: Name of the extension category
25 | :param page: Number of page in returning list. Default value - 1.
26 | Numbers of elements in returning list is equal to the range(page, page + page_size)
27 | :param page_size: Maximum length of returning list. Default value - 100
28 |
29 | :return: List of categories
30 | """
31 | http_client = cls._client
32 | args, handler = cls._query(id_, name, page, page_size)
33 |
34 | with http_client() as client:
35 | response = client.get(**args).json()['data']
36 |
37 | return handler(response)
38 |
--------------------------------------------------------------------------------
/adspower/sync_api/group.py:
--------------------------------------------------------------------------------
1 | from typing import ClassVar, Optional
2 | from typing import Self
3 | from adspower._base_group import _BaseGroup
4 | from .http_client import HTTPClient
5 |
6 |
7 | class Group(_BaseGroup):
8 | _client: ClassVar[type[HTTPClient]] = HTTPClient
9 |
10 | def __init__(self, id_: int, name: str, remark: str | None) -> None:
11 | """
12 | The class interacting with groups. You can use groups to combine profiles
13 | :param id_: Id of the group
14 | :param name: Name of the group
15 | :param remark: Description of the group
16 | """
17 | super().__init__(id_, name, remark)
18 |
19 | @classmethod
20 | def create(cls, name: str, remark: Optional[str] = None) -> Self:
21 | """
22 | Create a new group
23 |
24 | :param name: Name of the group
25 | :param remark: Description of the group
26 | :return: Instance of the Group
27 | """
28 | http_client = cls._client
29 | args, handler = cls._create(name, remark)
30 | with http_client() as client:
31 | response = client.post(**args).json()['data']
32 |
33 | return handler(response)
34 |
35 | @classmethod
36 | def query(
37 | cls,
38 | name: Optional[str] = None,
39 | profile_id: Optional[str] = None,
40 | page_size: Optional[int] = 100,
41 | ) -> list[Self]:
42 | """
43 | Query the list of groups.
44 |
45 | :param name: Name of the group
46 | :param profile_id: ID of existing profile in AdsPower
47 | :param page_size: Maximum length of returning list. Default value - 100
48 |
49 | :return: List of groups
50 | """
51 | http_client = cls._client
52 | args, handler = cls._query(name, profile_id, page_size)
53 |
54 | with http_client() as client:
55 | response = client.get(**args).json()['data']
56 |
57 | return handler(response)
58 |
59 | def update(self, name: Optional[str] = None, remark: Optional[str] = None) -> None:
60 | """
61 | Updates a group name or description for the existing group
62 |
63 | :param name: Name of the group
64 | :param remark: Description of the group
65 | :return: None
66 | """
67 | http_client = self._client
68 | args, handler = self._update(name, remark)
69 |
70 | with http_client() as client:
71 | client.post(**args)
72 |
73 | handler()
74 |
--------------------------------------------------------------------------------
/adspower/sync_api/http_client.py:
--------------------------------------------------------------------------------
1 | import time
2 | from functools import wraps
3 | from typing import Any, Optional, Callable
4 | from httpx import Client, USE_CLIENT_DEFAULT, Response, ConnectError, InvalidURL
5 | from httpx._client import UseClientDefault
6 | from httpx._types import (URLTypes, RequestContent, RequestData, RequestFiles, QueryParamTypes, HeaderTypes,
7 | CookieTypes,
8 | AuthTypes, TimeoutTypes, RequestExtensions)
9 | from urllib3.exceptions import MaxRetryError, NewConnectionError
10 | from adspower.exceptions import UnavailableAPIError
11 | from adspower._base_http_client import _BaseHTTPClient
12 |
13 |
14 | class HTTPClient(Client, _BaseHTTPClient):
15 |
16 | def __init__(self):
17 | """
18 | HTTPClient is a wrapper around httpx's Client to make it easier to perform requests against Local API.
19 | You can customize internal behaviour of the package using HTTPClient`s methods, such as `set_timeout`, `set_port`,
20 | `set_delay` and get information about client availability using `available`
21 | """
22 | port = 50325
23 | _BaseHTTPClient.__init__(self, port)
24 | Client.__init__(self, base_url=self._api_url, timeout=self._timeout)
25 |
26 | @staticmethod
27 | def _delay_request(func: Callable[..., Response]):
28 |
29 | @wraps(func)
30 | def wrapper(*args, **kwargs) -> Any:
31 | current_time = time.time()
32 |
33 | if _BaseHTTPClient.available():
34 | result = func(*args, **kwargs)
35 | _BaseHTTPClient._request_availability = time.time() + HTTPClient._delay
36 | else:
37 | time.sleep(HTTPClient._request_availability - current_time)
38 | _BaseHTTPClient._request_availability = time.time() + HTTPClient._delay
39 | result = func(*args, **kwargs)
40 |
41 | return result
42 |
43 | return wrapper
44 |
45 | @staticmethod
46 | def _handle_request(func: Callable[..., Response]):
47 | @wraps(func)
48 | def wrapper(
49 | self: "HTTPClient",
50 | *args,
51 | **kwargs,
52 | ) -> Response:
53 | try:
54 | super().get('/status')
55 | except (MaxRetryError, ConnectError, NewConnectionError, ConnectionRefusedError, InvalidURL):
56 | raise UnavailableAPIError(self._port)
57 | else:
58 | response = func(self, *args, **kwargs)
59 | HTTPClient._validate_response(response, kwargs['error_msg'])
60 |
61 | return response
62 |
63 | return wrapper
64 |
65 | @_handle_request
66 | @_delay_request
67 | def post(
68 | self,
69 | url: URLTypes,
70 | *,
71 | error_msg: str,
72 | content: Optional[RequestContent] = None,
73 | data: Optional[RequestData] = None,
74 | files: Optional[RequestFiles] = None,
75 | json: Optional[Any] = None,
76 | params: Optional[QueryParamTypes] = None,
77 | headers: Optional[HeaderTypes] = None,
78 | cookies: Optional[CookieTypes] = None,
79 | auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
80 | follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
81 | timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
82 | extensions: Optional[RequestExtensions] = None,
83 | ) -> Response:
84 | return super().post(
85 | url=url,
86 | content=content,
87 | data=data,
88 | files=files,
89 | json=json,
90 | params=params,
91 | headers=headers,
92 | cookies=cookies,
93 | auth=auth,
94 | follow_redirects=follow_redirects,
95 | timeout=timeout,
96 | extensions=extensions
97 | )
98 |
99 | @_handle_request
100 | @_delay_request
101 | def get(
102 | self,
103 | url: URLTypes,
104 | *,
105 | error_msg: str,
106 | params: QueryParamTypes | None = None,
107 | headers: HeaderTypes | None = None,
108 | cookies: CookieTypes | None = None,
109 | auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
110 | follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
111 | timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
112 | extensions: RequestExtensions | None = None,
113 | ) -> Response:
114 | return super().get(
115 | url=url,
116 | params=params,
117 | headers=headers,
118 | cookies=cookies,
119 | auth=auth,
120 | follow_redirects=follow_redirects,
121 | timeout=timeout,
122 | extensions=extensions
123 | )
124 |
--------------------------------------------------------------------------------
/adspower/sync_api/playwright/__init__.py:
--------------------------------------------------------------------------------
1 | from .profile import Profile, Group, Category
--------------------------------------------------------------------------------
/adspower/sync_api/playwright/profile.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from typing import Optional, ContextManager
3 | from adspower.sync_api._base_profile import _BaseProfile
4 | from adspower.sync_api.group import Group
5 | from adspower.sync_api.category import Category
6 | from playwright.sync_api import sync_playwright, BrowserContext, Playwright, Browser
7 | from adspower.types import IpChecker
8 |
9 |
10 | class Profile(_BaseProfile):
11 | def __init__(
12 | self,
13 | id_: str,
14 | serial_number: int,
15 | name: str | None,
16 | group: Group,
17 | domain_name: str | None,
18 | username: str | None,
19 | remark: str | None,
20 | created_time: datetime,
21 | category: Category | None,
22 | ip: str | None,
23 | ip_country: str | None,
24 | ip_checker: IpChecker,
25 | fakey: str | None,
26 | password: str | None,
27 | last_open_time: datetime | None
28 | ):
29 | """
30 | The class interacting with profile management.
31 |
32 | :param id_: Profile id
33 | :param serial_number: Serial number of the profile
34 | :param name: Name of the profile
35 | :param group: Group where the profile is located
36 | :param domain_name: Domain name, such as facebook.com, amazon.com... Will open when getting the browser.
37 | :param username: If username duplication is allowed, leave here empty.
38 | :param remark: Description of the profile
39 | :param created_time: Creation time of the profile
40 | :param category: Extension category for the profile
41 | :param ip: Proxy IP used for an account to log in. Fill in when proxy software is lumauto or oxylabs.
42 | :param ip_country: Country or region your lumauto and oxylabs account belongs to. Without lumauto and oxylabs IP please enter country.
43 | :param ip_checker: IP checker for the profile. Choose from ['ip2location', 'ipapi']
44 | :param fakey: 2FA-key. This applies to online 2FA code generator, which works similarly to authenticators.
45 | :param password: If password duplication is allowed, leave here empty.
46 | :param last_open_time: Last open time of the profile
47 | """
48 | super().__init__(
49 | id_,
50 | serial_number,
51 | name,
52 | group,
53 | domain_name,
54 | username,
55 | remark,
56 | created_time,
57 | category,
58 | ip,
59 | ip_country,
60 | ip_checker,
61 | fakey,
62 | password,
63 | last_open_time,
64 | )
65 | self.__playwright: Playwright | None = None
66 | self.__browser_app: Browser | None = None
67 |
68 | def __enter__(self) -> BrowserContext:
69 | return super().__enter__()
70 |
71 | @classmethod
72 | def anonymous(cls, group: Group) -> ContextManager[BrowserContext]:
73 | return super().anonymous(group)
74 |
75 | @property
76 | def browser(self) -> BrowserContext | None:
77 | """
78 | :return: BrowserContext connected to the profile, if it's open, None otherwise
79 | """
80 | return self._browser
81 |
82 | def get_browser(
83 | self,
84 | ip_tab: bool = True,
85 | new_first_tab: bool = True,
86 | launch_args: Optional[list[str]] = None,
87 | headless: bool = False,
88 | disable_password_filling: bool = False,
89 | clear_cache_after_closing: bool = False,
90 | enable_password_saving: bool = False,
91 | close_tabs: bool = True,
92 | ) -> BrowserContext:
93 | """
94 | Get a BrowserContext connected to the profile
95 | :param ip_tab: Whether to open the ip detection page
96 | :param new_first_tab: Whether to use the new version of the ip detection page
97 | :param launch_args: Browser startup parameters. eg: --blink-settings=imagesEnabled=false:
98 | Prohibit image loading. --disable-notifications: Disable notifications
99 | :param headless: Whether to start the headless browser
100 | :param disable_password_filling: Whether to disable the function of filling password
101 | :param clear_cache_after_closing: Whether to delete the cache after closing the browser
102 | :param enable_password_saving: Whether to allow password saving
103 | :param close_tabs: Whether to close all startup tabs
104 | :return: BrowserContext instance
105 | """
106 | response = self._get_browser(
107 | ip_tab,
108 | new_first_tab,
109 | launch_args,
110 | headless,
111 | disable_password_filling,
112 | clear_cache_after_closing,
113 | enable_password_saving,
114 | )
115 | playwright = self.__playwright = sync_playwright().start()
116 | browser_app = playwright.chromium.connect_over_cdp(f'http://localhost:{response["debug_port"]}')
117 | self.__browser_app = browser_app
118 | browser = self._browser = browser_app.contexts[0]
119 |
120 | if close_tabs:
121 | self.close_tabs()
122 |
123 | return browser
124 |
125 | def close_tabs(self) -> None:
126 | """
127 | Closes all tabs, exclude current tab
128 | :return: None
129 | """
130 | browser = self._browser
131 | new_page = browser.new_page()
132 |
133 | for page in browser.pages:
134 | if page != new_page:
135 | page.close()
136 |
137 | def quit(self) -> None:
138 | """
139 | Quit the browser
140 | :return: None
141 | """
142 | self._quit()
143 |
144 | self._browser.close()
145 | self.__browser_app.close()
146 | self.__playwright.stop()
147 |
148 | self._browser = None
149 | self.__playwright = None
150 | self.__browser_app = None
151 |
--------------------------------------------------------------------------------
/adspower/sync_api/profile_api.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from typing import Optional, ClassVar, Self, Any
3 | from .category import Category
4 | from .http_client import HTTPClient
5 | from .group import Group
6 | from adspower._base_profile_api import _BaseProfileAPI
7 | from adspower.types import (ProxyConfig, Cookies, FingerprintConfig, RepeatConfigType, BrowserResponse,
8 | UserSort, IpChecker)
9 | from ..utils import _convert_json
10 |
11 |
12 | class ProfileAPI(_BaseProfileAPI):
13 | _client: ClassVar[type[HTTPClient]] = HTTPClient
14 |
15 | def __init__(
16 | self,
17 | id_: str,
18 | serial_number: int,
19 | name: str | None,
20 | group: Group,
21 | domain_name: str | None,
22 | username: str | None,
23 | remark: str | None,
24 | created_time: datetime,
25 | category: Category | None,
26 | ip: str | None,
27 | ip_country: str | None,
28 | ip_checker: IpChecker,
29 | fakey: str | None,
30 | password: str | None,
31 | last_open_time: datetime | None
32 | ):
33 | """
34 | The class interacting with profile management. It doesn't interact with getting a browser to use it with Selenium or
35 | Playwright. If you want to interact with that, you need to use `Profile` class.
36 |
37 | :param id_: Profile id
38 | :param serial_number: Serial number of the profile
39 | :param name: Name of the profile
40 | :param group: Group where the profile is located
41 | :param domain_name: Domain name, such as facebook.com, amazon.com... Will open when getting the browser.
42 | :param username: Username for the domain name (e.g. facebook.com, amazon)
43 | :param remark: Description of the profile
44 | :param created_time: Creation time of the profile
45 | :param category: Extension category for the profile
46 | :param ip: Proxy IP used for an account to log in. Fill in when proxy software is lumauto or oxylabs.
47 | :param ip_country: Country or region your lumauto and oxylabs account belongs to. Without lumauto and oxylabs IP please enter country.
48 | :param ip_checker: IP checker for the profile. Choose from ['ip2location', 'ipapi']
49 | :param fakey: 2FA-key for the domain name (e.g. facebook.com, amazon).
50 | This applies to online 2FA code generator, which works similarly to authenticators.
51 | :param password: Password for the domain name (e.g. facebook.com, amazon)
52 | :param last_open_time: Last open time of the profile
53 | """
54 | _BaseProfileAPI.__init__(
55 | self,
56 | id_,
57 | serial_number,
58 | name, group,
59 | domain_name,
60 | username,
61 | remark,
62 | created_time,
63 | category,
64 | ip,
65 | ip_country,
66 | ip_checker,
67 | fakey,
68 | password,
69 | last_open_time
70 | )
71 |
72 | @staticmethod
73 | def _get_init_args(response: dict[str, Any]) -> dict[str, Any]:
74 | response = _convert_json(response)
75 |
76 | response['id_'] = response.pop('user_id')
77 | response['category'] = Category(id_=id_, name=None, remark=None) if (
78 | id_ := response.pop('sys_app_cate_id')) else None
79 |
80 | last_open_time = response['last_open_time']
81 | response['created_time'] = datetime.fromtimestamp(response['created_time'])
82 | response['last_open_time'] = datetime.fromtimestamp(last_open_time) if last_open_time else None
83 | response['ip_checker'] = response.pop('ipchecker')
84 |
85 | response['group'] = Group(id_=response.pop('group_id'), name=response.pop('group_name'), remark=None)
86 |
87 | response.pop('fbcc_proxy_acc_id')
88 |
89 | return response
90 |
91 | @classmethod
92 | def create(
93 | cls,
94 | group: Group,
95 | name: Optional[str] = None,
96 | domain_name: Optional[str] = None,
97 | open_urls: Optional[list[str]] = None,
98 | repeat_config: Optional[RepeatConfigType] = None,
99 | username: Optional[str] = None,
100 | password: Optional[str] = None,
101 | fakey: Optional[str] = None,
102 | cookies: Optional[Cookies] = None,
103 | ignore_cookie_error: bool = False,
104 | ip: Optional[str] = None,
105 | ip_country: Optional[str] = None,
106 | region: Optional[str] = None,
107 | city: Optional[str] = None,
108 | remark: Optional[str] = None,
109 | ip_checker: Optional[IpChecker] = None,
110 | category: Optional[Category] = None,
111 | proxy_config: Optional[ProxyConfig] = None,
112 | fingerprint_config: Optional[FingerprintConfig] = None
113 | ) -> Self:
114 | """
115 | Create a new profile
116 |
117 | :param name: Name of the profile
118 | :param group: Group where the profile is located
119 | :param domain_name: Domain name, such as facebook.com, amazon.com... Will open when getting the browser.
120 | :param open_urls: Other urls when opening browser. If leaving it empty, will open the domain name url.
121 | :param repeat_config: Account deduplication. Default setting: Allow duplication.
122 | 0: Allow duplication;
123 | 2: Deduplication based on the account name/password;
124 | 3: Deduplication based on cookie;
125 | 4: Deduplication based on c_user (c_user is a specific tag for Facebook)
126 | :param username: Username for the domain name (e.g. facebook.com, amazon)
127 | :param remark: Description of the profile
128 | :param category: Extension category for the profile
129 | :param ip: Proxy IP used for an account to log in. Fill in when proxy software is lumauto or oxylabs.
130 | :param ip_country: Country or region your lumauto and oxylabs account belongs to.Without lumauto and oxylabs IP please enter country.
131 | :param region: State or province where account logged in.
132 | :param city: City where account logged in.
133 | :param ip_checker: IP checker for the profile. Choose from ['ip2location', 'ipapi']
134 | :param fakey: 2FA-key for the domain name (e.g. facebook.com, amazon).
135 | This applies to online 2FA code generator, which works similarly to authenticators.
136 | :param password: Password for the domain name (e.g. facebook.com, amazon)
137 | :param cookies: Cookies to be set when opening browser
138 | :param ignore_cookie_error: 0:When the cookie verification fails, the cookie format is incorrectly returned directly
139 | 1:When the cookie verification fails, filter out the data in the wrong format and keep the cookie in the correct format
140 | Only supports netspace
141 | :param proxy_config: Dictionary containing proxy info
142 | :param fingerprint_config: Dictionary containing fingerprint info
143 |
144 | :return: A Profile instance
145 | """
146 | http_client = cls._client
147 | args, handler = cls._create(
148 | group,
149 | name,
150 | domain_name,
151 | open_urls,
152 | repeat_config,
153 | username,
154 | password,
155 | fakey,
156 | cookies,
157 | ignore_cookie_error,
158 | ip,
159 | ip_country,
160 | region,
161 | city,
162 | remark,
163 | ip_checker,
164 | category,
165 | proxy_config,
166 | fingerprint_config
167 | )
168 |
169 | with http_client() as client:
170 | response = client.post(**args).json()['data']
171 |
172 | return handler(response)[0]
173 |
174 | @classmethod
175 | def query(
176 | cls,
177 | group: Optional[Group] = None,
178 | id_: Optional[str] = None,
179 | name: Optional[str] = None,
180 | serial_number: Optional[int] = None,
181 | user_sort: Optional[UserSort] = None,
182 | page: int = 1,
183 | page_size: int = 100,
184 | ) -> list[Self]:
185 | """
186 | Query the list of profiles
187 | :param group: Group where profiles are located
188 | :param id_: Id of the profile
189 | :param name: Name of the profile
190 | :param serial_number: Serial number of the profile
191 | :param user_sort: User sorting. Can be sorted by the specified type, supporting
192 | three fields serial_number, last_open_time, created_time, and two values asc and desc.
193 | :param page: Number of page in returning list. Default value - 1.
194 | Numbers of elements in returning list is equal to the range(page, page + page_size)
195 | :param page_size: Maximum length of returning list. Default value - 100
196 | :return: List of profiles
197 | """
198 | http_client = cls._client
199 | args, handler = cls._query(
200 | group,
201 | id_,
202 | name,
203 | serial_number,
204 | user_sort,
205 | page,
206 | page_size
207 | )
208 |
209 | with http_client() as client:
210 | response = client.get(**args).json()['data']
211 |
212 | return handler(response)
213 |
214 | @classmethod
215 | def delete_cache(cls) -> None:
216 | """
217 | Deletes cache of all profiles
218 | :return: None
219 | """
220 | http_client = cls._client
221 | args, _ = cls._delete_cache()
222 |
223 | with http_client() as client:
224 | client.post(**args)
225 |
226 | def _get_browser(
227 | self,
228 | ip_tab: bool = True,
229 | new_first_tab: bool = True,
230 | launch_args: Optional[list[str]] = None,
231 | headless: bool = False,
232 | disable_password_filling: bool = False,
233 | clear_cache_after_closing: bool = False,
234 | enable_password_saving: bool = False
235 | ) -> BrowserResponse:
236 | http_client = self._client
237 |
238 | args, handler = super()._get_browser(
239 | ip_tab,
240 | new_first_tab,
241 | launch_args,
242 | headless,
243 | disable_password_filling,
244 | clear_cache_after_closing,
245 | enable_password_saving
246 | )
247 |
248 | with http_client() as client:
249 | response = client.get(**args).json()['data']
250 |
251 | handler()
252 | return response
253 |
254 | def update(
255 | self,
256 | name: Optional[str] = None,
257 | domain_name: Optional[str] = None,
258 | open_urls: Optional[list[str]] = None,
259 | username: Optional[str] = None,
260 | password: Optional[str] = None,
261 | fakey: Optional[str] = None,
262 | cookies: Optional[Cookies] = None,
263 | ignore_cookie_error: bool = False,
264 | ip: Optional[str] = None,
265 | ip_country: Optional[str] = None,
266 | region: Optional[str] = None,
267 | city: Optional[str] = None,
268 | remark: Optional[str] = None,
269 | category: Optional[Category] = None,
270 | proxy_config: Optional[ProxyConfig] = None,
271 | fingerprint_config: Optional[FingerprintConfig] = None
272 | ) -> None:
273 | """
274 | Update the profile
275 |
276 | :param name: Name of the profile
277 | :param domain_name: Domain name, such as facebook.com, amazon.com... Will open when getting the browser.
278 | :param open_urls: Other urls when opening browser. If leaving it empty, will open the domain name url.
279 | :param username: If username duplication is allowed, leave here empty.
280 | :param remark: Description of the profile
281 | :param category: Extension category for the profile
282 | :param ip: Proxy IP used for an account to log in. Fill in when proxy software is lumauto or oxylabs.
283 | :param ip_country: Country or region your lumauto and oxylabs account belongs to.Without lumauto and oxylabs IP please enter country.
284 | :param region: State or province where account logged in.
285 | :param city: City where account logged in.
286 | :param fakey: 2FA-key. This applies to online 2FA code generator, which works similarly to authenticators.
287 | :param cookies: Cookies to be set when opening browser
288 | :param ignore_cookie_error: 0:When the cookie verification fails, the cookie format is incorrectly returned directly
289 | 1:When the cookie verification fails, filter out the data in the wrong format and keep the cookie in the correct format
290 | Only supports netspace
291 | :param password: If password duplication is allowed, leave here empty.
292 | :param proxy_config: Dictionary containing proxy info
293 | :param fingerprint_config: Dictionary containing fingerprint info
294 | """
295 | http_client = self._client
296 | args, handler = self._update(
297 | name,
298 | domain_name,
299 | open_urls,
300 | username,
301 | password,
302 | fakey,
303 | cookies,
304 | ignore_cookie_error,
305 | ip,
306 | ip_country,
307 | region,
308 | city,
309 | remark,
310 | category,
311 | proxy_config,
312 | fingerprint_config
313 | )
314 |
315 | with http_client() as client:
316 | client.post(**args)
317 |
318 | handler()
319 |
320 | def move(self, group: Group) -> None:
321 | """
322 | Move profile from one group to another
323 | :param group: Group to which an account is to be moved,
324 | :return: None
325 | """
326 | http_client = self._client
327 | args, handler = self._move(group)
328 |
329 | with http_client() as client:
330 | client.post(**args)
331 |
332 | handler()
333 |
334 | def active(self) -> bool:
335 | """
336 | Return whether a browser is active
337 | :return: True if browser is active, False otherwise
338 | """
339 | args, handler = self._active()
340 | http_client = self._client
341 |
342 | with http_client() as client:
343 | response = client.get(**args).json()['data']
344 |
345 | return handler(response)
346 |
347 | def delete(self) -> None:
348 | """
349 | Delete profile
350 | :return: None
351 | """
352 | http_client = self._client
353 | args, _ = self._delete()
354 |
355 | with http_client() as client:
356 | client.post(**args)
357 |
358 | def _quit(self) -> None:
359 | http_client = self._client
360 | args, _ = super()._quit()
361 |
362 | with http_client() as client:
363 | client.get(**args)
364 |
--------------------------------------------------------------------------------
/adspower/sync_api/selenium/__init__.py:
--------------------------------------------------------------------------------
1 | from .profile import Profile, Group, Category
2 |
--------------------------------------------------------------------------------
/adspower/sync_api/selenium/profile.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from typing import Optional, ContextManager
3 | from selenium.webdriver.chrome.options import Options
4 | from selenium.webdriver.chrome.service import Service
5 | from selenium.webdriver.chrome.webdriver import WebDriver
6 | from adspower.sync_api._base_profile import _BaseProfile
7 | from adspower.sync_api.group import Group
8 | from adspower.sync_api.category import Category
9 | from adspower.types import IpChecker
10 |
11 |
12 | class Profile(_BaseProfile):
13 | def __init__(
14 | self,
15 | id_: str,
16 | serial_number: int,
17 | name: str | None,
18 | group: Group,
19 | domain_name: str | None,
20 | username: str | None,
21 | remark: str | None,
22 | created_time: datetime,
23 | category: Category | None,
24 | ip: str | None,
25 | ip_country: str | None,
26 | ip_checker: IpChecker,
27 | fakey: str | None,
28 | password: str | None,
29 | last_open_time: datetime | None
30 | ):
31 | """
32 | The class interacting with profile management.
33 |
34 | :param id_: Profile id
35 | :param serial_number: Serial number of the profile
36 | :param name: Name of the profile
37 | :param group: Group where the profile is located
38 | :param domain_name: Domain name, such as facebook.com, amazon.com... Will open when getting the browser.
39 | :param username: If username duplication is allowed, leave here empty.
40 | :param remark: Description of the profile
41 | :param created_time: Creation time of the profile
42 | :param category: Extension category for the profile
43 | :param ip: Proxy IP used for an account to log in. Fill in when proxy software is lumauto or oxylabs.
44 | :param ip_country: Country or region your lumauto and oxylabs account belongs to. Without lumauto and oxylabs IP please enter country.
45 | :param ip_checker: IP checker for the profile. Choose from ['ip2location', 'ipapi']
46 | :param fakey: 2FA-key. This applies to online 2FA code generator, which works similarly to authenticators.
47 | :param password: If password duplication is allowed, leave here empty.
48 | :param last_open_time: Last open time of the profile
49 | """
50 | super().__init__(
51 | id_,
52 | serial_number,
53 | name,
54 | group,
55 | domain_name,
56 | username,
57 | remark,
58 | created_time,
59 | category,
60 | ip,
61 | ip_country,
62 | ip_checker,
63 | fakey,
64 | password,
65 | last_open_time,
66 | )
67 |
68 | def __enter__(self) -> WebDriver:
69 | return super().__enter__()
70 |
71 | @classmethod
72 | def anonymous(cls, group: Group) -> ContextManager[WebDriver]:
73 | return super().anonymous(group)
74 |
75 | @property
76 | def browser(self) -> WebDriver:
77 | """
78 | :return: WebDriver connected to the profile if it's open, None otherwise
79 | """
80 | return self._browser
81 |
82 | def get_browser(
83 | self,
84 | ip_tab: bool = True,
85 | new_first_tab: bool = True,
86 | launch_args: Optional[list[str]] = None,
87 | headless: bool = False,
88 | disable_password_filling: bool = False,
89 | clear_cache_after_closing: bool = False,
90 | enable_password_saving: bool = False,
91 | close_tabs: bool = True,
92 | start_maximized: bool = True,
93 | options: Options = Options()
94 | ) -> WebDriver:
95 | """
96 | Get a WebDriver connected to the profile
97 | :param ip_tab: Whether to open the ip detection page
98 | :param new_first_tab: Whether to use the new version of the ip detection page
99 | :param launch_args: Browser startup parameters. eg: --blink-settings=imagesEnabled=false:
100 | Prohibit image loading. --disable-notifications: Disable notifications
101 | :param headless: Whether to start the headless browser
102 | :param disable_password_filling: Whether to disable the function of filling password
103 | :param clear_cache_after_closing: Whether to delete the cache after closing the browser
104 | :param enable_password_saving: Whether to allow password saving
105 | :param close_tabs: Whether to close all startup tabs
106 | :param start_maximized: Whether to enable maximized window size at start
107 | :param options: Options to pass to the WebDriver constructor
108 | :return: WebDriver instance
109 | """
110 | response = self._get_browser(
111 | ip_tab,
112 | new_first_tab,
113 | launch_args,
114 | headless,
115 | disable_password_filling,
116 | clear_cache_after_closing,
117 | enable_password_saving,
118 | )
119 |
120 | debugger_address = response['ws']['selenium']
121 | chrome_driver = response['webdriver']
122 |
123 | options.add_experimental_option('debuggerAddress', debugger_address)
124 | options.page_load_strategy = 'none'
125 | options.add_argument('--headless=new')
126 |
127 | if not headless:
128 | options.add_argument('--headless=new')
129 |
130 | service = Service(executable_path=chrome_driver)
131 | browser = self._browser = WebDriver(service=service, options=options)
132 |
133 | if start_maximized:
134 | browser.maximize_window()
135 |
136 | if close_tabs:
137 | self.close_tabs()
138 |
139 | return browser
140 |
141 | def close_tabs(self) -> None:
142 | """
143 | Closes all tabs, exclude current tab
144 | :return: None
145 | """
146 | browser = self._browser
147 | original_window_handle = browser.current_window_handle
148 |
149 | windows = browser.window_handles
150 | for window in windows:
151 | if original_window_handle != window:
152 | browser.switch_to.window(window)
153 | browser.close()
154 |
155 | browser.switch_to.window(original_window_handle)
156 |
157 | def quit(self) -> None:
158 | """
159 | Quit the browser
160 | :return: None
161 | """
162 | self._quit()
163 | self._browser.quit()
164 | self._browser = None
165 |
--------------------------------------------------------------------------------
/adspower/types.py:
--------------------------------------------------------------------------------
1 | from typing import TypedDict, Literal, NotRequired, Any, Callable
2 |
3 | HandlingTuple = tuple[dict[str, dict | str], Callable[[dict[str, Any]], Any] | Callable[[], Any]]
4 |
5 | ProxySoft = Literal['luminati', 'lumauto', 'oxylabsatuto', '922S5', 'ipideaauto', 'ipfoxyauto', 'ssh', 'other',
6 | 'no_proxy']
7 | IpChecker = Literal['ip2location', 'ipapi']
8 | RepeatConfigType = Literal[0, 2, 3, 4]
9 | ProxyType = Literal['http', 'https', 'socks5']
10 | WebRtcType = Literal['forward', 'proxy', 'local', 'disabled']
11 | LocationType = Literal['ask', 'allow', 'block']
12 | FlashType = Literal['allow', 'block']
13 | DeviceNameType = Literal[0, 1, 2]
14 | MediaDeviceType = Literal[0, 1, 2]
15 | GPUType = Literal[0, 1, 2]
16 | WebGLVersion = Literal[0, 2, 3]
17 | Cookies = list[dict[str, Any]] | dict[str, Any]
18 | IntBool = Literal[0, 1]
19 |
20 | UserSortKey = Literal['serial_number', 'last_open_time', 'created_time']
21 | UserSortValue = Literal['desc', 'asc']
22 | UserSort = dict[UserSortKey, UserSortValue]
23 |
24 |
25 | class ProxyConfig(TypedDict):
26 | soft: ProxySoft
27 | type: ProxyType
28 | host: str
29 | port: int
30 | user: str
31 | password: str
32 |
33 |
34 | class WebGLConfig(TypedDict):
35 | unmasked_vendor: str
36 | unmasked_renderer: str
37 | webgpu: dict[str, Any]
38 |
39 |
40 | class MediaDeviceConfig(TypedDict):
41 | audioinput_num: int
42 | videoinput_num: int
43 | audiooutput_num: int
44 |
45 |
46 | class RandomUserAgent(TypedDict):
47 | ua_browser: list[str]
48 | ua_version: list[int]
49 | ua_system_version: list[str]
50 |
51 |
52 | class MacAddressConfig(TypedDict):
53 | model: int
54 | address: str
55 |
56 |
57 | class BrowserKernelConfig(TypedDict):
58 | version: str
59 | type: str
60 |
61 |
62 | class FingerprintConfig(TypedDict):
63 | automatic_timezone: NotRequired[IntBool]
64 | timezone: NotRequired[str]
65 | webrtc: NotRequired[WebRtcType]
66 | location: NotRequired[LocationType]
67 | location_switch: NotRequired[IntBool]
68 | longitude: NotRequired[float]
69 | latitude: NotRequired[float]
70 | accuracy: NotRequired[int]
71 | language: NotRequired[list[str]]
72 | language_switch: NotRequired[IntBool]
73 | page_language_switch: NotRequired[IntBool]
74 | page_language: NotRequired[str]
75 | ua: NotRequired[str]
76 | screen_resolution: NotRequired[str]
77 | fonts: NotRequired[list[str]]
78 | canvas: NotRequired[IntBool]
79 | webgl_image: NotRequired[IntBool]
80 | webgl: NotRequired[WebGLVersion]
81 | webgl_config: NotRequired[WebGLConfig]
82 | audio: NotRequired[IntBool]
83 | do_not_track: NotRequired[IntBool]
84 | hardware_concurrency: NotRequired[int]
85 | device_memory: NotRequired[int]
86 | flash: NotRequired[IntBool | FlashType]
87 | scan_port_type: NotRequired[IntBool]
88 | allow_scan_ports: NotRequired[list[int]]
89 | media_devices: NotRequired[MediaDeviceType]
90 | media_devices_num: NotRequired[MediaDeviceConfig]
91 | client_rects: NotRequired[IntBool]
92 | device_name_switch: NotRequired[DeviceNameType]
93 | device_name: NotRequired[str]
94 | random_ua: NotRequired[RandomUserAgent]
95 | speech_switch: NotRequired[IntBool]
96 | mac_address_config: NotRequired[MacAddressConfig]
97 | browser_kernel_config: NotRequired[BrowserKernelConfig]
98 | gpu: NotRequired[GPUType]
99 |
100 |
101 | class DebugInterface(TypedDict):
102 | selenium: str
103 | puppeteer: str
104 |
105 |
106 | class BrowserResponse(TypedDict):
107 | ws: DebugInterface
108 | debug_port: str
109 | webdriver: str
110 |
111 |
112 | class GroupInfo(TypedDict):
113 | group_id: int
114 | group_name: str
115 | remark: str
116 |
117 |
118 | class CategoryInfo(TypedDict):
119 | id: int
120 | name: str
121 | remark: str
122 |
123 |
124 | class ProfileInfo(TypedDict):
125 | profile_id: str
126 | serial_number: int
127 | name: str
128 | group_id: int
129 | group_name: str
130 | domain_name: str
131 | username: str
132 | remark: str
133 | created_time: int
134 | category_id: int
135 | ip: str
136 | ip_country: str
137 | ip_checker: str
138 | fakey: str
139 | password: str
140 |
--------------------------------------------------------------------------------
/adspower/utils.py:
--------------------------------------------------------------------------------
1 | import ctypes
2 | import platform
3 | import subprocess
4 | from typing import TypeVar
5 |
6 | _KT = TypeVar('_KT')
7 | _VT = TypeVar('_VT')
8 |
9 |
10 | def _convert_json(data: dict[_KT, _VT]) -> dict[_KT, _VT]:
11 | """
12 | Convert string representations of numbers to actual numeric types within a dictionary and convert
13 | empty strings to None.
14 |
15 | :param data: A dictionary containing various key-value pairs.
16 | :return: A dictionary with string representations of numbers converted to their actual numeric types.
17 | """
18 | for key, value in data.items():
19 | if isinstance(value, str) and value.isdigit():
20 | data[key] = int(value)
21 | elif isinstance(value, str) and _is_float(value):
22 | data[key] = float(value)
23 | elif isinstance(value, str) and not len(value):
24 | data[key] = None
25 | return data
26 |
27 |
28 | def _is_float(__str: str) -> bool:
29 | """
30 | Check if a string represents a floating-point number.
31 |
32 | :param __str: A string to check.
33 | :return: True if the string represents a floating-point number, False otherwise.
34 | """
35 | try:
36 | float(__str)
37 | return True
38 | except ValueError:
39 | return False
40 |
41 |
42 | def _get_screen_size() -> tuple[int, int]:
43 | os_name = platform.system()
44 | if os_name == "Windows":
45 | h = ctypes.windll.user32.GetDC(0)
46 | width = ctypes.windll.gdi32.GetDeviceCaps(h, 118)
47 | height = ctypes.windll.gdi32.GetDeviceCaps(h, 117)
48 | ctypes.windll.user32.ReleaseDC(0, h)
49 | return width, height
50 |
51 | elif os_name == "Darwin":
52 | output = subprocess.run(['system_profiler', 'SPDisplaysDataType'], capture_output=True, text=True).stdout
53 | for line in output.split('\n'):
54 | if 'Resolution' in line:
55 | parts = line.split()
56 | width = int(parts[1])
57 | height = int(parts[3])
58 | return width, height
59 |
60 | elif os_name == "Linux":
61 | output = subprocess.run(['xrandr'], capture_output=True, text=True).stdout
62 | for line in output.split('\n'):
63 | if '*' in line:
64 | resolution = line.split()[0]
65 | width, height = map(int, resolution.split('x'))
66 | return width, height
67 |
68 | else:
69 | raise NotImplementedError(f"OS {os_name} not supported")
70 |
--------------------------------------------------------------------------------
/branding/adspower/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CrocoFactory/adspower/d32a8f2a4953eab6619f082e254f6d84c33bd90f/branding/adspower/banner.png
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.poetry]
2 | name = 'adspower'
3 | version = '2.1.0'
4 | description = 'The package for interacting with API of anti-detect browser AdsPower.'
5 | authors = ['Alexey ']
6 | license = 'MIT'
7 | readme = 'README.md'
8 | repository = 'https://github.com/CrocoFactory/adspower'
9 | homepage = 'https://github.com/CrocoFactory/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 :: Only',
15 | 'Programming Language :: Python :: 3.9',
16 | 'Programming Language :: Python :: 3.10',
17 | 'Programming Language :: Python :: 3.11',
18 | 'Programming Language :: Python :: 3.12',
19 | 'License :: OSI Approved :: MIT License',
20 | 'Operating System :: OS Independent',
21 | 'License :: OSI Approved :: MIT License',
22 | 'Typing :: Typed'
23 | ]
24 | packages = [{ include = 'adspower' }]
25 |
26 | [tool.poetry.dependencies]
27 | python = '^3.9'
28 | httpx = "^0.27.0"
29 | selenium = {version = "^4.16.0", optional = true}
30 | playwright = {version = "^1.42.0", optional = true}
31 |
32 | [tool.poetry.extras]
33 | selenium = ["selenium"]
34 | playwright = ["playwright"]
35 |
36 | [tool.poetry.group.dev.dependencies]
37 | pytest = "^7.4.3"
38 | twine = "^4.0.2"
39 | build = "^1.0.3"
40 | faker = "^24.9.0"
41 | pytest-asyncio = "^0.23.6"
42 |
43 | [build-system]
44 | requires = ['poetry-core']
45 | build-backend = 'poetry.core.masonry.api'
46 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CrocoFactory/adspower/d32a8f2a4953eab6619f082e254f6d84c33bd90f/tests/__init__.py
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import uuid
2 | from typing import Callable
3 |
4 | import pytest
5 | from faker import Faker
6 |
7 |
8 | @pytest.fixture(scope="function")
9 | def name() -> str:
10 | id_ = str(uuid.uuid4())
11 | return ''.join(id_.split('-')[:2])
12 |
13 |
14 | @pytest.fixture(scope="function")
15 | def get_name() -> Callable[[], str]:
16 | def _get_name() -> str:
17 | id_ = str(uuid.uuid4())
18 | return ''.join(id_.split('-')[:2])
19 | return _get_name
20 |
21 |
22 | @pytest.fixture(scope="function")
23 | def remark() -> str:
24 | fake = Faker()
25 | remark = fake.text()
26 | return remark
27 |
28 |
29 | @pytest.fixture(scope="function")
30 | def get_remark() -> Callable[[], str]:
31 | def _get_remark() -> str:
32 | fake = Faker()
33 | remark = fake.text()
34 | return remark
35 | return _get_remark
36 |
37 |
38 | @pytest.fixture(scope="function")
39 | def category_name() -> str:
40 | return 'my_category'
41 |
--------------------------------------------------------------------------------
/tests/test_async_api.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from typing import Callable, Coroutine
3 | from selenium.webdriver.chrome.webdriver import WebDriver
4 | from adspower.async_api import Group, Category, HTTPClient
5 | from adspower.async_api.selenium import Profile as ProfileSelenium
6 | from adspower.async_api.playwright import Profile as ProfilePlaywright
7 |
8 | HTTPClient.set_delay(1.1)
9 | HTTPClient.set_timeout(30)
10 | ProfileType = ProfileSelenium | ProfilePlaywright
11 |
12 |
13 | class TestGroup:
14 | @pytest.mark.asyncio
15 | async def test_create(self, name, remark):
16 | group = await Group.create(name=name, remark=remark)
17 | group_to_check = (await Group.query(name=name))[0]
18 | assert group == group_to_check
19 |
20 | @pytest.mark.asyncio
21 | async def test_update(self, name, remark):
22 | group = await Group.create(name=name, remark=remark)
23 | await group.update(name=f'new{name}', remark='new_remark')
24 | group_to_check = (await Group.query(name=f'new{name}'))[0]
25 | assert group == group_to_check
26 |
27 |
28 | class TestCategory:
29 | @pytest.mark.asyncio
30 | async def test_query(self, category_name):
31 | categories = await Category.query()
32 | assert len(categories) > 0
33 |
34 |
35 | @pytest.mark.parametrize('profile_cls', [ProfilePlaywright, ProfileSelenium])
36 | class TestProfile:
37 | @pytest.fixture(scope="function")
38 | def make_profile(
39 | self,
40 | profile_cls,
41 | get_name,
42 | get_remark
43 | ) -> Callable[[], Coroutine[..., ..., tuple[ProfileType, type[ProfileType]]]]:
44 | async def _make_profile() -> tuple[ProfileType, type[ProfileType]]:
45 |
46 | group = await Group.create(name=get_name(), remark=get_remark())
47 | profile = await profile_cls.create(name=get_name(), remark=get_remark(), group=group)
48 | return profile, profile_cls
49 |
50 | return _make_profile
51 |
52 | @pytest.mark.asyncio
53 | async def test_create(self, make_profile):
54 | profile, profile_cls = await make_profile()
55 |
56 | profile_to_check = (await profile_cls.query(id_=profile.id))[0]
57 | assert profile == profile_to_check
58 |
59 | @pytest.mark.asyncio
60 | async def test_update(self, make_profile, name, remark):
61 | profile, profile_cls = await make_profile()
62 |
63 | await profile.update(name=f'new{name}')
64 | profile_to_check = (await profile_cls.query(id_=profile.id))[0]
65 | assert profile == profile_to_check
66 |
67 | @pytest.mark.asyncio
68 | async def test_delete(self, make_profile):
69 | profile, profile_cls = await make_profile()
70 | await profile.delete()
71 |
72 | try:
73 | query = (await profile_cls.query(id_=profile.id))[0]
74 | assert not query
75 | except IndexError:
76 | pass
77 |
78 | @pytest.mark.asyncio
79 | async def test_move(self, make_profile, name, remark):
80 | profile, profile_cls = await make_profile()
81 |
82 | name = profile.group.name
83 | group = await Group.create(name=f'new{name}', remark=remark)
84 |
85 | group_id_before = profile.group.id
86 |
87 | await profile.move(group=group)
88 | assert group_id_before != profile.group.id
89 |
90 | @pytest.mark.asyncio
91 | async def test_get_browser(self, make_profile):
92 | profile, profile_cls = await make_profile()
93 |
94 | async with profile as browser:
95 | if isinstance(browser, WebDriver):
96 | browser.get('https://google.com')
97 | else:
98 | page = browser.pages[0]
99 | await page.goto('https://google.com')
100 |
--------------------------------------------------------------------------------
/tests/test_sync_api.py:
--------------------------------------------------------------------------------
1 | from typing import Callable
2 | import pytest
3 | from selenium.webdriver.chrome.webdriver import WebDriver
4 | from adspower.sync_api import Group, Category, HTTPClient
5 | from adspower.sync_api.selenium import Profile as ProfileSelenium
6 | from adspower.sync_api.playwright import Profile as ProfilePlaywright
7 |
8 | HTTPClient.set_delay(1.1)
9 | ProfileType = ProfileSelenium | ProfilePlaywright
10 |
11 |
12 | class TestGroup:
13 | def test_create(self, name, remark):
14 | group = Group.create(name=name, remark=remark)
15 | group_to_check = Group.query(name=name)[0]
16 | assert group == group_to_check
17 |
18 | def test_update(self, name, remark):
19 | group = Group.create(name=name, remark=remark)
20 | group.update(name=f'new{name}', remark='new_remark')
21 | group_to_check = Group.query(name=f'new{name}')[0]
22 | assert group == group_to_check
23 |
24 |
25 | class TestCategory:
26 | def test_query(self, category_name):
27 | categories = Category.query()
28 | assert len(categories) > 0
29 |
30 |
31 | @pytest.mark.parametrize('profile_cls', [ProfileSelenium, ProfilePlaywright])
32 | class TestProfile:
33 | @pytest.fixture(scope="function")
34 | def make_profile(
35 | self,
36 | profile_cls,
37 | get_name,
38 | get_remark
39 | ) -> Callable[[], tuple[ProfileType, type[ProfileType]]]:
40 | def _make_profile() -> tuple[ProfileType, type[ProfileType]]:
41 | group = Group.create(name=get_name(), remark=get_remark())
42 | profile = profile_cls.create(name=get_name(), remark=get_remark(), group=group)
43 | return profile, profile_cls
44 |
45 | return _make_profile
46 |
47 | def test_create(self, make_profile):
48 | profile, profile_cls = make_profile()
49 |
50 | profile_to_check = profile_cls.query(id_=profile.id)[0]
51 | assert profile == profile_to_check
52 |
53 | def test_update(self, make_profile, name, remark):
54 | profile, profile_cls = make_profile()
55 |
56 | profile.update(name=f'new{name}')
57 | profile_to_check = profile_cls.query(id_=profile.id)[0]
58 | assert profile == profile_to_check
59 |
60 | def test_delete(self, make_profile):
61 | profile, profile_cls = make_profile()
62 | profile.delete()
63 |
64 | try:
65 | query = profile_cls.query(id_=profile.id)[0]
66 | assert not query
67 | except IndexError:
68 | pass
69 |
70 | def test_move(self, make_profile, name, remark):
71 | profile, profile_cls = make_profile()
72 |
73 | name = profile.group.name
74 | group = Group.create(name=f'new{name}', remark=remark)
75 |
76 | group_id_before = profile.group.id
77 |
78 | profile.move(group=group)
79 | assert group_id_before != profile.group.id
80 |
81 | def test_get_browser(self, make_profile):
82 | profile, profile_cls = make_profile()
83 |
84 | with profile as browser:
85 | if isinstance(browser, WebDriver):
86 | browser.get('https://google.com')
87 | else:
88 | page = browser.pages[0]
89 | page.goto('https://google.com')
90 |
--------------------------------------------------------------------------------
/tests/utils.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CrocoFactory/adspower/d32a8f2a4953eab6619f082e254f6d84c33bd90f/tests/utils.py
--------------------------------------------------------------------------------