├── 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 | [![Python versions](https://img.shields.io/pypi/pyversions/adspower?color=%231D4DFF)](https://pypi.org/project/adspower/) 6 | [![PyPi Version](https://img.shields.io/pypi/v/adspower?color=%231D4DFF)](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 --------------------------------------------------------------------------------