├── .gitattributes ├── .gitignore ├── LICENSE ├── Pipfile ├── Pipfile.lock ├── README.md ├── pybrowser ├── __init__.py ├── api.py ├── common_utils.py ├── constants.py ├── decorators.py ├── downloader.py ├── driver_handler.py ├── elements │ ├── __init__.py │ ├── actions.py │ ├── button.py │ ├── checkbox.py │ ├── element.py │ ├── file.py │ ├── form.py │ ├── input.py │ ├── link.py │ ├── radio.py │ ├── select.py │ └── utils.py ├── exceptions.py ├── external │ ├── htmlrenderer.py │ ├── parse.py │ └── utils.py ├── htmlm.py ├── listeners.py ├── log_adapter.py └── requester.py ├── readthedocs ├── Makefile ├── build │ ├── doctrees │ │ ├── environment.pickle │ │ └── index.doctree │ └── html │ │ ├── .buildinfo │ │ ├── _sources │ │ ├── index.rst.txt │ │ └── index.txt │ │ ├── _static │ │ ├── ajax-loader.gif │ │ ├── alabaster.css │ │ ├── basic.css │ │ ├── comment-bright.png │ │ ├── comment-close.png │ │ ├── comment.png │ │ ├── custom.css │ │ ├── doctools.js │ │ ├── documentation_options.js │ │ ├── down-pressed.png │ │ ├── down.png │ │ ├── file.png │ │ ├── jquery-1.11.1.js │ │ ├── jquery-3.2.1.js │ │ ├── jquery.js │ │ ├── language_data.js │ │ ├── minus.png │ │ ├── plus.png │ │ ├── pygments.css │ │ ├── searchtools.js │ │ ├── underscore-1.3.1.js │ │ ├── underscore.js │ │ ├── up-pressed.png │ │ ├── up.png │ │ └── websupport.js │ │ ├── genindex.html │ │ ├── index.html │ │ ├── objects.inv │ │ ├── search.html │ │ └── searchindex.js ├── make.bat └── source │ ├── conf.py │ └── index.rst ├── requirements.txt ├── setup.py └── tests ├── action_tests.py ├── browsernav_tests.py ├── browsers_tests.py ├── content_tests.py ├── downloader_tests.py ├── element_tests.py ├── file_tests.py ├── form_tests.py ├── html_tests.py ├── listener_tests.py ├── requests_tests.py ├── screenshot_tests.py └── test_only.py /.gitattributes: -------------------------------------------------------------------------------- 1 | * linguist-vendored 2 | *.py linguist-vendored=false 3 | readthedocs/* linguist-documentation 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | __pycache__/ 3 | .vscode/ 4 | dist/ 5 | get_pybrowser.egg-info/ 6 | build/ 7 | reqs.txt 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ranjith 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. 22 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | selenium = "==3.141.0" 8 | requests = "*" 9 | pyppeteer = "==0.0.25" 10 | pyquery = "==1.4.0" 11 | 12 | [dev-packages] 13 | sphinx = "*" 14 | pylint = "*" 15 | twine = "*" 16 | 17 | [requires] 18 | python_version = "3.7" 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pybrowser 2 | 3 | For documentation, refer [here](https://pybrowser.readthedocs.io/en/latest/) 4 | 5 | Here is a quick example, 6 | 7 | ```python 8 | from pybrowser import Browser 9 | with Browser(browser_name=Browser.CHROME) as b: 10 | b.goto("https://www.google.com/") 11 | b.input("name:=q").enter("news") 12 | b.button("name:=btnK").click() 13 | screenshot_path = b.take_screenshot() 14 | print(b.html().elements.links()) 15 | ``` 16 | 17 | ## Release Notes: 18 | 19 | **0.2.0** 20 | --------- 21 | 22 | * **Changes** 23 | 24 | * Changes in underlying `Browser.requests_session` object, 25 | 26 | * `requests_session.result` has been removed. There is just `requests_session.response` which is a blocking call (in case `asynch` flag is set). Also note, `requests_session.is_request_done` is still available to see if request is complete of not. There are no changes to other properties and are blocking in case of asynchronous call. 27 | 28 | * `requests_session.content()` method with options for bytes and text has been changed to properties, just like in underlying requests. That is now you call, `requests_session.content` for bytes and `requests_session.text` for text. 29 | 30 | * In context of above changes, `Browser` object also has below changes, 31 | 32 | * `Browser.get` and `Browser.post` now returns `requests_session` object (used to be `requests_session.response`). 33 | * `Browser.content()` has been changed to properties. That is now you call, `Browser.content` for bytes and `Browser.text` for text. 34 | 35 | * **New features** 36 | 37 | * Support for remote url. Please note this requires [Selenium Grid](https://www.seleniumhq.org/docs/07_selenium_grid.jsp) to be setup explicitly. Once done use the URL here. 38 | 39 | * Flags for Opera browser (`Browser.OPERA`). Webdriver executable needs to be present in `driver_path`. 40 | Please note `EDGE` and `SAFARI` are also supported the same way. That is, webdriver isn't automatically downloaded, instead path needs to be provided. 41 | 42 | **0.1.0** 43 | --------- 44 | 45 | * **Changes** 46 | 47 | **Please note some key changes with regards to properties changing to methods**. 48 | 49 | * Methods which were properties in initial release are now method calls. Below is the impacted list. 50 | 51 | In Browser, 52 | 53 | * `Browser.refresh()` 54 | * `Browser.back()` 55 | * `Browser.forward()` 56 | * `Browser.maximize_window()` 57 | * `Browser.minimize_window()` 58 | * `Browser.fullscreen_window()` 59 | * `Browser.delete_all_cookies()` 60 | * `Browser.close()` 61 | 62 | In Action (used by all elements), 63 | 64 | * `Action.refresh()` 65 | * `Action.highlight()` 66 | * `Action.double_click()` 67 | * `Action.move_to_element()` 68 | 69 | Specific elements also, 70 | 71 | * `Checkbox.check()` 72 | * `Checkbox.uncheck()` 73 | * `Radio.select()` 74 | * `Radio.unselect()` 75 | * `Input.clear()` 76 | 77 | * Browser class `__init__` method now has more options, 78 | 79 | * `firefox_binary_path` 80 | * `firefox_profile_path` 81 | * `http_proxy` 82 | * `driver_path` 83 | 84 | * Select element has below method changes, 85 | 86 | `Select.options(get="text")` method has been split to multiple properties to keep it simple, 87 | 88 | * `Select.options_text` 89 | * `Select.options_value` 90 | * `Select.options_element` 91 | 92 | Similar change has been done for `Select.all_selected_options(get="text")`, 93 | 94 | * `Select.all_selected_options_text` 95 | * `Select.all_selected_options_value` 96 | * `Select.all_selected_options_element` 97 | 98 | * File element changes, 99 | 100 | Below enhancements have been made to download feature, 101 | 102 | * Added more parameters - 103 | 104 | * `unzip` - Set this flag to unzip file. Default is `False` 105 | * `del_zipfile` - Set this flag to delete zip file after it has been unzipped. Default is `False` 106 | * `add_to_ospath` - Set this flag to add directory to `PATH`. Default is `False` 107 | 108 | * New properties - 109 | 110 | As you might already know, download happens in background (asynchronous) by default and can of course be changed with `asynch`. To check if download was successful, below properties are available, 111 | 112 | * `is_download_complete` - `True` or `False` 113 | * `downloaded_files` - list of downloaded files 114 | 115 | * HTML links method has below method changes, 116 | 117 | This is the one you would invoke via Browser as `Browser.html().elements.link()`. 118 | 119 | * Added more parameters - 120 | 121 | * `images` - You can filter out images. Default is `False` that means to include `images`, you will need to set this to `True` 122 | * `name_length_limit` - This limits the length of name of the url. Default is 60 123 | 124 | * Change in return type. Before this change, return type was a list of types (name, url). This has been changed to list of [named_tuples](https://docs.python.org/3.7/library/collections.html#collections.namedtuple) of form `('Link', ['name', 'url'])` 125 | 126 | * Changes in some env options. 127 | 128 | Removed below options as they don't have sense. Either you provide complete driver download url or provide version 129 | so that the API tries to download, 130 | 131 | * `CHROME_FILENAME` 132 | * `IE_FILENAME` 133 | * `CHROMEDRIVER_DEFAULT_VERSION` 134 | 135 | Added below for Firefox support, 136 | 137 | * `FIREFOX_HOME_URL` 138 | * `FIREFOX_DOWNLOAD_URL` 139 | * `FIREFOXDRIVER_VERSION` 140 | 141 | * Refactoring (non functional) in code and tests. Lot more testing needed still :-\ 142 | 143 | * **New features** 144 | 145 | * Support for Firefox ! 146 | 147 | **0.0.1** 148 | --------- 149 | 150 | * Very first release ! -------------------------------------------------------------------------------- /pybrowser/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'ranjith' 2 | __version__ = '0.2.0' 3 | 4 | from .api import Browser 5 | -------------------------------------------------------------------------------- /pybrowser/api.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver 4 | from selenium.webdriver.support.events import EventFiringWebDriver 5 | from selenium.webdriver.support import expected_conditions as EC 6 | from .elements.button import Button 7 | from .elements.input import Input 8 | from .elements.link import Link 9 | from .elements.radio import Radio 10 | from .elements.checkbox import Checkbox 11 | from .elements.select import Select 12 | from .elements.element import Element 13 | from .elements.form import Form 14 | from .elements.file import File 15 | from .elements.utils import wait_until, find_element_for_locator 16 | from .constants import CONSTANTS 17 | from .requester import Requester 18 | from .htmlm import HTML 19 | from .log_adapter import get_logger, log_path 20 | from .exceptions import InvalidArgumentError 21 | from .listeners import ExceptionListener 22 | from .driver_handler import BaseDriverHandler 23 | from .common_utils import (hash_, get_unique_filename_from_url, get_user_home_dir, make_dir, dir_filename, uuid1_as_str) 24 | 25 | #TODO: caching driver versions to already downloaded 26 | #TODO: use selenium servers as utility & hook 27 | 28 | class Browser(object): 29 | #Various browsers 30 | IE = "ie" 31 | CHROME = "chrome" 32 | FIREFOX = "firefox" 33 | EDGE = "edge" 34 | SAFARI = "safari" 35 | OPERA = "opera" 36 | 37 | def __init__(self, browser_name=None, incognito=False, headless=False, browser_options_dict=None, 38 | browser_options_args=None, http_proxy=None, screenshot_on_exception=True, silent_fail=False, 39 | firefox_binary_path=None, firefox_profile_path=None, driver_path=None, remote_url=None, wait_time=10): 40 | self.screenshot_on_exception = screenshot_on_exception 41 | #TODO: this currently has no effect. Once on_exception is handled in selenium api, will start to work 42 | self.silent_fail = silent_fail 43 | self.incognito = incognito 44 | self.headless = headless 45 | if browser_options_args and (not isinstance(browser_options_args, list)): 46 | browser_options_args = [browser_options_args] 47 | self.browser_options_args = browser_options_args 48 | self.browser_options_dict = browser_options_dict 49 | self.firefox_binary_path = firefox_binary_path 50 | self.firefox_profile_path = firefox_profile_path 51 | self._driver = None 52 | self._url = None 53 | self.http_proxy = http_proxy 54 | self.content_hash = None 55 | self.wait_time = wait_time 56 | self.remote_url = remote_url 57 | self.logger = get_logger() 58 | self._content_session = Requester() 59 | self._set_driver(browser_name, driver_path) 60 | self._presets() 61 | 62 | def __enter__(self): 63 | return self 64 | 65 | def __exit__(self, *args): 66 | self.close() 67 | 68 | def _set_driver(self, browser_name, driver_path): 69 | #browser_name of None is allowed to permit access to other features such as html/get/post 70 | if browser_name is None: 71 | return 72 | #TODO: hate passing self, need better solution 73 | self._driver = BaseDriverHandler.create_driver(browser_name, driver_path, self) 74 | if self._driver and self.screenshot_on_exception: 75 | filename = f"Exception_{uuid1_as_str()}.png" 76 | self._driver = EventFiringWebDriver(self._driver, ExceptionListener(self, filename)) 77 | 78 | def _presets(self): 79 | if self._driver is None: 80 | return 81 | try: 82 | self.maximize_window() 83 | self.execute_script("document.body.style.zoom='100%'") 84 | self.switch_to.window(self._driver.current_window_handle) 85 | except Exception as e: 86 | self.logger.warning(str(e)) 87 | #print(str(e)) 88 | 89 | @property 90 | def driver(self): 91 | if isinstance(self._driver, EventFiringWebDriver): 92 | return self._driver.wrapped_driver 93 | return self._driver 94 | 95 | @driver.setter 96 | def driver(self, driver): 97 | if isinstance(driver, RemoteWebDriver): 98 | self._driver = driver 99 | return 100 | raise InvalidArgumentError("Not a valid driver object") 101 | 102 | #TODO : not sure page load is complete at this point, perhaps need a better solution 103 | def goto(self, url): 104 | if self._driver is None: 105 | return 106 | self._driver.get(url) 107 | self._set_url_and_hash() 108 | return self 109 | 110 | def back(self): 111 | if self._driver is None: 112 | return 113 | self._driver.back() 114 | self._set_url_and_hash() 115 | return self 116 | 117 | def forward(self): 118 | if self._driver is None: 119 | return 120 | self._driver.forward() 121 | self._set_url_and_hash() 122 | return self 123 | 124 | def refresh(self): 125 | if self._driver is None: 126 | return 127 | self._driver.refresh() 128 | self._set_url_and_hash() 129 | return self 130 | 131 | def maximize_window(self): 132 | if self._driver is None: 133 | return 134 | self._driver.maximize_window() 135 | return self 136 | 137 | def minimize_window(self): 138 | if self._driver is None: 139 | return 140 | self._driver.minimize_window() 141 | return self 142 | 143 | def fullscreen_window(self): 144 | if self._driver is None: 145 | return 146 | self._driver.fullscreen_window() 147 | return self 148 | 149 | @property 150 | def url(self): 151 | if self._driver is None: 152 | return 153 | return self._driver.current_url 154 | 155 | @property 156 | def title(self): 157 | if self._driver is None: 158 | return 159 | return self._driver.title 160 | 161 | @property 162 | def switch_to(self): 163 | if self._driver is None: 164 | return 165 | return self._driver.switch_to 166 | 167 | def _set_url_and_hash(self, url=None): 168 | self._url = url or (self._driver.current_url if self._driver else self._url) 169 | if self._driver is None: 170 | return 171 | self.content_hash = self._current_page_content_hash 172 | 173 | @property 174 | def _current_page_content_hash(self): 175 | if self._driver is None: 176 | return 177 | return hash_(self._driver.page_source) 178 | 179 | def button(self, locator, wait_time=None, visible=False): 180 | wait_time = wait_time or self.wait_time 181 | return Button(self._driver, locator=locator, wait_time=wait_time, visible=visible) 182 | 183 | def link(self, locator, wait_time=None, visible=False): 184 | wait_time = wait_time or self.wait_time 185 | return Link(self._driver, locator, wait_time=wait_time, visible=visible) 186 | 187 | def input(self, locator, wait_time=None, visible=False): 188 | wait_time = wait_time or self.wait_time 189 | return Input(self._driver, locator, wait_time=wait_time, visible=visible) 190 | 191 | def radio(self, locator, wait_time=None, visible=False): 192 | wait_time = wait_time or self.wait_time 193 | return Radio(self._driver, locator, wait_time=wait_time, visible=visible) 194 | 195 | def checkbox(self, locator, wait_time=None, visible=False): 196 | wait_time = wait_time or self.wait_time 197 | return Checkbox(self._driver, locator, wait_time=wait_time, visible=visible) 198 | 199 | def select(self, locator, wait_time=None, visible=False): 200 | wait_time = wait_time or self.wait_time 201 | return Select(self._driver, locator, wait_time=wait_time, visible=visible) 202 | 203 | def element(self, locator, wait_time=None, visible=False): 204 | wait_time = wait_time or self.wait_time 205 | return Element(self._driver, locator, wait_time=wait_time, visible=visible) 206 | 207 | def form(self, locator, wait_time=None, visible=False): 208 | wait_time = wait_time or self.wait_time 209 | return Form(self._driver, locator, wait_time=wait_time, visible=visible) 210 | 211 | def file(self, locator, wait_time=None, visible=False): 212 | wait_time = wait_time or self.wait_time 213 | return File(self._driver, locator, wait_time=wait_time, visible=visible) 214 | 215 | @property 216 | def cookies(self): 217 | if self._driver: 218 | return self._driver.get_cookies() 219 | self._do_get(url=self._url) # assuming get ! 220 | resp = self._content_session.response 221 | cookies = [] 222 | if resp: 223 | cookies_jar = resp.cookies 224 | if not cookies_jar: 225 | return cookies 226 | names = self._cookie_attr_names() 227 | for c in cookies_jar: 228 | cookies.append(self._get_dict_for_attrs(c, names)) 229 | return cookies 230 | 231 | def _cookie_attr_names(self): 232 | attr_names = ("version", "name", "value", 233 | "port", "port_specified", 234 | "domain", "domain_specified", "domain_initial_dot", 235 | "path", "path_specified", 236 | "secure", "expires", "discard", "comment", "comment_url" 237 | ) 238 | return attr_names 239 | 240 | def _get_dict_for_attrs(self, obj, attr_names): 241 | d = {} 242 | for name in attr_names: 243 | if hasattr(obj, name): 244 | d[name] = getattr(obj, name) 245 | return d 246 | 247 | def delete_all_cookies(self): 248 | if self._driver is None: 249 | return 250 | self._driver.delete_all_cookies() 251 | return self 252 | 253 | def delete_cookie(self, name): 254 | if self._driver is None: 255 | return 256 | self._driver.delete_cookie(name) 257 | return self 258 | 259 | def add_cookie(self, cookie_dict): 260 | if self._driver is None: 261 | return 262 | self._driver.add_cookie(cookie_dict) 263 | return self 264 | 265 | def _do_get(self, url=None): 266 | url2 = url or (self._driver.current_url if self._driver else self._url) 267 | if not url2: 268 | raise InvalidArgumentError("url is mandatory, please navigate to a url first or provide one") 269 | if ((self._url != url2) or (not self._content_session.response) or (self._content_session._req_url != url2) 270 | or (self.content_hash != self._current_page_content_hash)): 271 | self._content_session.get(url=url2) 272 | self._set_url_and_hash(url=url2) 273 | 274 | def html(self, url=None, print_style=False, print_js=False, remove_tags=None): 275 | if self._driver and self._driver.page_source and (not url): 276 | return HTML(self._driver.page_source, url=self._driver.current_url, print_style=print_style, 277 | print_js=print_js, remove_tags=remove_tags) 278 | self._do_get(url=url) 279 | return HTML(self._content_session.content(), url=self._url, print_style=print_style, 280 | print_js=print_js, remove_tags=remove_tags) 281 | 282 | @property 283 | def content(self): 284 | if self._driver: 285 | c = self._driver.page_source or b"" 286 | return c.encode(errors="ignore") 287 | self._do_get(url=self._url) # assuming get ! 288 | return self._content_session.content 289 | 290 | @property 291 | def text(self): 292 | if self._driver: 293 | c = self._driver.page_source or "" 294 | return c 295 | self._do_get(url=self._url) # assuming get ! 296 | return self._content_session.text 297 | 298 | @property 299 | def json(self): 300 | if self._driver: 301 | c = self._driver.page_source 302 | try: 303 | j = json.loads(c) 304 | return j 305 | except: 306 | pass # not json 307 | self._do_get(url=self._url) # assuming get ! 308 | return self._content_session.json 309 | 310 | @property 311 | def response_headers(self): 312 | ''' #doesn't work ! 313 | if self._driver: 314 | script = "new Response().headers.entries();" 315 | resp = self.execute_script(script) 316 | if resp: 317 | return str(resp) 318 | ''' 319 | self._do_get(url=self._url) # assuming get ! 320 | return self._content_session.response_headers 321 | 322 | @property 323 | def response_code(self): 324 | if self._driver: 325 | script = "new Response().status;" 326 | resp = self.execute_script(script) 327 | if resp: 328 | return int(resp) 329 | self._do_get(url=self._url) # assuming get ! 330 | return self._content_session.response_code 331 | 332 | @property 333 | def response_encoding(self): 334 | self._do_get(url=self._url) # assuming get ! 335 | return self._content_session.response_encoding 336 | 337 | def get(self, url=None, asynch=False, headers=None, cookies=None, **kwargs): 338 | self._url = url = url or (self._driver.current_url if self._driver else self._url) 339 | if not url: 340 | raise InvalidArgumentError("url is mandatory, please navigate to a url first or provide one") 341 | return self._content_session.get(url=url, future=asynch, headers=headers, cookies=cookies, **kwargs) 342 | 343 | def post(self, url=None, asynch=False, body=None, headers=None, cookies=None, **kwargs): 344 | self._url = url = url or (self._driver.current_url if self._driver else self._url) 345 | if not url: 346 | raise InvalidArgumentError("url is mandatory, please navigate to a url first or provide one") 347 | return self._content_session.post(url=url, future=asynch, body=body, headers=headers, cookies=cookies, **kwargs) 348 | 349 | @property 350 | def requests_session(self): 351 | return self._content_session.req_session 352 | 353 | def take_screenshot(self, filename=None): 354 | if self._driver and self._driver.current_url: 355 | final_path = self._get_filename_from_url(filename) 356 | self._driver.get_screenshot_as_file(filename=final_path) 357 | return final_path 358 | 359 | def _get_filename_from_url(self, filename=None): 360 | url = self._driver.current_url if self.driver else None 361 | if not url: 362 | url = self._url 363 | if filename is None: 364 | f = get_unique_filename_from_url(url, ext="png") 365 | p = CONSTANTS.DIR_PATH or get_user_home_dir() 366 | d = os.path.join(p, CONSTANTS.DIR_NAME, CONSTANTS.SCREENSHOTS_DIR) 367 | else: 368 | d, f = dir_filename(filename, default_ext="png") 369 | if not d: 370 | p = CONSTANTS.DIR_PATH or get_user_home_dir() 371 | d = os.path.join(p, CONSTANTS.DIR_NAME, CONSTANTS.SCREENSHOTS_DIR) 372 | if not f: 373 | f = get_unique_filename_from_url(url, ext="png") 374 | make_dir(d) 375 | #final path 376 | return os.path.join(d, f) if (d and f) else None 377 | 378 | def execute_script(self, script): 379 | if self._driver and self._driver.current_url and script: 380 | return self._driver.execute_script(script) 381 | 382 | def wait_for_page_load(self, wait_time=None): 383 | def _wait(driver): 384 | script = "return document.readyState" 385 | COMPLETE = "complete" 386 | status = driver.execute_script(script) 387 | if status and status.lower() == COMPLETE: 388 | return True 389 | return False 390 | if self._driver is None: 391 | return 392 | wait_time = wait_time or self.wait_time 393 | try: 394 | wait_until(self._driver, wait_time, _wait) 395 | except Exception: 396 | self.logger.error("Page load failed") 397 | raise 398 | 399 | def wait_for_element(self, locator, visible=False, wait_time=None): 400 | if self._driver is None: 401 | return 402 | wait_time = wait_time or self.wait_time 403 | try: 404 | find_element_for_locator(self._driver, locator, wait_time=wait_time, visible=visible) 405 | except Exception as e: 406 | self.logger.error("Element not found in wait_for_element method : " + str(e)) 407 | raise 408 | 409 | def close(self): 410 | if self._driver: 411 | try: self._driver.close() 412 | except: pass 413 | try: self._driver.quit() 414 | except: pass 415 | if self._content_session: 416 | try: self._content_session.close() 417 | except: pass 418 | -------------------------------------------------------------------------------- /pybrowser/common_utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | import glob 4 | import sys 5 | import shutil 6 | import json 7 | import hashlib 8 | import re 9 | import uuid 10 | try: 11 | from urllib.parse import urlparse, unquote 12 | except ImportError: 13 | from urlparse import urlparse, unquote 14 | from .constants import CONSTANTS 15 | 16 | def get_user_home_dir(user=None): 17 | home_dir = CONSTANTS.DIR_PATH 18 | if home_dir and os.path.isdir(home_dir): 19 | return os.path.abspath(home_dir) 20 | if user: 21 | home_dir = os.path.expanduser(f"~{user}") 22 | if os.path.isdir(home_dir): 23 | return home_dir 24 | home_dir = os.getenv('HOME') or os.path.expanduser(os.getenv('USERPROFILE')) 25 | return home_dir 26 | 27 | #Ripped from pipenv 28 | def is_valid_url(url): 29 | if not url: 30 | return False 31 | """Checks if a given string is an url""" 32 | try: 33 | pieces = urlparse(url) 34 | return all([pieces.scheme, pieces.netloc]) 35 | except Exception: 36 | return False 37 | 38 | #TODO : also handle src is directory 39 | def copy_file(src, dst, overwrite=True): 40 | if not os.path.isfile(src): 41 | return 42 | if overwrite: 43 | shutil.copy2(src, dst) 44 | else: 45 | if os.path.isfile(dst): 46 | return 47 | elif os.path.isdir(dst): 48 | f = os.path.basename(src) 49 | if os.path.isfile(os.path.join(dst, f)): 50 | return 51 | shutil.copy2(src, dst) 52 | 53 | def rm_files(files): 54 | if not isinstance(files, list): 55 | files = [files] 56 | for f in files: 57 | if os.path.isfile(f): 58 | os.remove(f) 59 | 60 | #returns the SHA256 hash 61 | def hash_(data): 62 | if not data: 63 | return 64 | content = data 65 | #handle filenames 66 | try: 67 | if os.path.isfile(data): 68 | with open(data, "rb") as fd: 69 | content = fd.read() 70 | except: 71 | pass 72 | #handle json, dicts etc 73 | if isinstance(data, dict): 74 | content = json.dumps(data, sort_keys=True, separators=(",", ":")) 75 | if isinstance(content, bytes): 76 | return hashlib.sha256(content).hexdigest() 77 | return hashlib.sha256(content.encode("utf8")).hexdigest() 78 | 79 | def guess_filename_from_url(url, has_ext=True): 80 | if not url: 81 | return 82 | s = url.rsplit("/") 83 | if s and len(s) > 0: 84 | filename = s[-1] 85 | filename = unquote(filename) 86 | filename = "_".join(filename.split()) 87 | if not has_ext: 88 | return filename 89 | #return filename only if it is of the format #.# 90 | ws = [w for w in filename.split(".") if w.strip()] 91 | if len(ws) > 1: 92 | return filename 93 | 94 | def add_to_osenv(name, path, overwrite=True): 95 | if (not overwrite) and in_osenv(name): 96 | return 97 | try: 98 | os.environ[name] = path 99 | except (OSError, KeyError): 100 | pass 101 | 102 | #Adds a given path to the PATH if not already present 103 | def add_to_path(p): 104 | if in_path(p): 105 | return 106 | try: 107 | os.environ['PATH'] = f"{p}{os.pathsep}{os.environ.get('PATH', '')}" 108 | except (OSError, KeyError): 109 | pass 110 | 111 | def in_path(p): 112 | return p in os.environ.get('PATH', '').split(os.pathsep) 113 | 114 | def in_osenv(name): 115 | return True if os.environ.get(name, None) else False 116 | 117 | def path_exists(p): 118 | if not p: 119 | return False 120 | if os.path.isdir(p): 121 | return True 122 | return file_exists(p) 123 | 124 | def file_exists(f): 125 | if not f: 126 | return False 127 | if os.path.isfile(f): 128 | return True 129 | #handle filename without extensions 130 | d = os.path.dirname(f) 131 | b = os.path.basename(f) 132 | if not(d and b): 133 | return False 134 | for fname in serve_files_in_dir(d, ext=False): 135 | if fname == b: 136 | return True 137 | return False 138 | 139 | def dir_filename(p, default_ext=None): 140 | BLANK = "" 141 | if not p: 142 | return 143 | if os.path.isdir(p): 144 | return p, BLANK 145 | d = os.path.dirname(p) or BLANK 146 | b = os.path.basename(p) or BLANK 147 | if b: 148 | b = os.path.splitext(b) 149 | if default_ext and not b[1]: 150 | b = (b[0], default_ext) 151 | if len(b) == 2: 152 | b = f"{b[0]}.{b[1].strip('.')}" 153 | else: 154 | b = b[0] 155 | return d, b 156 | 157 | def serve_files_in_dir(p, ext=True): 158 | if not os.path.isdir(p): 159 | return 160 | for f in os.listdir(p): 161 | if os.path.isfile(os.path.join(p, f)): 162 | if ext: 163 | yield f 164 | else: 165 | yield os.path.splitext(f)[0] 166 | 167 | def file_exists_in_path(directory, filename): 168 | if in_path(directory) and path_exists(os.path.join(directory, filename)): 169 | return True 170 | return False 171 | 172 | def file_exists_but_not_in_path(directory, filename): 173 | if not in_path(directory) and path_exists(os.path.join(directory, filename)): 174 | return True 175 | return False 176 | 177 | def find_patterns_in_str(p, s, first=False): 178 | empty = lambda : "" if first else [] 179 | if not isinstance(p, str) or not isinstance(s, str): 180 | return empty() 181 | pc = re.compile(p) 182 | l = re.findall(pc, s) 183 | if not l: 184 | return empty() 185 | if first: 186 | return l[0] 187 | return l 188 | 189 | def os_name(): 190 | if os.name.lower() == "nt": 191 | return "windows" 192 | elif os.name.lower() == "posix": 193 | return "mac" 194 | else: 195 | return "linux" 196 | 197 | def os_bits(): 198 | #might not work in all OSes 199 | machine = platform.machine() 200 | return 64 if machine.endswith("64") else 32 201 | 202 | def set_winreg_fromlocalmachine(path, name, value, overwrite=False, dword=True): 203 | #needed only for windows 204 | if "window" not in os_name().lower(): 205 | return 206 | #ignore and return incase winreg isn't available 207 | try: 208 | import winreg 209 | except ImportError: 210 | return 211 | key_type = winreg.REG_DWORD if dword else winreg.REG_SZ 212 | try: 213 | current_value = get_winreg_fromlocalmachine(winreg, path, name) 214 | if current_value and (not overwrite): 215 | return current_value 216 | k = winreg.CreateKeyEx(winreg.HKEY_LOCAL_MACHINE, path, 0, winreg.KEY_CREATE_SUB_KEY | winreg.KEY_WOW64_64KEY) 217 | winreg.CloseKey(k) 218 | with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, path, 0, winreg.KEY_WRITE) as registry_entry: 219 | winreg.SetValueEx(registry_entry, name, 0, key_type, value) 220 | return value 221 | except (WindowsError, OSError): 222 | return None 223 | 224 | def get_winreg_fromlocalmachine(winreg, path, name): 225 | value = None 226 | try: 227 | with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, path, 0, winreg.KEY_READ) as registry_entry: 228 | value, type_id = winreg.QueryValueEx(registry_entry, name) 229 | except (WindowsError, OSError): 230 | pass 231 | return value 232 | 233 | def make_dir(name, mode=0o777): 234 | if not os.path.exists(name): 235 | os.makedirs(name, mode=mode) 236 | 237 | def uuid1_as_str(): 238 | return str(uuid.uuid1()).replace("-","_") 239 | 240 | def get_unique_filename_from_url(url, ext=None, length_limit=100): 241 | if not url: 242 | return 243 | pieces = urlparse(url) 244 | parts = [] 245 | if pieces.netloc: 246 | host = [n for n in pieces.netloc.split(".") 247 | if n and n.strip() and (not n.lower().startswith("ww"))] 248 | parts.append(host[0]) 249 | if pieces.path: 250 | paths = "_".join([p for p in pieces.path.split("/") if p and p.strip()]) 251 | parts.append(paths) 252 | filename = "_".join(parts) + "_" + uuid1_as_str() 253 | filename = unquote(filename) 254 | filename = "_".join(filename.split()) 255 | #limit filename 256 | filename = filename[:length_limit] 257 | if ext: 258 | return f"{filename}.{ext}" 259 | else: 260 | return filename -------------------------------------------------------------------------------- /pybrowser/constants.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | #Webdriver constants 4 | class CONSTANTS(): 5 | DOWNLOAD_DIR_NAME = os.environ.get('DRIVERS_DOWNLOAD_DIR_NAME') or "browserdrivers" 6 | CHROME_DRIVER = "CHROME_DRIVER" 7 | IE_DRIVER = "IE_DRIVER" 8 | FIREFOX_DRIVER = "FIREFOX_DRIVER" 9 | DEFAULT_LOGGER = os.environ.get('DEFAULT_LOGGER_NAME') or "pybrowser" 10 | DEFAULT_LOGGER_PATH = os.environ.get('DEFAULT_LOGGER_PATH') 11 | DIR_PATH = os.environ.get('PYBROWSER_HOME_DIR_PATH') 12 | DIR_NAME = os.environ.get('PYBROWSER_DIR_NAME') or "pybrowser" 13 | HTML_DIR = os.environ.get('HTML_DIR_NAME') or "html" 14 | SCREENSHOTS_DIR = os.environ.get('SCREENSHOTS_DIR_NAME') or "screenshots" 15 | PYPPETEER_DIR = os.environ.get('PYPPETEER_DIR_NAME') or "puppeteer" 16 | PYPPETEER_HOME = os.environ.get('PYPPETEER_HOME') 17 | #Chrome constants 18 | class CHROME_CONSTANTS(): 19 | #HOME_URL = "http://chromedriver.chromium.org/downloads" 20 | HOME_URL = os.environ.get('CHROME_HOME_URL') or "http://chromedriver.chromium.org/home" 21 | DOWNLOAD_URL = os.environ.get('CHROME_DOWNLOAD_URL') 22 | DOWNLOAD_URL_TEMPLATE = "https://chromedriver.storage.googleapis.com/{}/{}" 23 | FILENAME_TEMPLATE = "chromedriver_{}.zip" 24 | vers = os.environ.get('CHROMEDRIVER_VERSION') 25 | vers = int(vers) if vers else None 26 | VERSION = vers or 2.46 27 | #TODO: used when unable to pull from home url. TBI 28 | DEFAULT_VERSION = 2.46 29 | #IE constants 30 | class IE_CONSTANTS(): 31 | HOME_URL = os.environ.get('IE_HOME_URL') or "http://selenium-release.storage.googleapis.com/index.html?" 32 | DOWNLOAD_URL = os.environ.get('IE_DOWNLOAD_URL') 33 | DOWNLOAD_URL_TEMPLATE = "http://selenium-release.storage.googleapis.com/{}/{}" 34 | #TODO: check if win32 is ok 35 | FILENAME_TEMPLATE = "IEDriverServer_Win32_{}.0.zip" 36 | vers = os.environ.get('IEDRIVER_VERSION') 37 | vers = int(vers) if vers else None 38 | VERSION = vers or 3.14 39 | #Firefox constants 40 | class FIREFOX_CONSTANTS(): 41 | HOME_URL = os.environ.get('FIREFOX_HOME_URL') or "https://github.com/mozilla/geckodriver/releases" 42 | DOWNLOAD_URL = os.environ.get('FIREFOX_DOWNLOAD_URL') 43 | DOWNLOAD_URL_TEMPLATE = "https://github.com/mozilla/geckodriver/releases/download/{}/{}" 44 | FILENAME_TEMPLATE = "geckodriver-{}-{}.{}" 45 | vers = os.environ.get('FIREFOXDRIVER_VERSION') 46 | if vers: 47 | vers = str(vers) 48 | if vers.startswith("V"): 49 | vers = "v" + vers[1:] 50 | if not vers.startswith("v"): 51 | vers = "v" + vers 52 | VERSION = vers or "v0.24.0" 53 | #TODO: used when unable to pull from home url. TBI 54 | DEFAULT_VERSION = "v0.24.0" -------------------------------------------------------------------------------- /pybrowser/decorators.py: -------------------------------------------------------------------------------- 1 | from threading import Thread 2 | from types import MethodType 3 | from selenium.common.exceptions import NoSuchElementException 4 | from .exceptions import InvalidArgumentError 5 | from .log_adapter import get_logger 6 | 7 | class task_runner(object): 8 | 9 | def __init__(self, func): 10 | self.func = func 11 | self.background = False 12 | 13 | def __call__(self, *args, **kwargs): 14 | if self.background: 15 | thr = Thread(target=self.func, args=args, kwargs=kwargs) 16 | thr.start() 17 | else: 18 | return self.func(*args, **kwargs) 19 | 20 | def __get__(self, obj, clazz): 21 | if hasattr(obj, 'asynch'): 22 | self.background = obj.asynch 23 | return MethodType(self, obj) 24 | 25 | def action_wrapper(action_clazz): 26 | class action_handler(object): 27 | SKIP_ATTRS1 = ["if_found", "is_found"] 28 | SKIP_ATTRS2 = ["if_stale", "if_enabled", "if_displayed"] 29 | 30 | def __init__(self, *args, **kwargs): 31 | self.action_obj = action_clazz(*args, **kwargs) 32 | self.action_obj._deco_clazz = self #i know, lol 33 | 34 | def __getattr__(self, name): 35 | attr = None 36 | try: 37 | attr = self.action_obj.__getattribute__(name) 38 | except: 39 | pass 40 | if name in action_handler.SKIP_ATTRS1: 41 | return attr 42 | if self._exception_handler(): 43 | if name in action_handler.SKIP_ATTRS2: 44 | return attr 45 | if self.action_obj._if_enabled: 46 | if self._enabled_handler(): 47 | return attr 48 | elif self.action_obj._if_displayed: 49 | if self._displayed_handler(): 50 | return attr 51 | elif self.action_obj._if_stale: 52 | if self._stale_handler(): 53 | return attr 54 | else: 55 | return attr 56 | #TODO: this will be a problem in chained calls, but will deal with that later ! 57 | return action_handler._dummy_callable if callable(attr) else None 58 | 59 | @staticmethod 60 | def _dummy_callable(*args, **kwargs): 61 | pass 62 | 63 | def _exception_handler(self): 64 | if (not self.action_obj._if_found) and (not self.action_obj._element_found): 65 | get_logger().error("action_wrapper._exception_handler : Element not found to perform the action") 66 | raise NoSuchElementException("Element not found to perform the action") 67 | if self.action_obj._if_found: 68 | self.action_obj._if_found = False #reset flag 69 | if not self.action_obj._element_found: 70 | return False 71 | return True 72 | 73 | def _enabled_handler(self): 74 | self.action_obj._if_enabled = False #reset flag 75 | return True if self.action_obj.is_enabled else False 76 | 77 | def _displayed_handler(self): 78 | self.action_obj._if_displayed = False #reset flag 79 | return True if self.action_obj.is_displayed else False 80 | 81 | def _stale_handler(self): 82 | self.action_obj._if_stale = False #reset flag 83 | return True if self.action_obj.is_stale else False 84 | 85 | return action_handler -------------------------------------------------------------------------------- /pybrowser/downloader.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import zipfile 4 | try: 5 | import tarfile 6 | except ImportError: 7 | pass 8 | try: 9 | from urllib import request 10 | except ImportError: 11 | import urllib as request 12 | from requests import Session as HTTPSession 13 | from selenium import webdriver 14 | from selenium.webdriver.chrome.options import Options as ChromeOptions 15 | from .constants import CONSTANTS, CHROME_CONSTANTS, IE_CONSTANTS, FIREFOX_CONSTANTS 16 | from .exceptions import InvalidArgumentError, OperationFailedException 17 | from .common_utils import (get_user_home_dir, is_valid_url, copy_file, hash_, rm_files, 18 | guess_filename_from_url, add_to_path, find_patterns_in_str, os_name, 19 | make_dir, file_exists) 20 | from .htmlm import HTML 21 | from .log_adapter import get_logger 22 | from .decorators import task_runner 23 | 24 | #TODO: abstract away all usage of anything dealing with file systems. eg: os, platform, ntpath etc 25 | #TODO: user provided hash comparsion and security 26 | 27 | def download_driver(driver_name, version=None, download_filename=None, add_to_ospath=True, 28 | overwrite_existing=True): 29 | if driver_name == CONSTANTS.CHROME_DRIVER: 30 | webdriver_downloader.download_chrome(version=version, download_filename=download_filename, 31 | add_to_ospath=add_to_ospath, overwrite_existing=overwrite_existing) 32 | elif driver_name == CONSTANTS.IE_DRIVER: 33 | webdriver_downloader.download_ie(version=version, download_filename=download_filename, 34 | add_to_ospath=add_to_ospath, overwrite_existing=overwrite_existing) 35 | elif driver_name == CONSTANTS.FIREFOX_DRIVER: 36 | webdriver_downloader.download_firefox(version=version, download_filename=download_filename, 37 | add_to_ospath=add_to_ospath, overwrite_existing=overwrite_existing) 38 | else: 39 | get_logger().error(f"Unable to download {driver_name} driver at this point") 40 | 41 | def download_url(url, to_dir=None, download_filename=None, overwrite_existing=True, asynch=True, 42 | unzip=False, del_zipfile=False, add_to_ospath=False, callback=None): 43 | d = Downloader(from_url=url, to_dir=to_dir, download_filename=download_filename, 44 | overwrite_existing=overwrite_existing, asynch=asynch, unzip=unzip, 45 | del_zipfile=del_zipfile, add_to_ospath=add_to_ospath, callback=callback) 46 | d_files = d.download() 47 | return d_files 48 | 49 | class Downloader(object): 50 | 51 | @staticmethod 52 | def any_file_exists(files): 53 | if not isinstance(files, list): 54 | files = [files] 55 | for f in files: 56 | if file_exists(f): 57 | return True 58 | return False 59 | 60 | def __init__(self, from_url=None, to_dir=None, download_filename=None, unzip_filename=None, 61 | overwrite_existing=True, asynch=False, unzip=True, del_zipfile=True, add_to_ospath=False, callback=None): 62 | if not is_valid_url(from_url): 63 | get_logger().error(f"{__class__.__name__}: from_url is mandatory") 64 | raise InvalidArgumentError("from_url is mandatory and should be a valid url") 65 | self.from_url = from_url 66 | self.to_dir = to_dir or get_user_home_dir() 67 | self.overwrite_existing = overwrite_existing 68 | self.download_ok = False 69 | self.download_fullfilename = None 70 | if not download_filename: 71 | download_filename = guess_filename_from_url(from_url) 72 | if download_filename: 73 | self.download_fullfilename = os.path.join(self.to_dir, download_filename) 74 | if unzip_filename: 75 | self.unzip_fullfilename = os.path.join(self.to_dir, unzip_filename) 76 | self.unzip = unzip 77 | self.del_zipfile = del_zipfile 78 | self.add_to_ospath = add_to_ospath 79 | self.filehash = None 80 | self.downloaded_files = None 81 | self.asynch = asynch 82 | self.callback = callback 83 | 84 | def _downloadhook(self, blocknum, blocksize, totalsize): 85 | readsofar = blocknum * blocksize 86 | if totalsize > 0: 87 | percent = 100.0 if readsofar > totalsize else (readsofar * 100.0 / totalsize) 88 | s = f"\rdownloading...[{percent:.0f}%]" 89 | sys.stdout.write(s) 90 | if readsofar >= totalsize: # near the end 91 | sys.stdout.write("\n") 92 | self.download_ok = True 93 | else: # total size is unknown 94 | sys.stderr.write("read %d\n" % (readsofar,)) 95 | 96 | def _can_proceed(self): 97 | if not self.overwrite_existing: 98 | #print("self.unzip_fullfilename - " + self.unzip_fullfilename) 99 | if Downloader.any_file_exists([self.unzip_fullfilename, self.download_fullfilename]): 100 | return False 101 | return True 102 | 103 | def _unzip(self): 104 | f = self.download_fullfilename 105 | if not f: 106 | return 107 | extracted_names = [] 108 | extracted = False 109 | if zipfile.is_zipfile(f): 110 | with zipfile.ZipFile(f) as zf: 111 | zf.extractall(path=self.to_dir) 112 | extracted_names = zf.namelist() 113 | extracted = True 114 | elif f.endswith("tar.gz"): 115 | with tarfile.open(f, "r:gz") as tar: 116 | tar.extractall(path=self.to_dir) 117 | extracted_names = tar.getnames() 118 | extracted = True 119 | elif f.endswith("tar"): 120 | with tarfile.open(f, "r:") as tar: 121 | tar.extractall(path=self.to_dir) 122 | extracted_names = tar.getnames() 123 | extracted = True 124 | 125 | if extracted: 126 | self.downloaded_files = [os.path.join(self.to_dir, fl) for fl in extracted_names] 127 | if self.del_zipfile: 128 | rm_files(f) 129 | 130 | def _add_to_path(self): 131 | if not self.to_dir: 132 | return 133 | add_to_path(self.to_dir) 134 | 135 | #decorate download function for ability to run in background 136 | @task_runner 137 | def download(self): 138 | if self._can_proceed(): 139 | filename, headers = None, None 140 | try: 141 | hook = None 142 | if not self.asynch: 143 | hook = self._downloadhook 144 | filename, headers = request.urlretrieve(self.from_url, filename=self.download_fullfilename, 145 | reporthook=hook) 146 | self.download_ok = True 147 | except Exception as e: 148 | raise OperationFailedException(f"Download from {self.from_url} failed. Details - \n{str(e)}") 149 | 150 | self.filehash = hash_(filename) 151 | if (not self.download_fullfilename) and filename: 152 | bn = os.path.basename(filename) 153 | copy_file(filename, self.to_dir, overwrite=self.overwrite_existing) 154 | rm_files(filename) 155 | self.download_fullfilename = os.path.join(self.to_dir, bn) 156 | 157 | if self.download_ok: 158 | self.downloaded_files = [self.download_fullfilename] 159 | if self.unzip: 160 | #print("unzipping") 161 | self._unzip() 162 | else: 163 | raise OperationFailedException(f"Download from {self.from_url} failed") 164 | 165 | if self.add_to_ospath: 166 | self._add_to_path() 167 | if self.callback and callable(self.callback): 168 | self.callback(self.downloaded_files) 169 | return self.downloaded_files 170 | 171 | class WebdriverDownloader(Downloader): 172 | 173 | _OSMAP_CHROME = {'windows':"win32", 'mac':"mac64", 'linux':"linux64"} 174 | _OSMAP_FIREFOX = {'windows':"win64", 'mac':"macos", 'linux':"linux64"} 175 | _ZIPEXTMAP_FIREFOX = {'windows':"zip", 'mac':"tar.gz", 'linux':"tar.gz"} 176 | WEBDRIVERNAMES = {CONSTANTS.CHROME_DRIVER : "chromedriver", 177 | CONSTANTS.IE_DRIVER : "IEDriverServer", 178 | CONSTANTS.FIREFOX_DRIVER : "geckodriver" } 179 | 180 | def __init__(self, url=None, to_dir=None, overwrite_existing=True, download_filename=None, 181 | unzip_filename=None, asynch=False, unzip=True, add_to_ospath=False): 182 | self.to_dir = to_dir or self.default_download_directory() 183 | super(WebdriverDownloader, self).__init__(from_url=url, to_dir=self.to_dir, download_filename=download_filename, 184 | unzip_filename=unzip_filename, overwrite_existing=overwrite_existing, 185 | asynch=asynch, unzip=unzip, add_to_ospath=add_to_ospath) 186 | 187 | @staticmethod 188 | def default_download_directory(): 189 | dir_name = CONSTANTS.DOWNLOAD_DIR_NAME 190 | start_dir = CONSTANTS.DIR_PATH or get_user_home_dir() 191 | #home_dir = get_user_home_dir() 192 | default_dir = os.path.join(start_dir, CONSTANTS.DIR_NAME, dir_name) 193 | make_dir(default_dir) 194 | return default_dir 195 | 196 | @staticmethod 197 | def latest_chrome_version(use_default=True): 198 | #TODO: this needs to change 199 | #get from chromium website 200 | LATEST_VERSION_PATTERN = r'Latest Release:.*ChromeDriver ([\d+\.+]+)' 201 | plain_text = "" 202 | with HTTPSession() as session: 203 | h = session.get(CHROME_CONSTANTS.HOME_URL) 204 | r = HTML(h.text) 205 | r.render() 206 | plain_text = str(r.text).encode('ascii', errors='ignore').decode() 207 | v = find_patterns_in_str(LATEST_VERSION_PATTERN, plain_text, first=True) 208 | if (not v) and use_default: 209 | v = CHROME_CONSTANTS.DEFAULT_VERSION 210 | if not v: 211 | message = """ 212 | Unable to pull latest available Chromedriver version. Check, 213 | 1. Your internet connection 214 | 2. If internet is fine, contact implementor. Perhaps requires logic change 215 | """ 216 | raise OperationFailedException(message) 217 | return str(v).strip() 218 | 219 | @staticmethod 220 | def ie_download_url(version, filename): 221 | home_url = IE_CONSTANTS.DOWNLOAD_URL.format(version, filename) 222 | return home_url 223 | 224 | @classmethod 225 | def download_chrome(cls, to_dir=None, version=None, download_filename=None, overwrite_existing=False, 226 | asynch=False, unzip=True, add_to_ospath=True): 227 | unzip_filename = WebdriverDownloader.WEBDRIVERNAMES[CONSTANTS.CHROME_DRIVER] 228 | #TODO: de-duplication 229 | start_dir = to_dir or WebdriverDownloader.default_download_directory() 230 | f_dir = os.path.join(start_dir, unzip_filename) 231 | if (not overwrite_existing) and file_exists(f_dir): 232 | if add_to_ospath: 233 | add_to_path(start_dir) 234 | return 235 | download_url = CHROME_CONSTANTS.DOWNLOAD_URL 236 | if download_url: 237 | url = download_url 238 | else: 239 | #determine download_url 240 | if version: 241 | version = str(version) 242 | version = version or CHROME_CONSTANTS.VERSION or WebdriverDownloader.latest_chrome_version() 243 | filename = CHROME_CONSTANTS.FILENAME_TEMPLATE.format(WebdriverDownloader._OSMAP_CHROME[os_name()]) 244 | if not download_filename: 245 | download_filename = filename 246 | url = CHROME_CONSTANTS.DOWNLOAD_URL_TEMPLATE.format(version, filename) 247 | wd = WebdriverDownloader(url=url, to_dir=to_dir, download_filename=download_filename, 248 | unzip_filename=unzip_filename, overwrite_existing=overwrite_existing, 249 | asynch=asynch, unzip=unzip, add_to_ospath=add_to_ospath) 250 | wd.download() 251 | 252 | #TODO: automatically determine version 253 | @classmethod 254 | def download_ie(cls, to_dir=None, version=None, download_filename=None, overwrite_existing=False, 255 | asynch=False, unzip=True, add_to_ospath=True): 256 | unzip_filename = WebdriverDownloader.WEBDRIVERNAMES[CONSTANTS.IE_DRIVER] 257 | #TODO: de-duplication 258 | start_dir = to_dir or WebdriverDownloader.default_download_directory() 259 | f_dir = os.path.join(start_dir, unzip_filename) 260 | if (not overwrite_existing) and file_exists(f_dir): 261 | if add_to_ospath: 262 | add_to_path(start_dir) 263 | return 264 | download_url = IE_CONSTANTS.DOWNLOAD_URL 265 | if download_url: 266 | url = download_url 267 | else: 268 | #determine download_url 269 | version = version or IE_CONSTANTS.VERSION 270 | version = str(version) 271 | filename = IE_CONSTANTS.FILENAME_TEMPLATE.format(version) 272 | if not download_filename: 273 | download_filename = filename 274 | url = IE_CONSTANTS.DOWNLOAD_URL_TEMPLATE.format(version, filename) 275 | wd = WebdriverDownloader(url=url, to_dir=to_dir, download_filename=download_filename, 276 | unzip_filename=unzip_filename, overwrite_existing=overwrite_existing, 277 | asynch=asynch, unzip=unzip, add_to_ospath=add_to_ospath) 278 | wd.download() 279 | 280 | #TODO: logic to get latest version from Website 281 | @classmethod 282 | def download_firefox(cls, to_dir=None, version=None, download_filename=None, overwrite_existing=False, 283 | asynch=False, unzip=True, add_to_ospath=True): 284 | unzip_filename = WebdriverDownloader.WEBDRIVERNAMES[CONSTANTS.FIREFOX_DRIVER] 285 | #TODO: de-duplication 286 | start_dir = to_dir or WebdriverDownloader.default_download_directory() 287 | f_dir = os.path.join(start_dir, unzip_filename) 288 | if (not overwrite_existing) and file_exists(f_dir): 289 | if add_to_ospath: 290 | add_to_path(start_dir) 291 | return 292 | download_url = FIREFOX_CONSTANTS.DOWNLOAD_URL 293 | if download_url: 294 | url = download_url 295 | else: 296 | #determine download_url 297 | version = version or FIREFOX_CONSTANTS.VERSION or FIREFOX_CONSTANTS.DEFAULT_VERSION 298 | version = str(version) 299 | ospart = WebdriverDownloader._OSMAP_FIREFOX[os_name()] 300 | extpart = WebdriverDownloader._ZIPEXTMAP_FIREFOX[os_name()] 301 | filename = FIREFOX_CONSTANTS.FILENAME_TEMPLATE.format(version, ospart, extpart) 302 | if not download_filename: 303 | download_filename = filename 304 | url = FIREFOX_CONSTANTS.DOWNLOAD_URL_TEMPLATE.format(version, filename) 305 | wd = WebdriverDownloader(url=url, to_dir=to_dir, download_filename=download_filename, 306 | unzip_filename=unzip_filename, overwrite_existing=overwrite_existing, 307 | asynch=asynch, unzip=unzip, add_to_ospath=add_to_ospath) 308 | wd.download() 309 | 310 | webdriver_downloader = WebdriverDownloader 311 | -------------------------------------------------------------------------------- /pybrowser/driver_handler.py: -------------------------------------------------------------------------------- 1 | import os 2 | from selenium import webdriver 3 | from selenium.webdriver.chrome.options import Options as ChromeOptions 4 | from selenium.webdriver.ie.options import Options as IEOptions 5 | from selenium.webdriver.firefox.options import Options as FirefoxOptions 6 | from selenium.webdriver.common.proxy import Proxy as WebdriverProxy 7 | from selenium.webdriver.common.desired_capabilities import DesiredCapabilities 8 | from .log_adapter import get_logger, log_path 9 | from .exceptions import InvalidArgumentError 10 | from .common_utils import add_to_path, path_exists, set_winreg_fromlocalmachine, os_bits, os_name, is_valid_url 11 | from .constants import CONSTANTS 12 | 13 | #Various browsers 14 | IE = "ie" 15 | CHROME = "chrome" 16 | FIREFOX = "firefox" 17 | EDGE = "edge" 18 | SAFARI = "safari" 19 | OPERA = "opera" 20 | 21 | #TODO: need better abstraction (no duplication) 22 | class BaseDriverHandler(object): 23 | 24 | @staticmethod 25 | def create_driver(browser_name, driver_path, api_obj): 26 | if browser_name == IE: 27 | driver = IEHandler.create_driver(driver_path, api_obj) 28 | elif browser_name == CHROME: 29 | driver = ChromeHandler.create_driver(driver_path, api_obj) 30 | elif browser_name == FIREFOX: 31 | driver = FirefoxHandler.create_driver(driver_path, api_obj) 32 | elif browser_name == EDGE: 33 | driver = EdgeHandler.create_driver(driver_path, api_obj) 34 | elif browser_name == SAFARI: 35 | driver = SafariHandler.create_driver(driver_path, api_obj) 36 | else: 37 | get_logger().error(f"Invalid browser_name: {browser_name}") 38 | raise InvalidArgumentError(f"Invalid browser_name: {browser_name}") 39 | return driver 40 | 41 | @staticmethod 42 | def create_remote_driver(hub_url, desired_caps): 43 | driver = webdriver.Remote(command_executor=hub_url, desired_capabilities=desired_caps) 44 | return driver 45 | 46 | class ChromeHandler(BaseDriverHandler): 47 | 48 | @staticmethod 49 | def create_driver(driver_path, api_obj): 50 | options = ChromeHandler._create_chrome_options(api_obj) 51 | if hasattr(api_obj, "remote_url") and is_valid_url(api_obj.remote_url): 52 | caps = DesiredCapabilities.CHROME.copy() 53 | if options: 54 | caps.update(options.to_capabilities()) 55 | driver = ChromeHandler.create_remote_driver(api_obj.remote_url, caps) 56 | return driver 57 | if driver_path and path_exists(driver_path): 58 | add_to_path(driver_path) 59 | else: 60 | from .downloader import download_driver 61 | download_driver(CONSTANTS.CHROME_DRIVER, overwrite_existing=False) 62 | driver = webdriver.Chrome(options=options) 63 | return driver 64 | 65 | @staticmethod 66 | def _create_chrome_options(api_obj): 67 | options = ChromeOptions() 68 | args = set() 69 | if api_obj.headless: 70 | args.add("--headless") 71 | if api_obj.incognito: 72 | args.add("--incognito") 73 | if api_obj.browser_options_args: 74 | args.update(api_obj.browser_options_args) 75 | for arg in args: 76 | if arg: 77 | options.add_argument(arg) 78 | if api_obj.http_proxy: 79 | options.add_argument('--proxy-server=%s' %api_obj.http_proxy) 80 | if api_obj.browser_options_dict: 81 | for k, v in api_obj.browser_options_dict.items(): 82 | options.set_capability(k, v) 83 | return options 84 | 85 | class IEHandler(BaseDriverHandler): 86 | 87 | @staticmethod 88 | def create_driver(driver_path, api_obj): 89 | options = IEHandler._create_IE_options(api_obj) 90 | if hasattr(api_obj, "remote_url") and is_valid_url(api_obj.remote_url): 91 | caps = options.to_capabilities() 92 | driver = IEHandler.create_remote_driver(api_obj.remote_url, caps) 93 | return driver 94 | if driver_path and path_exists(driver_path): 95 | add_to_path(driver_path) 96 | else: 97 | from .downloader import download_driver 98 | download_driver(CONSTANTS.IE_DRIVER, overwrite_existing=False) 99 | IEHandler._handle_IE_registry_entry() 100 | driver = webdriver.Ie(options=options) 101 | return driver 102 | 103 | #takes care of adding necessary registry entries needed in windows (needs admin privileges) 104 | @staticmethod 105 | def _handle_IE_registry_entry(): 106 | if "window" not in os_name().lower(): 107 | return 108 | try: 109 | path32 = r"SOFTWARE\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_BFCACHE" 110 | path64 = r"SOFTWARE\Wow6432Node\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_BFCACHE" 111 | path = path32 if os_bits() == 32 else path64 112 | set_winreg_fromlocalmachine(path, "iexplore.exe", 0, overwrite=True) 113 | except Exception as e: 114 | get_logger().warning(str(e)) 115 | 116 | @staticmethod 117 | def _create_IE_options(api_obj): 118 | options = IEOptions() 119 | args = set() 120 | #not recommended, ensure correct security settings in IE 121 | args.add(IEOptions.IGNORE_PROTECTED_MODE_SETTINGS) 122 | args.add(IEOptions.ENSURE_CLEAN_SESSION) 123 | args.add(IEOptions.IGNORE_ZOOM_LEVEL) 124 | args.add(IEOptions.REQUIRE_WINDOW_FOCUS) 125 | args.add(IEOptions.PERSISTENT_HOVER) 126 | if api_obj.browser_options_args: 127 | args.update(api_obj.browser_options_args) 128 | for arg in args: 129 | if arg: 130 | options.add_argument(arg) 131 | if api_obj.http_proxy: 132 | proxy = { 'proxyType': "manual", 'httpProxy': str(api_obj.http_proxy)} 133 | options.set_capability("proxy", proxy) 134 | options.set_capability(IEOptions.NATIVE_EVENTS, False) 135 | if api_obj.browser_options_dict: 136 | for k, v in api_obj.browser_options_dict.items(): 137 | options.set_capability(k, v) 138 | return options 139 | 140 | class FirefoxHandler(BaseDriverHandler): 141 | 142 | @staticmethod 143 | def create_driver(driver_path, api_obj): 144 | options = FirefoxHandler._create_firefox_options(api_obj) 145 | if hasattr(api_obj, "remote_url") and is_valid_url(api_obj.remote_url): 146 | caps = DesiredCapabilities.FIREFOX.copy() 147 | if options: 148 | caps.update(options.to_capabilities()) 149 | driver = FirefoxHandler.create_remote_driver(api_obj.remote_url, caps) 150 | return driver 151 | if driver_path and path_exists(driver_path): 152 | add_to_path(driver_path) 153 | else: 154 | from .downloader import download_driver 155 | download_driver(CONSTANTS.FIREFOX_DRIVER, overwrite_existing=False) 156 | log_p = os.path.join(log_path(), "geckodriver.log") 157 | driver = webdriver.Firefox(options=options, service_log_path=log_p) 158 | return driver 159 | 160 | @staticmethod 161 | def _create_firefox_options(api_obj): 162 | options = FirefoxOptions() 163 | args = set() 164 | if api_obj.browser_options_args: 165 | args.update(api_obj.browser_options_args) 166 | for arg in args: 167 | if arg: 168 | options.add_argument(arg) 169 | options.headless = api_obj.headless 170 | if api_obj.firefox_binary_path: 171 | options.binary_location = api_obj.firefox_binary_path 172 | if api_obj.firefox_profile_path: 173 | options.profile = api_obj.firefox_profile_path 174 | if api_obj.http_proxy: 175 | raw = { 'proxyType': "manual", 'httpProxy': str(api_obj.http_proxy)} 176 | proxy = WebdriverProxy(raw=raw) 177 | options.proxy = proxy 178 | options.set_capability("acceptInsecureCerts", False) # why isnt this the default 179 | if api_obj.browser_options_dict: 180 | for k, v in api_obj.browser_options_dict.items(): 181 | options.set_capability(k, v) 182 | return options 183 | 184 | class EdgeHandler(BaseDriverHandler): 185 | 186 | @staticmethod 187 | def create_driver(driver_path, api_obj): 188 | if hasattr(api_obj, "remote_url") and is_valid_url(api_obj.remote_url): 189 | caps = DesiredCapabilities.EDGE.copy() 190 | driver = EdgeHandler.create_remote_driver(api_obj.remote_url, caps) 191 | return driver 192 | driver = webdriver.Edge() 193 | return driver 194 | 195 | class SafariHandler(BaseDriverHandler): 196 | 197 | @staticmethod 198 | def create_driver(driver_path, api_obj): 199 | if hasattr(api_obj, "remote_url") and is_valid_url(api_obj.remote_url): 200 | caps = DesiredCapabilities.SAFARI.copy() 201 | driver = SafariHandler.create_remote_driver(api_obj.remote_url, caps) 202 | return driver 203 | driver = webdriver.Safari() 204 | return driver 205 | 206 | class OperaHandler(BaseDriverHandler): 207 | 208 | @staticmethod 209 | def create_driver(driver_path, api_obj): 210 | if hasattr(api_obj, "remote_url") and is_valid_url(api_obj.remote_url): 211 | caps = DesiredCapabilities.OPERA.copy() 212 | driver = OperaHandler.create_remote_driver(api_obj.remote_url, caps) 213 | return driver 214 | driver = webdriver.Opera() 215 | return driver -------------------------------------------------------------------------------- /pybrowser/elements/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'ranjith' 2 | __version__ = '0.2.0' 3 | -------------------------------------------------------------------------------- /pybrowser/elements/actions.py: -------------------------------------------------------------------------------- 1 | __author__ = 'ranjith' 2 | from time import sleep 3 | from selenium import webdriver 4 | from selenium.webdriver.common.keys import Keys 5 | from selenium.webdriver.remote.webelement import WebElement 6 | from selenium.webdriver.support.event_firing_webdriver import EventFiringWebElement 7 | from selenium.webdriver.support.ui import Select 8 | from selenium.common.exceptions import StaleElementReferenceException 9 | from selenium.webdriver.common.action_chains import ActionChains 10 | from .utils import find_element_for_locator, exec_func, wait_until_stale, wait_until 11 | from ..external.utils import cached_property 12 | from ..decorators import action_wrapper 13 | from ..log_adapter import get_logger 14 | 15 | #TODO: custom exceptions 16 | #TODO: have more functionalities here such as getting tagnames/classes/text/name etc 17 | 18 | @action_wrapper 19 | class Action(object): 20 | 21 | def __init__(self, driver, locator=None, element=None, wait_time=10, visible=False): 22 | if element is None: 23 | element = find_element_for_locator(driver, locator, wait_time, visible=visible, ignore_exception=True) 24 | self._driver = driver 25 | self.element = element 26 | self.locator = locator 27 | self._element_found = True if isinstance(self.element, (WebElement, EventFiringWebElement)) else False 28 | #print("_element_found ", self._element_found) 29 | #print("element ", self.element) 30 | self.visible = visible 31 | self.wait_time = wait_time 32 | self._if_found = False 33 | self._if_enabled = False 34 | self._if_displayed = False 35 | self._if_stale = False 36 | self.logger = get_logger() 37 | 38 | #TODO: needs better implementation 39 | def click(self, wait_time=None, hook=None): 40 | _ALL_CLICK_FUNCS = [self.element.click, self.element.submit] 41 | before_source = self._driver.page_source 42 | for f in _ALL_CLICK_FUNCS: 43 | if exec_func(f)[0]: 44 | break 45 | #in case none of the function calls were successful, try ENTER Key ! 46 | else: 47 | self._try_key_send() 48 | self._wait_after_click(before_source, wait_time, hook) 49 | return self._dispatch() 50 | 51 | def _try_key_send(self): 52 | try: 53 | self.element.send_keys(Keys.ENTER) 54 | except Exception as e: 55 | self.logger.error("Could not click on element !") 56 | raise Exception("Could not click on element !") 57 | 58 | def _wait_after_click(self, old_source, wait_time=None, hook=None): 59 | def _wait(driver): 60 | script = "return document.readyState" 61 | COMPLETE = "complete" 62 | status = driver.execute_script(script) 63 | if status and status.lower() == COMPLETE: 64 | new_source = driver.page_source 65 | return new_source != old_source 66 | return False 67 | if self._driver is None: 68 | return 69 | wait_time = wait_time or self.wait_time 70 | hook = hook or _wait 71 | try: 72 | wait_until(self._driver, wait_time, hook) 73 | except Exception as e: 74 | self.logger.warning("_wait_after_click might not have succedded") 75 | 76 | def submit(self, wait_time=None, hook=None): 77 | before_source = self._driver.page_source 78 | self.element.submit() 79 | self._wait_after_click(before_source, wait_time, hook) 80 | return self._dispatch() 81 | 82 | @cached_property 83 | def tag_name(self): 84 | return self.element.tag_name 85 | 86 | @cached_property 87 | def id(self): 88 | return self.element.get_attribute('id') 89 | 90 | @cached_property 91 | def name(self): 92 | return self.element.get_attribute('name') 93 | 94 | @cached_property 95 | def type(self): 96 | return self.element.get_attribute('type') 97 | 98 | @cached_property 99 | def css_classes(self): 100 | clazzes = self.element.get_attribute('class') 101 | if clazzes: 102 | return clazzes.split() 103 | 104 | @cached_property 105 | def value(self): 106 | return self.element.get_attribute('value') 107 | 108 | @cached_property 109 | def text(self): 110 | val = self.element.text 111 | if val: 112 | return val 113 | #TODO: check if this is valid 114 | return self.element.get_attribute('innerText') 115 | 116 | @cached_property 117 | def href(self): 118 | return self.element.get_attribute('href') 119 | 120 | @property 121 | def is_found(self): 122 | return self._element_found 123 | 124 | @property 125 | def if_found(self): 126 | self._if_found = True 127 | return self._dispatch() 128 | 129 | @property 130 | def is_displayed(self): 131 | return self.element.is_displayed() 132 | 133 | @property 134 | def is_visible(self): 135 | return self.is_displayed 136 | 137 | @property 138 | def if_displayed(self): 139 | self._if_displayed = True 140 | return self._dispatch() 141 | 142 | @property 143 | def if_visible(self): 144 | return self.if_displayed 145 | 146 | @property 147 | def is_enabled(self): 148 | return self.element.is_enabled() 149 | 150 | @property 151 | def if_enabled(self): 152 | self._if_enabled = True 153 | return self._dispatch() 154 | 155 | @property 156 | def is_stale(self): 157 | try: 158 | self.element.tag_name 159 | except Exception as e: 160 | if isinstance(e, StaleElementReferenceException): 161 | return True 162 | return False 163 | 164 | @property 165 | def if_stale(self): 166 | self._if_stale = True 167 | return self._dispatch() 168 | 169 | def wait_for_staleness(self, wait_time=None): 170 | wait_time = wait_time or self.wait_time 171 | try: 172 | wait_until_stale(self._driver, self.element, wait_time) 173 | except Exception as e: 174 | self.logger.error(f"Exception in wait_for_staleness - {str(e)}") 175 | return self._dispatch() 176 | 177 | def wait(self, wait_time=None): 178 | wait_time = wait_time or self.wait_time 179 | sleep(wait_time) 180 | return self._dispatch() 181 | 182 | def refresh(self): 183 | CACHED_ATTRS = ['tag_name', 'id', 'name', 'type', 'css_classes', 'value', 'text', 'href'] # i don't like this either! 184 | self.element = find_element_for_locator(self._driver, self.locator, self.wait_time, visible=self.visible, ignore_exception=True) 185 | self._element_found = True if isinstance(self.element, (WebElement, EventFiringWebElement)) else False 186 | #delete cached attributes via cached_property 187 | for attr in CACHED_ATTRS: 188 | del self.__dict__[attr] 189 | return self._dispatch() 190 | 191 | def highlight(self): 192 | if not (self._driver and self.element): 193 | return 194 | STYLE = "style" 195 | TIMES = 3 196 | DURATION1, DURATION2 = 0.5, 0.3 #seconds 197 | current_style = self.element.get_attribute(STYLE) 198 | highlight_script = "arguments[0].setAttribute(arguments[1],arguments[2])" 199 | highlight_style = "border:2px; color:#ff8547; border-style:dashed;background-color:#ffd247;" 200 | for _ in range(TIMES): 201 | self._driver.execute_script(highlight_script, self.element, STYLE, highlight_style) 202 | #TODO:perhaps a better way ! 203 | sleep(DURATION1) 204 | self._driver.execute_script(highlight_script, self.element, STYLE, current_style) 205 | sleep(DURATION2) 206 | return self._dispatch() 207 | 208 | def double_click(self): 209 | if not self.element: 210 | return 211 | ac = ActionChains(self._driver) 212 | ac.double_click(on_element=self.element).perform() 213 | 214 | def move_to_element(self): 215 | if not self.element: 216 | return 217 | ac = ActionChains(self._driver) 218 | ac.move_to_element(self.element).perform() 219 | 220 | def drag_and_drop_at(self, to_element=None, to_locator=None, wait_time=10, visible=False): 221 | if not (to_element or to_locator): 222 | return 223 | if to_element is None: 224 | to_element = find_element_for_locator(self._driver, to_locator, wait_time, visible=visible, ignore_exception=False) 225 | if to_element is None: 226 | return 227 | ac = ActionChains(self._driver) 228 | ac.drag_and_drop(self.element, to_element).perform() 229 | 230 | def _dispatch(self): 231 | if hasattr(self, "_deco_clazz"): 232 | return self._deco_clazz 233 | return self 234 | -------------------------------------------------------------------------------- /pybrowser/elements/button.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Ranjith' 2 | from .actions import Action 3 | 4 | class Button(Action): 5 | 6 | def __init__(self, driver, locator=None, element=None, wait_time=10, visible=False): 7 | super().__init__(driver, locator, element, wait_time, visible) 8 | 9 | @property 10 | def button_name(self): 11 | return self.text 12 | -------------------------------------------------------------------------------- /pybrowser/elements/checkbox.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Ranjith' 2 | from .actions import Action 3 | 4 | class Checkbox(Action): 5 | 6 | def __init__(self, driver, locator=None, element=None, wait_time=10, visible=False): 7 | super().__init__(driver, locator, element, wait_time, visible) 8 | 9 | def check(self): 10 | if not self.is_checked: 11 | self.click() 12 | return self 13 | 14 | def uncheck(self): 15 | if self.is_checked: 16 | self.click() 17 | return self 18 | 19 | @property 20 | def is_checked(self): 21 | return self.element.is_selected() 22 | -------------------------------------------------------------------------------- /pybrowser/elements/element.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Ranjith' 2 | from .actions import Action 3 | 4 | class Element(Action): 5 | 6 | def __init__(self, driver, locator=None, element=None, wait_time=10, visible=False): 7 | super().__init__(driver, locator, element, wait_time, visible) 8 | -------------------------------------------------------------------------------- /pybrowser/elements/file.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Ranjith' 2 | import os 3 | from .utils import find_elements_for_element 4 | from .actions import Action 5 | from ..common_utils import get_user_home_dir 6 | from ..exceptions import InvalidArgumentError 7 | from ..downloader import download_url 8 | 9 | class File(Action): 10 | 11 | def __init__(self, driver, locator=None, element=None, wait_time=10, visible=False): 12 | super().__init__(driver, locator, element, wait_time, visible) 13 | self._downloaded_files = None 14 | self._is_download_complete = False 15 | 16 | def upload(self, filename=""): 17 | if not os.path.isfile(filename): 18 | raise InvalidArgumentError(f"{filename} is not a file. Please provide valid filename for upload") 19 | self.element.send_keys(filename) 20 | return self 21 | 22 | #TODO: requires work - link not found, more testing 23 | def download(self, directory=None, as_filename=None, asynch=True, unzip=False, del_zipfile=False, add_to_ospath=False): 24 | #flag reset 25 | self._is_download_complete = False 26 | self._downloaded_files = None 27 | if not directory: 28 | directory = get_user_home_dir() 29 | if not os.path.isdir(directory): 30 | raise InvalidArgumentError(f"{directory} is not a directory. Please provide valid directory for download") 31 | link = self.href 32 | if link: 33 | self._download_file(link, directory, as_filename, asynch, unzip, del_zipfile, add_to_ospath) 34 | else: 35 | links = self._get_child_links() 36 | #TODO: not a good solution, think of a better way to resolve this 37 | for l in links: 38 | self._download_file(l, directory, None, asynch, unzip, del_zipfile, add_to_ospath) 39 | return self 40 | 41 | def _download_file(self, link, directory, as_filename, asynch, unzip, del_zipfile, add_to_ospath): 42 | try: 43 | download_url(url=link, to_dir=directory, download_filename=as_filename, asynch=asynch, 44 | unzip=unzip, del_zipfile=del_zipfile, add_to_ospath=add_to_ospath, callback=self._callback) 45 | except Exception: 46 | pass 47 | 48 | def _get_child_links(self): 49 | child_links_xpath = "xpath=.//a" 50 | links = find_elements_for_element(self.element, child_links_xpath) 51 | return links 52 | 53 | @property 54 | def is_download_complete(self): 55 | return self._is_download_complete 56 | 57 | @property 58 | def downloaded_files(self): 59 | return self._downloaded_files 60 | 61 | def _callback(self, files): 62 | self._is_download_complete = True 63 | if self._downloaded_files: 64 | if files: 65 | self._downloaded_files += files 66 | else: 67 | self._downloaded_files = files 68 | -------------------------------------------------------------------------------- /pybrowser/elements/form.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Ranjith' 2 | from collections import OrderedDict 3 | from .utils import find_element_for_locator 4 | from ..exceptions import InvalidArgumentError 5 | from .actions import Action 6 | from .input import Input 7 | from .file import File 8 | from .radio import Radio 9 | from .checkbox import Checkbox 10 | 11 | #TODO: property to get all form inputs 12 | class Form(Action): 13 | 14 | def __init__(self, driver, locator=None, element=None, wait_time=10, visible=False): 15 | super().__init__(driver, locator, element, wait_time, visible) 16 | 17 | def _check_data_and_raise(self, form_data): 18 | if not isinstance(form_data, list): 19 | raise InvalidArgumentError("""Form data is mandatory and needs to be a list of tuples with 20 | 2 elements (locator and value)""") 21 | for d in form_data: 22 | if not isinstance(d, tuple) or len(d) != 2: 23 | raise InvalidArgumentError("""Form data is mandatory and needs to be a list of tuples with 24 | 2 elements (locator and value)""") 25 | 26 | #TODO: time, week, month, image, file support 27 | def fill_and_submit_form(self, form_data=None): 28 | #if there is no form data, just submit form 29 | if not form_data: 30 | self.submit() 31 | return self 32 | _TEXT_TYPES = ["search", "text", "password", "number", "email", "tel"] 33 | self._check_data_and_raise(form_data) 34 | self.form_data = OrderedDict(form_data) 35 | for field, value in self.form_data.items(): 36 | child_element = find_element_for_locator(self._driver, field) 37 | tag_name, input_type = child_element.tag_name, child_element.get_attribute('type') or "text" #default 38 | if input_type.lower() in _TEXT_TYPES: 39 | self._handle_text(child_element, value) 40 | elif input_type.lower() == "checkbox": 41 | self._handle_checkbox(child_element, value) 42 | elif input_type.lower() == "radio": 43 | self._handle_radio(child_element, value) 44 | elif input_type.lower() == "date": 45 | self._handle_date(child_element, value) 46 | elif input_type.lower() == "file": 47 | self._handle_file(child_element, value) 48 | self.submit() 49 | return self 50 | 51 | def _handle_text(self, child_element, value=""): 52 | input_ = Input(self._driver, element=child_element) 53 | input_.enter(value) 54 | 55 | def _handle_checkbox(self, child_element, value=False): 56 | checkbox = Checkbox(self._driver, element=child_element) 57 | if value: 58 | checkbox.check 59 | else: 60 | checkbox.uncheck 61 | 62 | def _handle_radio(self, child_element, value=False): 63 | radio = Radio(self._driver, element=child_element) 64 | if value: 65 | radio.select 66 | else: 67 | chkbox.unselect 68 | 69 | def _handle_file(self, child_element, value): 70 | f = File(self._driver, element=child_element) 71 | f.upload(value) 72 | -------------------------------------------------------------------------------- /pybrowser/elements/input.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Ranjith' 2 | from .actions import Action 3 | 4 | class Input(Action): 5 | 6 | def __init__(self, driver, locator=None, element=None, wait_time=10, visible=False): 7 | super().__init__(driver, locator, element, wait_time, visible) 8 | 9 | def enter(self, some_text = ""): 10 | self.element.clear() 11 | self.element.send_keys(some_text) 12 | return self 13 | 14 | def clear(self): 15 | self.element.clear() 16 | return self 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /pybrowser/elements/link.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Ranjith' 2 | from .actions import Action 3 | 4 | class Link(Action): 5 | 6 | def __init__(self, driver, locator=None, element=None, wait_time=10, visible=False): 7 | super().__init__(driver, locator, element, wait_time, visible) 8 | 9 | @property 10 | def url(self): 11 | return self.href 12 | 13 | 14 | -------------------------------------------------------------------------------- /pybrowser/elements/radio.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Ranjith' 2 | from .actions import Action 3 | 4 | class Radio(Action): 5 | 6 | def __init__(self, driver, locator=None, element=None, wait_time=10, visible=False): 7 | super().__init__(driver, locator, element, wait_time, visible) 8 | 9 | def select(self): 10 | if not self.is_selected: 11 | self.click() 12 | return self 13 | 14 | #TODO: dont think this will be valid, check back 15 | def unselect(self): 16 | if self.is_selected: 17 | self.click() 18 | return self 19 | 20 | @property 21 | def is_selected(self): 22 | return self.element.is_selected() 23 | 24 | 25 | -------------------------------------------------------------------------------- /pybrowser/elements/select.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Ranjith' 2 | from selenium.webdriver.support.ui import Select as Select_ 3 | from .actions import Action 4 | from ..exceptions import InvalidArgumentError 5 | from ..log_adapter import get_logger 6 | 7 | class Select(Action): 8 | 9 | def __init__(self, driver, locator=None, element=None, wait_time=10, visible=False): 10 | super().__init__(driver, locator, element, wait_time, visible) 11 | self.dropdown = Select_(self.element) 12 | 13 | def select_all(self): 14 | all_elements = self.dropdown.options 15 | if not all_elements: 16 | return 17 | for ele in all_elements: 18 | if not ele.is_selected(): 19 | ele.click() 20 | 21 | def select_by_visible_texts(self, values=""): 22 | values = self._convert(values) 23 | for value in values: 24 | self.dropdown.select_by_visible_text(value) 25 | return self 26 | 27 | def select_by_indices(self, indices): 28 | indices = self._convert(indices) 29 | for index in indices: 30 | self.dropdown.select_by_index(index) 31 | return self 32 | 33 | def select_by_values(self, values=""): 34 | values = self._convert(values) 35 | for value in values: 36 | self.dropdown.select_by_value(value) 37 | return self 38 | 39 | def deselect_all(self): 40 | self.dropdown.deselect_all() 41 | 42 | def deselect_by_visible_texts(self, values=""): 43 | values = self._convert(values) 44 | for value in values: 45 | self.dropdown.deselect_by_visible_text(value) 46 | return self 47 | 48 | def deselect_by_indices(self, indices): 49 | indices = self._convert(indices) 50 | for index in indices: 51 | self.dropdown.deselect_by_index(index) 52 | return self 53 | 54 | def deselect_by_values(self, values=""): 55 | values = self._convert(values) 56 | for value in values: 57 | self.dropdown.deselect_by_value(value) 58 | return self 59 | 60 | @property 61 | def options_text(self): 62 | #get can be text, value, element 63 | all_elements = self.dropdown.options 64 | results = self._get_options(all_elements, "text") 65 | return results 66 | 67 | @property 68 | def options_value(self): 69 | #get can be text, value, element 70 | all_elements = self.dropdown.options 71 | results = self._get_options(all_elements, "value") 72 | return results 73 | 74 | @property 75 | def options_element(self): 76 | #get can be text, value, element 77 | all_elements = self.dropdown.options 78 | results = self._get_options(all_elements, "element") 79 | return results 80 | 81 | @property 82 | def all_selected_options_text(self): 83 | #get can be text, value, element 84 | all_elements = self.dropdown.all_selected_options 85 | results = self._get_options(all_elements, "text") 86 | return results 87 | 88 | @property 89 | def all_selected_options_value(self): 90 | #get can be text, value, element 91 | all_elements = self.dropdown.all_selected_options 92 | results = self._get_options(all_elements, "value") 93 | return results 94 | 95 | @property 96 | def all_selected_options_element(self): 97 | #get can be text, value, element 98 | all_elements = self.dropdown.all_selected_options 99 | results = self._get_options(all_elements, "element") 100 | return results 101 | 102 | def _get_options(self, all_elements, get): 103 | results = [] 104 | if not all_elements: 105 | return results 106 | if "text" in get.strip().lower(): 107 | for ele in all_elements: 108 | results.append(ele.text) 109 | elif "value" in get.strip().lower(): 110 | for ele in all_elements: 111 | results.append(ele.get_attribute('value')) 112 | elif "element" in get.strip().lower(): 113 | results = all_elements 114 | else: 115 | get_logger().error("Invalid input. Valid values for get - text, value, element") 116 | raise InvalidArgumentError("Invalid input. Valid values for get - text, value, element") 117 | return results 118 | 119 | def _convert(self, values): 120 | if not values: 121 | values = [] 122 | if not isinstance(values, list): 123 | values = [values] 124 | return values 125 | -------------------------------------------------------------------------------- /pybrowser/elements/utils.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Ranjith' 2 | from selenium.webdriver.common.by import By 3 | from selenium.webdriver.support.ui import WebDriverWait 4 | from selenium.webdriver.support import expected_conditions as EC 5 | from selenium.common.exceptions import * 6 | from ..log_adapter import get_logger 7 | 8 | IGNORE_EXCEPTIONS = [StaleElementReferenceException, ElementNotVisibleException, InvalidElementStateException] 9 | DELIMITER = ":=" 10 | 11 | def _get_By(locator_type): 12 | if locator_type.upper() == "ID": 13 | return By.ID 14 | elif locator_type.upper() == "NAME": 15 | return By.NAME 16 | elif locator_type.upper() == "XPATH": 17 | return By.XPATH 18 | elif locator_type.upper() == "LINK_TEXT": 19 | return By.LINK_TEXT 20 | elif locator_type.upper() == "PARTIAL_LINK_TEXT": 21 | return By.PARTIAL_LINK_TEXT 22 | elif locator_type.upper() == "TAG_NAME": 23 | return By.TAG_NAME 24 | elif locator_type.upper() == "CLASS_NAME": 25 | return By.CLASS_NAME 26 | elif locator_type.upper() == "CSS_SELECTOR": 27 | return By.CSS_SELECTOR 28 | 29 | def get_By_value(locator): 30 | if locator is None: 31 | get_logger().error("locator is mandatory and is not provided") 32 | raise InvalidSelectorException("locator is mandatory and is not provided") 33 | locator_list = [l.strip() for l in locator.split(DELIMITER, 1)] 34 | 35 | if len(locator_list) == 1: 36 | return [By.ID, By.NAME], locator_list[0] 37 | else: 38 | return _get_By(locator_list[0]), locator_list[1] 39 | 40 | def get_element(driver, by_types, by_value, wait_time, visible=False, ignore_exception=False): 41 | if not isinstance(by_types, list): 42 | by_types = [by_types] 43 | for b_type in by_types: 44 | try: 45 | ele = _find_element(driver, wait_time, b_type, by_value, visible) 46 | return ele 47 | except Exception: 48 | pass 49 | if not ignore_exception: 50 | raise NoSuchElementException("Element not found") 51 | return None #don't know why 52 | 53 | def _find_element(driver, wait_time, by_type, by_value, visible=False): 54 | if visible: 55 | func = EC.visibility_of_element_located((by_type, by_value)) 56 | else: 57 | func = EC.presence_of_element_located((by_type, by_value)) 58 | try: 59 | return wait_until(driver, wait_time, func) 60 | except Exception: 61 | raise NoSuchElementException("Element not found") 62 | 63 | def wait_until(driver, wait_time, func): 64 | return WebDriverWait(driver, wait_time, 0.5, IGNORE_EXCEPTIONS).until(func) 65 | 66 | def wait_until_stale(driver, element, wait_time): 67 | func = EC.staleness_of(element) 68 | return WebDriverWait(driver, wait_time, 0.5).until(func) 69 | 70 | def find_element_for_locator(driver, locator, wait_time=10, visible=False, ignore_exception=False): 71 | by_type, by_value = get_By_value(locator) 72 | element = get_element(driver, by_type, by_value, wait_time, visible, ignore_exception) 73 | return element 74 | 75 | #TODO: change to EC sometime in future 76 | def find_elements_for_element(element, locator, wait_time=10): 77 | by_type, by_value = get_By_value(locator) 78 | child_elements = element.find_elements(by=by_type, value=by_value) 79 | return child_elements 80 | 81 | #TODO: need better design here 82 | def exec_func(f, *args, **kwargs): 83 | try: 84 | r = f(*args, **kwargs) 85 | return (True, r) 86 | except Exception: 87 | pass 88 | return (False,) -------------------------------------------------------------------------------- /pybrowser/exceptions.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Master of all exceptions, errors 3 | ''' 4 | 5 | class InvalidArgumentError(ValueError): 6 | pass 7 | 8 | class OperationFailedException(Exception): 9 | pass 10 | 11 | class NotImplementedException(Exception): 12 | pass -------------------------------------------------------------------------------- /pybrowser/external/htmlrenderer.py: -------------------------------------------------------------------------------- 1 | import os 2 | import asyncio 3 | from pyppeteer import launch 4 | from ..log_adapter import get_logger 5 | from ..common_utils import get_user_home_dir, add_to_osenv 6 | from ..constants import CONSTANTS 7 | 8 | def render_html(url=None, html=None, get_text=False, script=None, reload_=False, wait_time=5, timeout=10): 9 | result, content = None, None 10 | _set_env() 11 | try: 12 | result, content = asyncio.get_event_loop().run_until_complete(_render(url=url, html=html, get_text=get_text, script=script, reload_=reload_, wait_time=wait_time, timeout=timeout)) 13 | except IOError as e: 14 | get_logger().error(f"Error in render_html method - {str(e)}") 15 | #print({str(e)}) 16 | return result, content 17 | 18 | def _set_env(): 19 | d = CONSTANTS.PYPPETEER_HOME 20 | if not d: 21 | start_dir = CONSTANTS.DIR_PATH or get_user_home_dir() 22 | d = os.path.join(start_dir, CONSTANTS.DIR_NAME, CONSTANTS.PYPPETEER_DIR) 23 | add_to_osenv('PYPPETEER_HOME', d) 24 | 25 | async def _render(url=None, html=None, get_text=False, script=None, reload_=False, wait_time=5, timeout=10): 26 | page = None 27 | result, content = None, None 28 | try: 29 | browser = await launch(ignoreHTTPSErrors=True, headless=True) 30 | page = await browser.newPage() 31 | await asyncio.sleep(wait_time) 32 | if url: 33 | await page.goto(url, options={'timeout': int(timeout * 1000), 'waitUntil': ['domcontentloaded', 'load']}) 34 | elif html: 35 | await page.goto(f'data:text/html,{html}', options={'timeout': int(timeout * 1000), 'waitUntil': ['domcontentloaded', 'load']}) 36 | #elif url: 37 | # await page.goto(url, options={'timeout': int(timeout * 1000), 'waitUntil': ['domcontentloaded', 'load']}) 38 | #await page.screenshot({'path': 'example.png'}) 39 | if script: 40 | result = await page.evaluate(script) 41 | if get_text: 42 | content = await page.evaluate('document.body.textContent') 43 | else: 44 | #content = await page.evaluate('document.body.outerHTML') 45 | content = await page.content() 46 | if page: 47 | await page.close() 48 | page = None 49 | except Exception: 50 | if page: 51 | try: await page.close() 52 | except: pass 53 | page = None 54 | return result, content 55 | 56 | #TODO: make it talk 57 | async def _talk(html, wait_time=5): 58 | page = None 59 | result, content = None, None 60 | try: 61 | browser = await launch(ignoreHTTPSErrors=True, headless=False, args=['--window-size=0,0', '--window-position=25,25']) 62 | page = await browser.newPage() 63 | await asyncio.sleep(wait_time) 64 | #await page.evaluateOnNewDocument('window.TEXT2SPEECH', text) 65 | await page.goto(f'data:text/html,{html}') 66 | await page.click('.button') 67 | if page: 68 | await page.close() 69 | page = None 70 | except Exception: 71 | if page: 72 | await page.close() 73 | page = None 74 | 75 | if __name__ == "__main__": 76 | result, content = render_html("http://selenium-release.storage.googleapis.com/index.html?path=3.14/") 77 | print(content) 78 | -------------------------------------------------------------------------------- /pybrowser/external/utils.py: -------------------------------------------------------------------------------- 1 | 2 | #taken from wekzeug project 3 | class cached_property(property): 4 | """A decorator that converts a function into a lazy property. The 5 | function wrapped is called the first time to retrieve the result 6 | and then that calculated result is used the next time you access 7 | the value:: 8 | class Foo(object): 9 | @cached_property 10 | def foo(self): 11 | # calculate something important here 12 | return 42 13 | The class has to have a `__dict__` in order for this property to 14 | work. 15 | """ 16 | 17 | # implementation detail: A subclass of python's builtin property 18 | # decorator, we override __get__ to check for a cached value. If one 19 | # chooses to invoke __get__ by hand the property will still work as 20 | # expected because the lookup logic is replicated in __get__ for 21 | # manual invocation. 22 | 23 | def __init__(self, func, name=None, doc=None): 24 | self.__name__ = name or func.__name__ 25 | self.__module__ = func.__module__ 26 | self.__doc__ = doc or func.__doc__ 27 | self.func = func 28 | 29 | def __set__(self, obj, value): 30 | obj.__dict__[self.__name__] = value 31 | 32 | def __get__(self, obj, clazz=None): 33 | _missing = "_missing_" 34 | if obj is None: 35 | return self 36 | value = obj.__dict__.get(self.__name__, _missing) 37 | #print("cached_prop value before func call", value) 38 | if value is _missing: 39 | value = self.func(obj) 40 | obj.__dict__[self.__name__] = value 41 | return value -------------------------------------------------------------------------------- /pybrowser/htmlm.py: -------------------------------------------------------------------------------- 1 | import os 2 | from collections import namedtuple 3 | import mimetypes 4 | import re 5 | from lxml import html 6 | from lxml.html.clean import Cleaner 7 | from .common_utils import (is_valid_url, get_unique_filename_from_url, get_user_home_dir, 8 | make_dir, dir_filename) 9 | from .external.parse import search as parse_search 10 | from .external.parse import findall 11 | from .external.htmlrenderer import render_html 12 | from .constants import CONSTANTS 13 | 14 | LINESEP = os.linesep 15 | 16 | class HTML(object): 17 | 18 | def __init__(self, content=None, url=None, encoding="utf-8", print_style=False, print_js=True, 19 | remove_tags=None): 20 | self._set_content(content, encoding) 21 | self.encoding = encoding 22 | self.url = url 23 | self.lxmltree = html.fromstring(self.content) 24 | self.cleaner = self._get_cleaner(print_style, print_js, remove_tags) 25 | 26 | def __str__(self): 27 | return html.tostring(self.cleaner.clean_html(self.lxmltree), pretty_print=True, encoding='unicode') 28 | 29 | def _set_content(self, content, encoding): 30 | self.content = None 31 | if isinstance(content, bytes): 32 | self.content = content.decode(encoding=encoding, errors="ignore") 33 | else: 34 | self.content = str(content).encode(encoding=encoding, errors='ignore').decode() 35 | 36 | def _get_cleaner(self, print_style, print_js, remove_tags): 37 | c = Cleaner() 38 | c.scripts = not print_js 39 | c.javascript = not print_js 40 | c.style = not print_style 41 | c.remove_tags = remove_tags 42 | c.page_structure = False 43 | return c 44 | 45 | @property 46 | def text(self): 47 | if self.lxmltree is None: 48 | return 49 | s = self.lxmltree.text_content() 50 | #TODO: might need a performance fix in future 51 | if isinstance(s, str): 52 | s = LINESEP.join([t.strip() for t in s.split(LINESEP) if t and t.strip()]) 53 | return s 54 | 55 | @property 56 | def elements(self): 57 | return Elements(self.lxmltree, self.encoding) 58 | 59 | def search(self, template, use_text=False): 60 | """Search the :class:`Element ` for the given Parse template. 61 | 62 | :param template: The Parse template to use. 63 | """ 64 | c = self.text if use_text else self.content 65 | return parse_search(template, c) 66 | 67 | def search_all(self, template, use_text=False): 68 | """Search the :class:`Element ` (multiple times) for the given parse 69 | template. 70 | 71 | :param template: The Parse template to use. 72 | """ 73 | c = self.text if use_text else self.content 74 | return [r for r in findall(template, c)] 75 | 76 | def save(self, filename=None): 77 | final_path = self._get_filename_from_url(filename) 78 | with open(final_path, "w", encoding=self.encoding) as f: 79 | f.write(self.content) 80 | return final_path 81 | 82 | def _get_filename_from_url(self, filename=None): 83 | if filename is None: 84 | f = get_unique_filename_from_url(self.url, ext="html") 85 | p = CONSTANTS.DIR_PATH or get_user_home_dir() 86 | d = os.path.join(p, CONSTANTS.DIR_NAME, CONSTANTS.HTML_DIR) 87 | else: 88 | d, f = dir_filename(filename, default_ext="html") 89 | if not d: 90 | p = CONSTANTS.DIR_PATH or get_user_home_dir() 91 | d = os.path.join(p, CONSTANTS.DIR_NAME, CONSTANTS.HTML_DIR) 92 | if not f: 93 | f = get_unique_filename_from_url(self.driver.current_url, ext="html") 94 | make_dir(d) 95 | #final path 96 | return os.path.join(d, f) 97 | 98 | def render(self, script=None, text=False): 99 | result, content = render_html(url=self.url, html=self.content, get_text=text, script=script, reload_=False) 100 | if content: 101 | self.content = content 102 | #script execution results 103 | return result 104 | 105 | class Elements(object): 106 | 107 | def __init__(self, lxmltree, encoding): 108 | self.lxmltree = lxmltree 109 | self.encoding = encoding 110 | 111 | def find_by_id(self, id_): 112 | if not (id_ and self.lxmltree): 113 | return 114 | return self.lxmltree.get_element_by_id(id_) 115 | 116 | def find_by_class(self, clazz): 117 | if not (clazz and self.lxmltree): 118 | return 119 | return self.lxmltree.find_class(clazz) 120 | 121 | def rel_links(self, rel): 122 | #Find any links like ``...``; returns a list of elements. 123 | if not (rel and self.lxmltree): 124 | return 125 | return self.lxmltree.find_rel_links(rel) 126 | 127 | def find_by_css_selector(self, selector): 128 | if not (selector and self.lxmltree): 129 | return 130 | return self.lxmltree.cssselect(selector) 131 | 132 | def find_by_xpath(self, selector): 133 | if not (selector and self.lxmltree): 134 | return 135 | return self.lxmltree.xpath(selector) 136 | 137 | def links(self, containing=None, url_only=True, images=False, name_length_limit=60): 138 | if self.lxmltree is None: 139 | return 140 | links_ = [] 141 | named_link = namedtuple('Link', ['name', 'url']) 142 | for element, attribute, link, pos in self.lxmltree.iterlinks(): 143 | if containing and containing not in element.text_content(): 144 | continue 145 | lk = None 146 | link_key = self._get_element_text_content(element.text_content(), limit=name_length_limit) or attribute 147 | if url_only: 148 | if is_valid_url(link): 149 | lk = named_link(link_key, link) 150 | else: 151 | lk = named_link(link_key, link) 152 | if lk and (not images): 153 | mt = mimetypes.guess_type(link) 154 | typ_ = mt[0] if mt else None 155 | typ_ = typ_ or "" 156 | lk = None if "image" in typ_ else lk 157 | if lk: 158 | links_.append(lk) 159 | return links_ 160 | 161 | #TODO: might need some cleaning up in text department 162 | def _get_element_text_content(self, text, limit=100): 163 | SPACE = " " 164 | BLANK = "" 165 | exclude_except_chars = r"[^A-Za-z0-9\s\._-]+" 166 | extra_spaces = r"\s+" 167 | if isinstance(text, bytes): 168 | text = text.decode(encoding=self.encoding, errors="ignore") 169 | else: 170 | text = str(text).encode(encoding=self.encoding, errors='ignore').decode() 171 | text = text.strip() if text else BLANK 172 | rc = re.compile(exclude_except_chars) 173 | text = rc.sub(BLANK, text) 174 | rc = re.compile(extra_spaces) 175 | text = rc.sub(SPACE, text) 176 | return text[:limit] 177 | -------------------------------------------------------------------------------- /pybrowser/listeners.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver.support.events import AbstractEventListener 2 | 3 | class ExceptionListener(AbstractEventListener): 4 | 5 | def __init__(self, browser_object, filename): 6 | #TODO: more of a hack to get hold of browser object, there's got to be better way! 7 | self.obj = browser_object 8 | self.filename = filename 9 | 10 | def on_exception(self, exception, driver): 11 | self.obj.driver = driver 12 | self.obj.take_screenshot(filename=self.filename) 13 | self.obj.logger.error(f"Exception occurred. Details - {str(exception)}") 14 | if not self.obj.silent_fail: 15 | raise exception 16 | -------------------------------------------------------------------------------- /pybrowser/log_adapter.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from logging.handlers import RotatingFileHandler 3 | import os 4 | import sys 5 | from functools import lru_cache 6 | from .constants import CONSTANTS 7 | from .common_utils import get_user_home_dir, make_dir 8 | 9 | _CURRENT_LOGGER = None 10 | _DEFAULT_LOGGER = CONSTANTS.DEFAULT_LOGGER 11 | 12 | def _default_handler(level=logging.DEBUG): 13 | global _DEFAULT_LOGGER 14 | MAX_SIZE_BYTES = 1000000 15 | BACKUP_COUNT = 2 16 | filename = f"{_DEFAULT_LOGGER}.log" 17 | final_path = log_path() 18 | p = os.path.abspath(final_path) 19 | p = os.path.join(p, filename) 20 | h = RotatingFileHandler(p, maxBytes=MAX_SIZE_BYTES, backupCount=BACKUP_COUNT) 21 | h.setLevel(level) 22 | h.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')) 23 | return h 24 | 25 | def log_path(): 26 | default_path = _get_default_path() 27 | given_path = CONSTANTS.DEFAULT_LOGGER_PATH 28 | final_path = given_path or default_path 29 | return final_path 30 | 31 | def _get_default_path(): 32 | default_path = CONSTANTS.DIR_PATH or get_user_home_dir() 33 | #default_path = default_path or os.path.dirname(sys.argv[0]) 34 | default_path = os.path.join(default_path, CONSTANTS.DIR_NAME, "logs") 35 | make_dir(default_path) 36 | return default_path 37 | 38 | def _logger_has_handler(logger): 39 | level = logger.getEffectiveLevel() 40 | current = logger 41 | while current: 42 | if any(h.level <= level for h in current.handlers): 43 | return True 44 | if not current.propagate: 45 | break 46 | current = current.parent 47 | return False 48 | 49 | @lru_cache(maxsize=10) 50 | def get_logger(logger_name=None): 51 | global _CURRENT_LOGGER, _DEFAULT_LOGGER 52 | if not logger_name: 53 | if _CURRENT_LOGGER: 54 | logger_name = _CURRENT_LOGGER 55 | else: 56 | logger_name = _DEFAULT_LOGGER 57 | logger = logging.getLogger(logger_name) 58 | _CURRENT_LOGGER = logger_name 59 | if logger.level == logging.NOTSET: 60 | logger.setLevel(logging.DEBUG) 61 | if _logger_has_handler(logger): 62 | return logger 63 | h = _default_handler(logger.level) 64 | logger.addHandler(h) 65 | return logger 66 | -------------------------------------------------------------------------------- /pybrowser/requester.py: -------------------------------------------------------------------------------- 1 | from concurrent.futures import ThreadPoolExecutor 2 | import requests 3 | from .exceptions import NotImplementedException 4 | 5 | class Requester(object): 6 | 7 | def __init__(self, headers=None, cookies=None, **kwargs): 8 | self.req_session = requests.Session() 9 | self._response = None 10 | self.future = None 11 | self._req_url=None 12 | 13 | def __enter__(self): 14 | return self 15 | 16 | def __exit__(self, *args): 17 | self.close() 18 | 19 | def get(self, url, future=False, headers=None, cookies=None, **kwargs): 20 | self._req_url = url 21 | self.future = None 22 | if not future: 23 | self._response = self.req_session.get(url, headers=headers, cookies=cookies, **kwargs) 24 | with ThreadPoolExecutor(max_workers=1) as executor: 25 | self.future = executor.submit(self.req_session.get, url, headers=headers, cookies=cookies, **kwargs) 26 | return self 27 | 28 | def post(self, url, future=False, body=None, headers=None, cookies=None, **kwargs): 29 | self._req_url = url 30 | self.future = None 31 | if not future: 32 | self._response = self.req_session.post(url, data=body, headers=headers, cookies=cookies, **kwargs) 33 | with ThreadPoolExecutor(max_workers=1) as executor: 34 | self.future = executor.submit(self.req_session.post, url, data=body, headers=headers, cookies=cookies, **kwargs) 35 | return self 36 | 37 | @property 38 | def response(self): 39 | if self.future: 40 | self._response = self.future.result() 41 | return self._response 42 | 43 | @property 44 | def is_request_done(self): 45 | if self.future: 46 | return self.future.done() 47 | return True 48 | 49 | @property 50 | def content(self): 51 | if not self.response: 52 | return 53 | return self.response.content 54 | 55 | @property 56 | def text(self): 57 | if not self.response: 58 | return "" 59 | return self.response.text 60 | 61 | @property 62 | def json(self): 63 | if not self.response: 64 | return 65 | json_ = "" 66 | try: 67 | json_ = self.response.json() 68 | except: 69 | pass 70 | return json_ 71 | 72 | @property 73 | def response_headers(self): 74 | if not self.response: 75 | return 76 | return self.response.headers 77 | 78 | @property 79 | def response_code(self): 80 | if not self.response: 81 | return 82 | return self.response.status_code 83 | 84 | @property 85 | def response_encoding(self): 86 | if not self.response: 87 | return 88 | return self.response.encoding 89 | 90 | def close(self): 91 | if self.req_session: 92 | try: self.req_session.close() 93 | except: pass 94 | if self.response and hasattr(self._response, "close"): 95 | try: self._response.close() 96 | except: pass 97 | -------------------------------------------------------------------------------- /readthedocs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = source 8 | BUILDDIR = build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /readthedocs/build/doctrees/environment.pickle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abranjith/pybrowser/8a4d435c7071e64e881f2c274fabd5cd7805ea34/readthedocs/build/doctrees/environment.pickle -------------------------------------------------------------------------------- /readthedocs/build/doctrees/index.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abranjith/pybrowser/8a4d435c7071e64e881f2c274fabd5cd7805ea34/readthedocs/build/doctrees/index.doctree -------------------------------------------------------------------------------- /readthedocs/build/html/.buildinfo: -------------------------------------------------------------------------------- 1 | # Sphinx build info version 1 2 | # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. 3 | config: 630260155f2022c1fe10b19354cff9be 4 | tags: 645f666f9bcd5a90fca523b33c5a78b7 5 | -------------------------------------------------------------------------------- /readthedocs/build/html/_static/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abranjith/pybrowser/8a4d435c7071e64e881f2c274fabd5cd7805ea34/readthedocs/build/html/_static/ajax-loader.gif -------------------------------------------------------------------------------- /readthedocs/build/html/_static/alabaster.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | @import url("basic.css"); 19 | 20 | /* -- page layout ----------------------------------------------------------- */ 21 | 22 | body { 23 | font-family: 'goudy old style', 'minion pro', 'bell mt', Georgia, 'Hiragino Mincho Pro', serif; 24 | font-size: 17px; 25 | background-color: white; 26 | color: #000; 27 | margin: 0; 28 | padding: 0; 29 | } 30 | 31 | 32 | div.document { 33 | width: 940px; 34 | margin: 30px auto 0 auto; 35 | } 36 | 37 | div.documentwrapper { 38 | float: left; 39 | width: 100%; 40 | } 41 | 42 | div.bodywrapper { 43 | margin: 0 0 0 220px; 44 | } 45 | 46 | div.sphinxsidebar { 47 | width: 220px; 48 | font-size: 14px; 49 | line-height: 1.5; 50 | } 51 | 52 | hr { 53 | border: 1px solid #B1B4B6; 54 | } 55 | 56 | div.body { 57 | background-color: #ffffff; 58 | color: #3E4349; 59 | padding: 0 30px 0 30px; 60 | } 61 | 62 | div.body > .section { 63 | text-align: left; 64 | } 65 | 66 | div.footer { 67 | width: 940px; 68 | margin: 20px auto 30px auto; 69 | font-size: 14px; 70 | color: #888; 71 | text-align: right; 72 | } 73 | 74 | div.footer a { 75 | color: #888; 76 | } 77 | 78 | p.caption { 79 | font-family: ; 80 | font-size: inherit; 81 | } 82 | 83 | 84 | div.relations { 85 | display: none; 86 | } 87 | 88 | 89 | div.sphinxsidebar a { 90 | color: #444; 91 | text-decoration: none; 92 | border-bottom: 1px dotted #999; 93 | } 94 | 95 | div.sphinxsidebar a:hover { 96 | border-bottom: 1px solid #999; 97 | } 98 | 99 | div.sphinxsidebarwrapper { 100 | padding: 18px 10px; 101 | } 102 | 103 | div.sphinxsidebarwrapper p.logo { 104 | padding: 0; 105 | margin: -10px 0 0 0px; 106 | text-align: center; 107 | } 108 | 109 | div.sphinxsidebarwrapper h1.logo { 110 | margin-top: -10px; 111 | text-align: center; 112 | margin-bottom: 5px; 113 | text-align: left; 114 | } 115 | 116 | div.sphinxsidebarwrapper h1.logo-name { 117 | margin-top: 0px; 118 | } 119 | 120 | div.sphinxsidebarwrapper p.blurb { 121 | margin-top: 0; 122 | font-style: normal; 123 | } 124 | 125 | div.sphinxsidebar h3, 126 | div.sphinxsidebar h4 { 127 | font-family: 'Garamond', 'Georgia', serif; 128 | color: #444; 129 | font-size: 24px; 130 | font-weight: normal; 131 | margin: 0 0 5px 0; 132 | padding: 0; 133 | } 134 | 135 | div.sphinxsidebar h4 { 136 | font-size: 20px; 137 | } 138 | 139 | div.sphinxsidebar h3 a { 140 | color: #444; 141 | } 142 | 143 | div.sphinxsidebar p.logo a, 144 | div.sphinxsidebar h3 a, 145 | div.sphinxsidebar p.logo a:hover, 146 | div.sphinxsidebar h3 a:hover { 147 | border: none; 148 | } 149 | 150 | div.sphinxsidebar p { 151 | color: #555; 152 | margin: 10px 0; 153 | } 154 | 155 | div.sphinxsidebar ul { 156 | margin: 10px 0; 157 | padding: 0; 158 | color: #000; 159 | } 160 | 161 | div.sphinxsidebar ul li.toctree-l1 > a { 162 | font-size: 120%; 163 | } 164 | 165 | div.sphinxsidebar ul li.toctree-l2 > a { 166 | font-size: 110%; 167 | } 168 | 169 | div.sphinxsidebar input { 170 | border: 1px solid #CCC; 171 | font-family: 'goudy old style', 'minion pro', 'bell mt', Georgia, 'Hiragino Mincho Pro', serif; 172 | font-size: 1em; 173 | } 174 | 175 | div.sphinxsidebar hr { 176 | border: none; 177 | height: 1px; 178 | color: #AAA; 179 | background: #AAA; 180 | 181 | text-align: left; 182 | margin-left: 0; 183 | width: 50%; 184 | } 185 | 186 | /* -- body styles ----------------------------------------------------------- */ 187 | 188 | a { 189 | color: #004B6B; 190 | text-decoration: underline; 191 | } 192 | 193 | a:hover { 194 | color: #6D4100; 195 | text-decoration: underline; 196 | } 197 | 198 | div.body h1, 199 | div.body h2, 200 | div.body h3, 201 | div.body h4, 202 | div.body h5, 203 | div.body h6 { 204 | font-family: 'Garamond', 'Georgia', serif; 205 | font-weight: normal; 206 | margin: 30px 0px 10px 0px; 207 | padding: 0; 208 | } 209 | 210 | div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; } 211 | div.body h2 { font-size: 180%; } 212 | div.body h3 { font-size: 150%; } 213 | div.body h4 { font-size: 130%; } 214 | div.body h5 { font-size: 100%; } 215 | div.body h6 { font-size: 100%; } 216 | 217 | a.headerlink { 218 | color: #DDD; 219 | padding: 0 4px; 220 | text-decoration: none; 221 | } 222 | 223 | a.headerlink:hover { 224 | color: #444; 225 | background: #EAEAEA; 226 | } 227 | 228 | div.body p, div.body dd, div.body li { 229 | line-height: 1.4em; 230 | } 231 | 232 | div.admonition { 233 | margin: 20px 0px; 234 | padding: 10px 30px; 235 | background-color: #FCC; 236 | border: 1px solid #FAA; 237 | } 238 | 239 | div.admonition tt.xref, div.admonition a tt { 240 | border-bottom: 1px solid #fafafa; 241 | } 242 | 243 | dd div.admonition { 244 | margin-left: -60px; 245 | padding-left: 60px; 246 | } 247 | 248 | div.admonition p.admonition-title { 249 | font-family: 'Garamond', 'Georgia', serif; 250 | font-weight: normal; 251 | font-size: 24px; 252 | margin: 0 0 10px 0; 253 | padding: 0; 254 | line-height: 1; 255 | } 256 | 257 | div.admonition p.last { 258 | margin-bottom: 0; 259 | } 260 | 261 | div.highlight { 262 | background-color: white; 263 | } 264 | 265 | dt:target, .highlight { 266 | background: #FAF3E8; 267 | } 268 | 269 | div.note { 270 | background-color: #EEE; 271 | border: 1px solid #CCC; 272 | } 273 | 274 | div.seealso { 275 | background-color: #EEE; 276 | border: 1px solid #CCC; 277 | } 278 | 279 | div.topic { 280 | background-color: #eee; 281 | } 282 | 283 | p.admonition-title { 284 | display: inline; 285 | } 286 | 287 | p.admonition-title:after { 288 | content: ":"; 289 | } 290 | 291 | pre, tt, code { 292 | font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; 293 | font-size: 0.9em; 294 | } 295 | 296 | .hll { 297 | background-color: #FFC; 298 | margin: 0 -12px; 299 | padding: 0 12px; 300 | display: block; 301 | } 302 | 303 | img.screenshot { 304 | } 305 | 306 | tt.descname, tt.descclassname, code.descname, code.descclassname { 307 | font-size: 0.95em; 308 | } 309 | 310 | tt.descname, code.descname { 311 | padding-right: 0.08em; 312 | } 313 | 314 | img.screenshot { 315 | -moz-box-shadow: 2px 2px 4px #eee; 316 | -webkit-box-shadow: 2px 2px 4px #eee; 317 | box-shadow: 2px 2px 4px #eee; 318 | } 319 | 320 | table.docutils { 321 | border: 1px solid #888; 322 | -moz-box-shadow: 2px 2px 4px #eee; 323 | -webkit-box-shadow: 2px 2px 4px #eee; 324 | box-shadow: 2px 2px 4px #eee; 325 | } 326 | 327 | table.docutils td, table.docutils th { 328 | border: 1px solid #888; 329 | padding: 0.25em 0.7em; 330 | } 331 | 332 | table.field-list, table.footnote { 333 | border: none; 334 | -moz-box-shadow: none; 335 | -webkit-box-shadow: none; 336 | box-shadow: none; 337 | } 338 | 339 | table.footnote { 340 | margin: 15px 0; 341 | width: 100%; 342 | border: 1px solid #EEE; 343 | background: #FDFDFD; 344 | font-size: 0.9em; 345 | } 346 | 347 | table.footnote + table.footnote { 348 | margin-top: -15px; 349 | border-top: none; 350 | } 351 | 352 | table.field-list th { 353 | padding: 0 0.8em 0 0; 354 | } 355 | 356 | table.field-list td { 357 | padding: 0; 358 | } 359 | 360 | table.field-list p { 361 | margin-bottom: 0.8em; 362 | } 363 | 364 | table.footnote td.label { 365 | width: .1px; 366 | padding: 0.3em 0 0.3em 0.5em; 367 | } 368 | 369 | table.footnote td { 370 | padding: 0.3em 0.5em; 371 | } 372 | 373 | dl { 374 | margin: 0; 375 | padding: 0; 376 | } 377 | 378 | dl dd { 379 | margin-left: 30px; 380 | } 381 | 382 | blockquote { 383 | margin: 0 0 0 30px; 384 | padding: 0; 385 | } 386 | 387 | ul, ol { 388 | /* Matches the 30px from the narrow-screen "li > ul" selector below */ 389 | margin: 10px 0 10px 30px; 390 | padding: 0; 391 | } 392 | 393 | pre { 394 | background: #EEE; 395 | padding: 7px 30px; 396 | margin: 15px 0px; 397 | line-height: 1.3em; 398 | } 399 | 400 | dl pre, blockquote pre, li pre { 401 | margin-left: 0; 402 | padding-left: 30px; 403 | } 404 | 405 | dl dl pre { 406 | margin-left: -90px; 407 | padding-left: 90px; 408 | } 409 | 410 | tt, code { 411 | background-color: #ecf0f3; 412 | color: #222; 413 | /* padding: 1px 2px; */ 414 | } 415 | 416 | tt.xref, code.xref, a tt { 417 | background-color: #FBFBFB; 418 | border-bottom: 1px solid white; 419 | } 420 | 421 | a.reference { 422 | text-decoration: none; 423 | border-bottom: 1px dotted #004B6B; 424 | } 425 | 426 | /* Don't put an underline on images */ 427 | a.image-reference, a.image-reference:hover { 428 | border-bottom: none; 429 | } 430 | 431 | a.reference:hover { 432 | border-bottom: 1px solid #6D4100; 433 | } 434 | 435 | a.footnote-reference { 436 | text-decoration: none; 437 | font-size: 0.7em; 438 | vertical-align: top; 439 | border-bottom: 1px dotted #004B6B; 440 | } 441 | 442 | a.footnote-reference:hover { 443 | border-bottom: 1px solid #6D4100; 444 | } 445 | 446 | a:hover tt, a:hover code { 447 | background: #EEE; 448 | } 449 | 450 | 451 | @media screen and (max-width: 870px) { 452 | 453 | div.sphinxsidebar { 454 | display: none; 455 | } 456 | 457 | div.document { 458 | width: 100%; 459 | 460 | } 461 | 462 | div.documentwrapper { 463 | margin-left: 0; 464 | margin-top: 0; 465 | margin-right: 0; 466 | margin-bottom: 0; 467 | } 468 | 469 | div.bodywrapper { 470 | margin-top: 0; 471 | margin-right: 0; 472 | margin-bottom: 0; 473 | margin-left: 0; 474 | } 475 | 476 | ul { 477 | margin-left: 0; 478 | } 479 | 480 | li > ul { 481 | /* Matches the 30px from the "ul, ol" selector above */ 482 | margin-left: 30px; 483 | } 484 | 485 | .document { 486 | width: auto; 487 | } 488 | 489 | .footer { 490 | width: auto; 491 | } 492 | 493 | .bodywrapper { 494 | margin: 0; 495 | } 496 | 497 | .footer { 498 | width: auto; 499 | } 500 | 501 | .github { 502 | display: none; 503 | } 504 | 505 | 506 | 507 | } 508 | 509 | 510 | 511 | @media screen and (max-width: 875px) { 512 | 513 | body { 514 | margin: 0; 515 | padding: 20px 30px; 516 | } 517 | 518 | div.documentwrapper { 519 | float: none; 520 | background: white; 521 | } 522 | 523 | div.sphinxsidebar { 524 | display: block; 525 | float: none; 526 | width: 102.5%; 527 | margin: 50px -30px -20px -30px; 528 | padding: 10px 20px; 529 | background: #333; 530 | color: #FFF; 531 | } 532 | 533 | div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, 534 | div.sphinxsidebar h3 a { 535 | color: white; 536 | } 537 | 538 | div.sphinxsidebar a { 539 | color: #AAA; 540 | } 541 | 542 | div.sphinxsidebar p.logo { 543 | display: none; 544 | } 545 | 546 | div.document { 547 | width: 100%; 548 | margin: 0; 549 | } 550 | 551 | div.footer { 552 | display: none; 553 | } 554 | 555 | div.bodywrapper { 556 | margin: 0; 557 | } 558 | 559 | div.body { 560 | min-height: 0; 561 | padding: 0; 562 | } 563 | 564 | .rtd_doc_footer { 565 | display: none; 566 | } 567 | 568 | .document { 569 | width: auto; 570 | } 571 | 572 | .footer { 573 | width: auto; 574 | } 575 | 576 | .footer { 577 | width: auto; 578 | } 579 | 580 | .github { 581 | display: none; 582 | } 583 | } 584 | 585 | 586 | /* misc. */ 587 | 588 | .revsys-inline { 589 | display: none!important; 590 | } 591 | 592 | /* Make nested-list/multi-paragraph items look better in Releases changelog 593 | * pages. Without this, docutils' magical list fuckery causes inconsistent 594 | * formatting between different release sub-lists. 595 | */ 596 | div#changelog > div.section > ul > li > p:only-child { 597 | margin-bottom: 0; 598 | } 599 | 600 | /* Hide fugly table cell borders in ..bibliography:: directive output */ 601 | table.docutils.citation, table.docutils.citation td, table.docutils.citation th { 602 | border: none; 603 | /* Below needed in some edge cases; if not applied, bottom shadows appear */ 604 | -moz-box-shadow: none; 605 | -webkit-box-shadow: none; 606 | box-shadow: none; 607 | } -------------------------------------------------------------------------------- /readthedocs/build/html/_static/basic.css: -------------------------------------------------------------------------------- 1 | /* 2 | * basic.css 3 | * ~~~~~~~~~ 4 | * 5 | * Sphinx stylesheet -- basic theme. 6 | * 7 | * :copyright: Copyright 2007-2016 by the Sphinx team, see AUTHORS. 8 | * :license: BSD, see LICENSE for details. 9 | * 10 | */ 11 | 12 | /* -- main layout ----------------------------------------------------------- */ 13 | 14 | div.clearer { 15 | clear: both; 16 | } 17 | 18 | /* -- relbar ---------------------------------------------------------------- */ 19 | 20 | div.related { 21 | width: 100%; 22 | font-size: 90%; 23 | } 24 | 25 | div.related h3 { 26 | display: none; 27 | } 28 | 29 | div.related ul { 30 | margin: 0; 31 | padding: 0 0 0 10px; 32 | list-style: none; 33 | } 34 | 35 | div.related li { 36 | display: inline; 37 | } 38 | 39 | div.related li.right { 40 | float: right; 41 | margin-right: 5px; 42 | } 43 | 44 | /* -- sidebar --------------------------------------------------------------- */ 45 | 46 | div.sphinxsidebarwrapper { 47 | padding: 10px 5px 0 10px; 48 | } 49 | 50 | div.sphinxsidebar { 51 | float: left; 52 | width: 230px; 53 | margin-left: -100%; 54 | font-size: 90%; 55 | word-wrap: break-word; 56 | overflow-wrap : break-word; 57 | } 58 | 59 | div.sphinxsidebar ul { 60 | list-style: none; 61 | } 62 | 63 | div.sphinxsidebar ul ul, 64 | div.sphinxsidebar ul.want-points { 65 | margin-left: 20px; 66 | list-style: square; 67 | } 68 | 69 | div.sphinxsidebar ul ul { 70 | margin-top: 0; 71 | margin-bottom: 0; 72 | } 73 | 74 | div.sphinxsidebar form { 75 | margin-top: 10px; 76 | } 77 | 78 | div.sphinxsidebar input { 79 | border: 1px solid #98dbcc; 80 | font-family: sans-serif; 81 | font-size: 1em; 82 | } 83 | 84 | div.sphinxsidebar #searchbox input[type="text"] { 85 | width: 170px; 86 | } 87 | 88 | img { 89 | border: 0; 90 | max-width: 100%; 91 | } 92 | 93 | /* -- search page ----------------------------------------------------------- */ 94 | 95 | ul.search { 96 | margin: 10px 0 0 20px; 97 | padding: 0; 98 | } 99 | 100 | ul.search li { 101 | padding: 5px 0 5px 20px; 102 | background-image: url(file.png); 103 | background-repeat: no-repeat; 104 | background-position: 0 7px; 105 | } 106 | 107 | ul.search li a { 108 | font-weight: bold; 109 | } 110 | 111 | ul.search li div.context { 112 | color: #888; 113 | margin: 2px 0 0 30px; 114 | text-align: left; 115 | } 116 | 117 | ul.keywordmatches li.goodmatch a { 118 | font-weight: bold; 119 | } 120 | 121 | /* -- index page ------------------------------------------------------------ */ 122 | 123 | table.contentstable { 124 | width: 90%; 125 | } 126 | 127 | table.contentstable p.biglink { 128 | line-height: 150%; 129 | } 130 | 131 | a.biglink { 132 | font-size: 1.3em; 133 | } 134 | 135 | span.linkdescr { 136 | font-style: italic; 137 | padding-top: 5px; 138 | font-size: 90%; 139 | } 140 | 141 | /* -- general index --------------------------------------------------------- */ 142 | 143 | table.indextable { 144 | width: 100%; 145 | } 146 | 147 | table.indextable td { 148 | text-align: left; 149 | vertical-align: top; 150 | } 151 | 152 | table.indextable dl, table.indextable dd { 153 | margin-top: 0; 154 | margin-bottom: 0; 155 | } 156 | 157 | table.indextable tr.pcap { 158 | height: 10px; 159 | } 160 | 161 | table.indextable tr.cap { 162 | margin-top: 10px; 163 | background-color: #f2f2f2; 164 | } 165 | 166 | img.toggler { 167 | margin-right: 3px; 168 | margin-top: 3px; 169 | cursor: pointer; 170 | } 171 | 172 | div.modindex-jumpbox { 173 | border-top: 1px solid #ddd; 174 | border-bottom: 1px solid #ddd; 175 | margin: 1em 0 1em 0; 176 | padding: 0.4em; 177 | } 178 | 179 | div.genindex-jumpbox { 180 | border-top: 1px solid #ddd; 181 | border-bottom: 1px solid #ddd; 182 | margin: 1em 0 1em 0; 183 | padding: 0.4em; 184 | } 185 | 186 | /* -- general body styles --------------------------------------------------- */ 187 | 188 | div.body p, div.body dd, div.body li, div.body blockquote { 189 | -moz-hyphens: auto; 190 | -ms-hyphens: auto; 191 | -webkit-hyphens: auto; 192 | hyphens: auto; 193 | } 194 | 195 | a.headerlink { 196 | visibility: hidden; 197 | } 198 | 199 | h1:hover > a.headerlink, 200 | h2:hover > a.headerlink, 201 | h3:hover > a.headerlink, 202 | h4:hover > a.headerlink, 203 | h5:hover > a.headerlink, 204 | h6:hover > a.headerlink, 205 | dt:hover > a.headerlink, 206 | caption:hover > a.headerlink, 207 | p.caption:hover > a.headerlink, 208 | div.code-block-caption:hover > a.headerlink { 209 | visibility: visible; 210 | } 211 | 212 | div.body p.caption { 213 | text-align: inherit; 214 | } 215 | 216 | div.body td { 217 | text-align: left; 218 | } 219 | 220 | .field-list ul { 221 | padding-left: 1em; 222 | } 223 | 224 | .first { 225 | margin-top: 0 !important; 226 | } 227 | 228 | p.rubric { 229 | margin-top: 30px; 230 | font-weight: bold; 231 | } 232 | 233 | img.align-left, .figure.align-left, object.align-left { 234 | clear: left; 235 | float: left; 236 | margin-right: 1em; 237 | } 238 | 239 | img.align-right, .figure.align-right, object.align-right { 240 | clear: right; 241 | float: right; 242 | margin-left: 1em; 243 | } 244 | 245 | img.align-center, .figure.align-center, object.align-center { 246 | display: block; 247 | margin-left: auto; 248 | margin-right: auto; 249 | } 250 | 251 | .align-left { 252 | text-align: left; 253 | } 254 | 255 | .align-center { 256 | text-align: center; 257 | } 258 | 259 | .align-right { 260 | text-align: right; 261 | } 262 | 263 | /* -- sidebars -------------------------------------------------------------- */ 264 | 265 | div.sidebar { 266 | margin: 0 0 0.5em 1em; 267 | border: 1px solid #ddb; 268 | padding: 7px 7px 0 7px; 269 | background-color: #ffe; 270 | width: 40%; 271 | float: right; 272 | } 273 | 274 | p.sidebar-title { 275 | font-weight: bold; 276 | } 277 | 278 | /* -- topics ---------------------------------------------------------------- */ 279 | 280 | div.topic { 281 | border: 1px solid #ccc; 282 | padding: 7px 7px 0 7px; 283 | margin: 10px 0 10px 0; 284 | } 285 | 286 | p.topic-title { 287 | font-size: 1.1em; 288 | font-weight: bold; 289 | margin-top: 10px; 290 | } 291 | 292 | /* -- admonitions ----------------------------------------------------------- */ 293 | 294 | div.admonition { 295 | margin-top: 10px; 296 | margin-bottom: 10px; 297 | padding: 7px; 298 | } 299 | 300 | div.admonition dt { 301 | font-weight: bold; 302 | } 303 | 304 | div.admonition dl { 305 | margin-bottom: 0; 306 | } 307 | 308 | p.admonition-title { 309 | margin: 0px 10px 5px 0px; 310 | font-weight: bold; 311 | } 312 | 313 | div.body p.centered { 314 | text-align: center; 315 | margin-top: 25px; 316 | } 317 | 318 | /* -- tables ---------------------------------------------------------------- */ 319 | 320 | table.docutils { 321 | border: 0; 322 | border-collapse: collapse; 323 | } 324 | 325 | table caption span.caption-number { 326 | font-style: italic; 327 | } 328 | 329 | table caption span.caption-text { 330 | } 331 | 332 | table.docutils td, table.docutils th { 333 | padding: 1px 8px 1px 5px; 334 | border-top: 0; 335 | border-left: 0; 336 | border-right: 0; 337 | border-bottom: 1px solid #aaa; 338 | } 339 | 340 | table.field-list td, table.field-list th { 341 | border: 0 !important; 342 | } 343 | 344 | table.footnote td, table.footnote th { 345 | border: 0 !important; 346 | } 347 | 348 | th { 349 | text-align: left; 350 | padding-right: 5px; 351 | } 352 | 353 | table.citation { 354 | border-left: solid 1px gray; 355 | margin-left: 1px; 356 | } 357 | 358 | table.citation td { 359 | border-bottom: none; 360 | } 361 | 362 | /* -- figures --------------------------------------------------------------- */ 363 | 364 | div.figure { 365 | margin: 0.5em; 366 | padding: 0.5em; 367 | } 368 | 369 | div.figure p.caption { 370 | padding: 0.3em; 371 | } 372 | 373 | div.figure p.caption span.caption-number { 374 | font-style: italic; 375 | } 376 | 377 | div.figure p.caption span.caption-text { 378 | } 379 | 380 | 381 | /* -- other body styles ----------------------------------------------------- */ 382 | 383 | ol.arabic { 384 | list-style: decimal; 385 | } 386 | 387 | ol.loweralpha { 388 | list-style: lower-alpha; 389 | } 390 | 391 | ol.upperalpha { 392 | list-style: upper-alpha; 393 | } 394 | 395 | ol.lowerroman { 396 | list-style: lower-roman; 397 | } 398 | 399 | ol.upperroman { 400 | list-style: upper-roman; 401 | } 402 | 403 | dl { 404 | margin-bottom: 15px; 405 | } 406 | 407 | dd p { 408 | margin-top: 0px; 409 | } 410 | 411 | dd ul, dd table { 412 | margin-bottom: 10px; 413 | } 414 | 415 | dd { 416 | margin-top: 3px; 417 | margin-bottom: 10px; 418 | margin-left: 30px; 419 | } 420 | 421 | dt:target, .highlighted { 422 | background-color: #fbe54e; 423 | } 424 | 425 | dl.glossary dt { 426 | font-weight: bold; 427 | font-size: 1.1em; 428 | } 429 | 430 | .field-list ul { 431 | margin: 0; 432 | padding-left: 1em; 433 | } 434 | 435 | .field-list p { 436 | margin: 0; 437 | } 438 | 439 | .optional { 440 | font-size: 1.3em; 441 | } 442 | 443 | .sig-paren { 444 | font-size: larger; 445 | } 446 | 447 | .versionmodified { 448 | font-style: italic; 449 | } 450 | 451 | .system-message { 452 | background-color: #fda; 453 | padding: 5px; 454 | border: 3px solid red; 455 | } 456 | 457 | .footnote:target { 458 | background-color: #ffa; 459 | } 460 | 461 | .line-block { 462 | display: block; 463 | margin-top: 1em; 464 | margin-bottom: 1em; 465 | } 466 | 467 | .line-block .line-block { 468 | margin-top: 0; 469 | margin-bottom: 0; 470 | margin-left: 1.5em; 471 | } 472 | 473 | .guilabel, .menuselection { 474 | font-family: sans-serif; 475 | } 476 | 477 | .accelerator { 478 | text-decoration: underline; 479 | } 480 | 481 | .classifier { 482 | font-style: oblique; 483 | } 484 | 485 | abbr, acronym { 486 | border-bottom: dotted 1px; 487 | cursor: help; 488 | } 489 | 490 | /* -- code displays --------------------------------------------------------- */ 491 | 492 | pre { 493 | overflow: auto; 494 | overflow-y: hidden; /* fixes display issues on Chrome browsers */ 495 | } 496 | 497 | td.linenos pre { 498 | padding: 5px 0px; 499 | border: 0; 500 | background-color: transparent; 501 | color: #aaa; 502 | } 503 | 504 | table.highlighttable { 505 | margin-left: 0.5em; 506 | } 507 | 508 | table.highlighttable td { 509 | padding: 0 0.5em 0 0.5em; 510 | } 511 | 512 | div.code-block-caption { 513 | padding: 2px 5px; 514 | font-size: small; 515 | } 516 | 517 | div.code-block-caption code { 518 | background-color: transparent; 519 | } 520 | 521 | div.code-block-caption + div > div.highlight > pre { 522 | margin-top: 0; 523 | } 524 | 525 | div.code-block-caption span.caption-number { 526 | padding: 0.1em 0.3em; 527 | font-style: italic; 528 | } 529 | 530 | div.code-block-caption span.caption-text { 531 | } 532 | 533 | div.literal-block-wrapper { 534 | padding: 1em 1em 0; 535 | } 536 | 537 | div.literal-block-wrapper div.highlight { 538 | margin: 0; 539 | } 540 | 541 | code.descname { 542 | background-color: transparent; 543 | font-weight: bold; 544 | font-size: 1.2em; 545 | } 546 | 547 | code.descclassname { 548 | background-color: transparent; 549 | } 550 | 551 | code.xref, a code { 552 | background-color: transparent; 553 | font-weight: bold; 554 | } 555 | 556 | h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { 557 | background-color: transparent; 558 | } 559 | 560 | .viewcode-link { 561 | float: right; 562 | } 563 | 564 | .viewcode-back { 565 | float: right; 566 | font-family: sans-serif; 567 | } 568 | 569 | div.viewcode-block:target { 570 | margin: -1px -10px; 571 | padding: 0 10px; 572 | } 573 | 574 | /* -- math display ---------------------------------------------------------- */ 575 | 576 | img.math { 577 | vertical-align: middle; 578 | } 579 | 580 | div.body div.math p { 581 | text-align: center; 582 | } 583 | 584 | span.eqno { 585 | float: right; 586 | } 587 | 588 | /* -- printout stylesheet --------------------------------------------------- */ 589 | 590 | @media print { 591 | div.document, 592 | div.documentwrapper, 593 | div.bodywrapper { 594 | margin: 0 !important; 595 | width: 100%; 596 | } 597 | 598 | div.sphinxsidebar, 599 | div.related, 600 | div.footer, 601 | #top-link { 602 | display: none; 603 | } 604 | } -------------------------------------------------------------------------------- /readthedocs/build/html/_static/comment-bright.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abranjith/pybrowser/8a4d435c7071e64e881f2c274fabd5cd7805ea34/readthedocs/build/html/_static/comment-bright.png -------------------------------------------------------------------------------- /readthedocs/build/html/_static/comment-close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abranjith/pybrowser/8a4d435c7071e64e881f2c274fabd5cd7805ea34/readthedocs/build/html/_static/comment-close.png -------------------------------------------------------------------------------- /readthedocs/build/html/_static/comment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abranjith/pybrowser/8a4d435c7071e64e881f2c274fabd5cd7805ea34/readthedocs/build/html/_static/comment.png -------------------------------------------------------------------------------- /readthedocs/build/html/_static/custom.css: -------------------------------------------------------------------------------- 1 | /* This file intentionally left blank. */ 2 | -------------------------------------------------------------------------------- /readthedocs/build/html/_static/doctools.js: -------------------------------------------------------------------------------- 1 | /* 2 | * doctools.js 3 | * ~~~~~~~~~~~ 4 | * 5 | * Sphinx JavaScript utilities for all documentation. 6 | * 7 | * :copyright: Copyright 2007-2016 by the Sphinx team, see AUTHORS. 8 | * :license: BSD, see LICENSE for details. 9 | * 10 | */ 11 | 12 | /** 13 | * select a different prefix for underscore 14 | */ 15 | $u = _.noConflict(); 16 | 17 | /** 18 | * make the code below compatible with browsers without 19 | * an installed firebug like debugger 20 | if (!window.console || !console.firebug) { 21 | var names = ["log", "debug", "info", "warn", "error", "assert", "dir", 22 | "dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace", 23 | "profile", "profileEnd"]; 24 | window.console = {}; 25 | for (var i = 0; i < names.length; ++i) 26 | window.console[names[i]] = function() {}; 27 | } 28 | */ 29 | 30 | /** 31 | * small helper function to urldecode strings 32 | */ 33 | jQuery.urldecode = function(x) { 34 | return decodeURIComponent(x).replace(/\+/g, ' '); 35 | }; 36 | 37 | /** 38 | * small helper function to urlencode strings 39 | */ 40 | jQuery.urlencode = encodeURIComponent; 41 | 42 | /** 43 | * This function returns the parsed url parameters of the 44 | * current request. Multiple values per key are supported, 45 | * it will always return arrays of strings for the value parts. 46 | */ 47 | jQuery.getQueryParameters = function(s) { 48 | if (typeof s == 'undefined') 49 | s = document.location.search; 50 | var parts = s.substr(s.indexOf('?') + 1).split('&'); 51 | var result = {}; 52 | for (var i = 0; i < parts.length; i++) { 53 | var tmp = parts[i].split('=', 2); 54 | var key = jQuery.urldecode(tmp[0]); 55 | var value = jQuery.urldecode(tmp[1]); 56 | if (key in result) 57 | result[key].push(value); 58 | else 59 | result[key] = [value]; 60 | } 61 | return result; 62 | }; 63 | 64 | /** 65 | * highlight a given string on a jquery object by wrapping it in 66 | * span elements with the given class name. 67 | */ 68 | jQuery.fn.highlightText = function(text, className) { 69 | function highlight(node) { 70 | if (node.nodeType == 3) { 71 | var val = node.nodeValue; 72 | var pos = val.toLowerCase().indexOf(text); 73 | if (pos >= 0 && !jQuery(node.parentNode).hasClass(className)) { 74 | var span = document.createElement("span"); 75 | span.className = className; 76 | span.appendChild(document.createTextNode(val.substr(pos, text.length))); 77 | node.parentNode.insertBefore(span, node.parentNode.insertBefore( 78 | document.createTextNode(val.substr(pos + text.length)), 79 | node.nextSibling)); 80 | node.nodeValue = val.substr(0, pos); 81 | } 82 | } 83 | else if (!jQuery(node).is("button, select, textarea")) { 84 | jQuery.each(node.childNodes, function() { 85 | highlight(this); 86 | }); 87 | } 88 | } 89 | return this.each(function() { 90 | highlight(this); 91 | }); 92 | }; 93 | 94 | /* 95 | * backward compatibility for jQuery.browser 96 | * This will be supported until firefox bug is fixed. 97 | */ 98 | if (!jQuery.browser) { 99 | jQuery.uaMatch = function(ua) { 100 | ua = ua.toLowerCase(); 101 | 102 | var match = /(chrome)[ \/]([\w.]+)/.exec(ua) || 103 | /(webkit)[ \/]([\w.]+)/.exec(ua) || 104 | /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || 105 | /(msie) ([\w.]+)/.exec(ua) || 106 | ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || 107 | []; 108 | 109 | return { 110 | browser: match[ 1 ] || "", 111 | version: match[ 2 ] || "0" 112 | }; 113 | }; 114 | jQuery.browser = {}; 115 | jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true; 116 | } 117 | 118 | /** 119 | * Small JavaScript module for the documentation. 120 | */ 121 | var Documentation = { 122 | 123 | init : function() { 124 | this.fixFirefoxAnchorBug(); 125 | this.highlightSearchWords(); 126 | this.initIndexTable(); 127 | 128 | }, 129 | 130 | /** 131 | * i18n support 132 | */ 133 | TRANSLATIONS : {}, 134 | PLURAL_EXPR : function(n) { return n == 1 ? 0 : 1; }, 135 | LOCALE : 'unknown', 136 | 137 | // gettext and ngettext don't access this so that the functions 138 | // can safely bound to a different name (_ = Documentation.gettext) 139 | gettext : function(string) { 140 | var translated = Documentation.TRANSLATIONS[string]; 141 | if (typeof translated == 'undefined') 142 | return string; 143 | return (typeof translated == 'string') ? translated : translated[0]; 144 | }, 145 | 146 | ngettext : function(singular, plural, n) { 147 | var translated = Documentation.TRANSLATIONS[singular]; 148 | if (typeof translated == 'undefined') 149 | return (n == 1) ? singular : plural; 150 | return translated[Documentation.PLURALEXPR(n)]; 151 | }, 152 | 153 | addTranslations : function(catalog) { 154 | for (var key in catalog.messages) 155 | this.TRANSLATIONS[key] = catalog.messages[key]; 156 | this.PLURAL_EXPR = new Function('n', 'return +(' + catalog.plural_expr + ')'); 157 | this.LOCALE = catalog.locale; 158 | }, 159 | 160 | /** 161 | * add context elements like header anchor links 162 | */ 163 | addContextElements : function() { 164 | $('div[id] > :header:first').each(function() { 165 | $('\u00B6'). 166 | attr('href', '#' + this.id). 167 | attr('title', _('Permalink to this headline')). 168 | appendTo(this); 169 | }); 170 | $('dt[id]').each(function() { 171 | $('\u00B6'). 172 | attr('href', '#' + this.id). 173 | attr('title', _('Permalink to this definition')). 174 | appendTo(this); 175 | }); 176 | }, 177 | 178 | /** 179 | * workaround a firefox stupidity 180 | * see: https://bugzilla.mozilla.org/show_bug.cgi?id=645075 181 | */ 182 | fixFirefoxAnchorBug : function() { 183 | if (document.location.hash) 184 | window.setTimeout(function() { 185 | document.location.href += ''; 186 | }, 10); 187 | }, 188 | 189 | /** 190 | * highlight the search words provided in the url in the text 191 | */ 192 | highlightSearchWords : function() { 193 | var params = $.getQueryParameters(); 194 | var terms = (params.highlight) ? params.highlight[0].split(/\s+/) : []; 195 | if (terms.length) { 196 | var body = $('div.body'); 197 | if (!body.length) { 198 | body = $('body'); 199 | } 200 | window.setTimeout(function() { 201 | $.each(terms, function() { 202 | body.highlightText(this.toLowerCase(), 'highlighted'); 203 | }); 204 | }, 10); 205 | $('') 207 | .appendTo($('#searchbox')); 208 | } 209 | }, 210 | 211 | /** 212 | * init the domain index toggle buttons 213 | */ 214 | initIndexTable : function() { 215 | var togglers = $('img.toggler').click(function() { 216 | var src = $(this).attr('src'); 217 | var idnum = $(this).attr('id').substr(7); 218 | $('tr.cg-' + idnum).toggle(); 219 | if (src.substr(-9) == 'minus.png') 220 | $(this).attr('src', src.substr(0, src.length-9) + 'plus.png'); 221 | else 222 | $(this).attr('src', src.substr(0, src.length-8) + 'minus.png'); 223 | }).css('display', ''); 224 | if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) { 225 | togglers.click(); 226 | } 227 | }, 228 | 229 | /** 230 | * helper function to hide the search marks again 231 | */ 232 | hideSearchWords : function() { 233 | $('#searchbox .highlight-link').fadeOut(300); 234 | $('span.highlighted').removeClass('highlighted'); 235 | }, 236 | 237 | /** 238 | * make the url absolute 239 | */ 240 | makeURL : function(relativeURL) { 241 | return DOCUMENTATION_OPTIONS.URL_ROOT + '/' + relativeURL; 242 | }, 243 | 244 | /** 245 | * get the current relative url 246 | */ 247 | getCurrentURL : function() { 248 | var path = document.location.pathname; 249 | var parts = path.split(/\//); 250 | $.each(DOCUMENTATION_OPTIONS.URL_ROOT.split(/\//), function() { 251 | if (this == '..') 252 | parts.pop(); 253 | }); 254 | var url = parts.join('/'); 255 | return path.substring(url.lastIndexOf('/') + 1, path.length - 1); 256 | }, 257 | 258 | initOnKeyListeners: function() { 259 | $(document).keyup(function(event) { 260 | var activeElementType = document.activeElement.tagName; 261 | // don't navigate when in search box or textarea 262 | if (activeElementType !== 'TEXTAREA' && activeElementType !== 'INPUT' && activeElementType !== 'SELECT') { 263 | switch (event.keyCode) { 264 | case 37: // left 265 | var prevHref = $('link[rel="prev"]').prop('href'); 266 | if (prevHref) { 267 | window.location.href = prevHref; 268 | return false; 269 | } 270 | case 39: // right 271 | var nextHref = $('link[rel="next"]').prop('href'); 272 | if (nextHref) { 273 | window.location.href = nextHref; 274 | return false; 275 | } 276 | } 277 | } 278 | }); 279 | } 280 | }; 281 | 282 | // quick alias for translations 283 | _ = Documentation.gettext; 284 | 285 | $(document).ready(function() { 286 | Documentation.init(); 287 | }); -------------------------------------------------------------------------------- /readthedocs/build/html/_static/documentation_options.js: -------------------------------------------------------------------------------- 1 | var DOCUMENTATION_OPTIONS = { 2 | URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'), 3 | VERSION: '0.0.1', 4 | LANGUAGE: 'None', 5 | COLLAPSE_INDEX: false, 6 | FILE_SUFFIX: '.html', 7 | HAS_SOURCE: true, 8 | SOURCELINK_SUFFIX: '.txt', 9 | NAVIGATION_WITH_KEYS: false 10 | }; -------------------------------------------------------------------------------- /readthedocs/build/html/_static/down-pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abranjith/pybrowser/8a4d435c7071e64e881f2c274fabd5cd7805ea34/readthedocs/build/html/_static/down-pressed.png -------------------------------------------------------------------------------- /readthedocs/build/html/_static/down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abranjith/pybrowser/8a4d435c7071e64e881f2c274fabd5cd7805ea34/readthedocs/build/html/_static/down.png -------------------------------------------------------------------------------- /readthedocs/build/html/_static/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abranjith/pybrowser/8a4d435c7071e64e881f2c274fabd5cd7805ea34/readthedocs/build/html/_static/file.png -------------------------------------------------------------------------------- /readthedocs/build/html/_static/language_data.js: -------------------------------------------------------------------------------- 1 | /* 2 | * language_data.js 3 | * ~~~~~~~~~~~~~~~~ 4 | * 5 | * This script contains the language-specific data used by searchtools.js, 6 | * namely the list of stopwords, stemmer, scorer and splitter. 7 | * 8 | * :copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS. 9 | * :license: BSD, see LICENSE for details. 10 | * 11 | */ 12 | 13 | var stopwords = ["a","and","are","as","at","be","but","by","for","if","in","into","is","it","near","no","not","of","on","or","such","that","the","their","then","there","these","they","this","to","was","will","with"]; 14 | 15 | 16 | /* Non-minified version JS is _stemmer.js if file is provided */ 17 | /** 18 | * Porter Stemmer 19 | */ 20 | var Stemmer = function() { 21 | 22 | var step2list = { 23 | ational: 'ate', 24 | tional: 'tion', 25 | enci: 'ence', 26 | anci: 'ance', 27 | izer: 'ize', 28 | bli: 'ble', 29 | alli: 'al', 30 | entli: 'ent', 31 | eli: 'e', 32 | ousli: 'ous', 33 | ization: 'ize', 34 | ation: 'ate', 35 | ator: 'ate', 36 | alism: 'al', 37 | iveness: 'ive', 38 | fulness: 'ful', 39 | ousness: 'ous', 40 | aliti: 'al', 41 | iviti: 'ive', 42 | biliti: 'ble', 43 | logi: 'log' 44 | }; 45 | 46 | var step3list = { 47 | icate: 'ic', 48 | ative: '', 49 | alize: 'al', 50 | iciti: 'ic', 51 | ical: 'ic', 52 | ful: '', 53 | ness: '' 54 | }; 55 | 56 | var c = "[^aeiou]"; // consonant 57 | var v = "[aeiouy]"; // vowel 58 | var C = c + "[^aeiouy]*"; // consonant sequence 59 | var V = v + "[aeiou]*"; // vowel sequence 60 | 61 | var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0 62 | var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1 63 | var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1 64 | var s_v = "^(" + C + ")?" + v; // vowel in stem 65 | 66 | this.stemWord = function (w) { 67 | var stem; 68 | var suffix; 69 | var firstch; 70 | var origword = w; 71 | 72 | if (w.length < 3) 73 | return w; 74 | 75 | var re; 76 | var re2; 77 | var re3; 78 | var re4; 79 | 80 | firstch = w.substr(0,1); 81 | if (firstch == "y") 82 | w = firstch.toUpperCase() + w.substr(1); 83 | 84 | // Step 1a 85 | re = /^(.+?)(ss|i)es$/; 86 | re2 = /^(.+?)([^s])s$/; 87 | 88 | if (re.test(w)) 89 | w = w.replace(re,"$1$2"); 90 | else if (re2.test(w)) 91 | w = w.replace(re2,"$1$2"); 92 | 93 | // Step 1b 94 | re = /^(.+?)eed$/; 95 | re2 = /^(.+?)(ed|ing)$/; 96 | if (re.test(w)) { 97 | var fp = re.exec(w); 98 | re = new RegExp(mgr0); 99 | if (re.test(fp[1])) { 100 | re = /.$/; 101 | w = w.replace(re,""); 102 | } 103 | } 104 | else if (re2.test(w)) { 105 | var fp = re2.exec(w); 106 | stem = fp[1]; 107 | re2 = new RegExp(s_v); 108 | if (re2.test(stem)) { 109 | w = stem; 110 | re2 = /(at|bl|iz)$/; 111 | re3 = new RegExp("([^aeiouylsz])\\1$"); 112 | re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); 113 | if (re2.test(w)) 114 | w = w + "e"; 115 | else if (re3.test(w)) { 116 | re = /.$/; 117 | w = w.replace(re,""); 118 | } 119 | else if (re4.test(w)) 120 | w = w + "e"; 121 | } 122 | } 123 | 124 | // Step 1c 125 | re = /^(.+?)y$/; 126 | if (re.test(w)) { 127 | var fp = re.exec(w); 128 | stem = fp[1]; 129 | re = new RegExp(s_v); 130 | if (re.test(stem)) 131 | w = stem + "i"; 132 | } 133 | 134 | // Step 2 135 | re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; 136 | if (re.test(w)) { 137 | var fp = re.exec(w); 138 | stem = fp[1]; 139 | suffix = fp[2]; 140 | re = new RegExp(mgr0); 141 | if (re.test(stem)) 142 | w = stem + step2list[suffix]; 143 | } 144 | 145 | // Step 3 146 | re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; 147 | if (re.test(w)) { 148 | var fp = re.exec(w); 149 | stem = fp[1]; 150 | suffix = fp[2]; 151 | re = new RegExp(mgr0); 152 | if (re.test(stem)) 153 | w = stem + step3list[suffix]; 154 | } 155 | 156 | // Step 4 157 | re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; 158 | re2 = /^(.+?)(s|t)(ion)$/; 159 | if (re.test(w)) { 160 | var fp = re.exec(w); 161 | stem = fp[1]; 162 | re = new RegExp(mgr1); 163 | if (re.test(stem)) 164 | w = stem; 165 | } 166 | else if (re2.test(w)) { 167 | var fp = re2.exec(w); 168 | stem = fp[1] + fp[2]; 169 | re2 = new RegExp(mgr1); 170 | if (re2.test(stem)) 171 | w = stem; 172 | } 173 | 174 | // Step 5 175 | re = /^(.+?)e$/; 176 | if (re.test(w)) { 177 | var fp = re.exec(w); 178 | stem = fp[1]; 179 | re = new RegExp(mgr1); 180 | re2 = new RegExp(meq1); 181 | re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); 182 | if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) 183 | w = stem; 184 | } 185 | re = /ll$/; 186 | re2 = new RegExp(mgr1); 187 | if (re.test(w) && re2.test(w)) { 188 | re = /.$/; 189 | w = w.replace(re,""); 190 | } 191 | 192 | // and turn initial Y back to y 193 | if (firstch == "y") 194 | w = firstch.toLowerCase() + w.substr(1); 195 | return w; 196 | } 197 | } 198 | 199 | 200 | 201 | 202 | 203 | var splitChars = (function() { 204 | var result = {}; 205 | var singles = [96, 180, 187, 191, 215, 247, 749, 885, 903, 907, 909, 930, 1014, 1648, 206 | 1748, 1809, 2416, 2473, 2481, 2526, 2601, 2609, 2612, 2615, 2653, 2702, 207 | 2706, 2729, 2737, 2740, 2857, 2865, 2868, 2910, 2928, 2948, 2961, 2971, 208 | 2973, 3085, 3089, 3113, 3124, 3213, 3217, 3241, 3252, 3295, 3341, 3345, 209 | 3369, 3506, 3516, 3633, 3715, 3721, 3736, 3744, 3748, 3750, 3756, 3761, 210 | 3781, 3912, 4239, 4347, 4681, 4695, 4697, 4745, 4785, 4799, 4801, 4823, 211 | 4881, 5760, 5901, 5997, 6313, 7405, 8024, 8026, 8028, 8030, 8117, 8125, 212 | 8133, 8181, 8468, 8485, 8487, 8489, 8494, 8527, 11311, 11359, 11687, 11695, 213 | 11703, 11711, 11719, 11727, 11735, 12448, 12539, 43010, 43014, 43019, 43587, 214 | 43696, 43713, 64286, 64297, 64311, 64317, 64319, 64322, 64325, 65141]; 215 | var i, j, start, end; 216 | for (i = 0; i < singles.length; i++) { 217 | result[singles[i]] = true; 218 | } 219 | var ranges = [[0, 47], [58, 64], [91, 94], [123, 169], [171, 177], [182, 184], [706, 709], 220 | [722, 735], [741, 747], [751, 879], [888, 889], [894, 901], [1154, 1161], 221 | [1318, 1328], [1367, 1368], [1370, 1376], [1416, 1487], [1515, 1519], [1523, 1568], 222 | [1611, 1631], [1642, 1645], [1750, 1764], [1767, 1773], [1789, 1790], [1792, 1807], 223 | [1840, 1868], [1958, 1968], [1970, 1983], [2027, 2035], [2038, 2041], [2043, 2047], 224 | [2070, 2073], [2075, 2083], [2085, 2087], [2089, 2307], [2362, 2364], [2366, 2383], 225 | [2385, 2391], [2402, 2405], [2419, 2424], [2432, 2436], [2445, 2446], [2449, 2450], 226 | [2483, 2485], [2490, 2492], [2494, 2509], [2511, 2523], [2530, 2533], [2546, 2547], 227 | [2554, 2564], [2571, 2574], [2577, 2578], [2618, 2648], [2655, 2661], [2672, 2673], 228 | [2677, 2692], [2746, 2748], [2750, 2767], [2769, 2783], [2786, 2789], [2800, 2820], 229 | [2829, 2830], [2833, 2834], [2874, 2876], [2878, 2907], [2914, 2917], [2930, 2946], 230 | [2955, 2957], [2966, 2968], [2976, 2978], [2981, 2983], [2987, 2989], [3002, 3023], 231 | [3025, 3045], [3059, 3076], [3130, 3132], [3134, 3159], [3162, 3167], [3170, 3173], 232 | [3184, 3191], [3199, 3204], [3258, 3260], [3262, 3293], [3298, 3301], [3312, 3332], 233 | [3386, 3388], [3390, 3423], [3426, 3429], [3446, 3449], [3456, 3460], [3479, 3481], 234 | [3518, 3519], [3527, 3584], [3636, 3647], [3655, 3663], [3674, 3712], [3717, 3718], 235 | [3723, 3724], [3726, 3731], [3752, 3753], [3764, 3772], [3774, 3775], [3783, 3791], 236 | [3802, 3803], [3806, 3839], [3841, 3871], [3892, 3903], [3949, 3975], [3980, 4095], 237 | [4139, 4158], [4170, 4175], [4182, 4185], [4190, 4192], [4194, 4196], [4199, 4205], 238 | [4209, 4212], [4226, 4237], [4250, 4255], [4294, 4303], [4349, 4351], [4686, 4687], 239 | [4702, 4703], [4750, 4751], [4790, 4791], [4806, 4807], [4886, 4887], [4955, 4968], 240 | [4989, 4991], [5008, 5023], [5109, 5120], [5741, 5742], [5787, 5791], [5867, 5869], 241 | [5873, 5887], [5906, 5919], [5938, 5951], [5970, 5983], [6001, 6015], [6068, 6102], 242 | [6104, 6107], [6109, 6111], [6122, 6127], [6138, 6159], [6170, 6175], [6264, 6271], 243 | [6315, 6319], [6390, 6399], [6429, 6469], [6510, 6511], [6517, 6527], [6572, 6592], 244 | [6600, 6607], [6619, 6655], [6679, 6687], [6741, 6783], [6794, 6799], [6810, 6822], 245 | [6824, 6916], [6964, 6980], [6988, 6991], [7002, 7042], [7073, 7085], [7098, 7167], 246 | [7204, 7231], [7242, 7244], [7294, 7400], [7410, 7423], [7616, 7679], [7958, 7959], 247 | [7966, 7967], [8006, 8007], [8014, 8015], [8062, 8063], [8127, 8129], [8141, 8143], 248 | [8148, 8149], [8156, 8159], [8173, 8177], [8189, 8303], [8306, 8307], [8314, 8318], 249 | [8330, 8335], [8341, 8449], [8451, 8454], [8456, 8457], [8470, 8472], [8478, 8483], 250 | [8506, 8507], [8512, 8516], [8522, 8525], [8586, 9311], [9372, 9449], [9472, 10101], 251 | [10132, 11263], [11493, 11498], [11503, 11516], [11518, 11519], [11558, 11567], 252 | [11622, 11630], [11632, 11647], [11671, 11679], [11743, 11822], [11824, 12292], 253 | [12296, 12320], [12330, 12336], [12342, 12343], [12349, 12352], [12439, 12444], 254 | [12544, 12548], [12590, 12592], [12687, 12689], [12694, 12703], [12728, 12783], 255 | [12800, 12831], [12842, 12880], [12896, 12927], [12938, 12976], [12992, 13311], 256 | [19894, 19967], [40908, 40959], [42125, 42191], [42238, 42239], [42509, 42511], 257 | [42540, 42559], [42592, 42593], [42607, 42622], [42648, 42655], [42736, 42774], 258 | [42784, 42785], [42889, 42890], [42893, 43002], [43043, 43055], [43062, 43071], 259 | [43124, 43137], [43188, 43215], [43226, 43249], [43256, 43258], [43260, 43263], 260 | [43302, 43311], [43335, 43359], [43389, 43395], [43443, 43470], [43482, 43519], 261 | [43561, 43583], [43596, 43599], [43610, 43615], [43639, 43641], [43643, 43647], 262 | [43698, 43700], [43703, 43704], [43710, 43711], [43715, 43738], [43742, 43967], 263 | [44003, 44015], [44026, 44031], [55204, 55215], [55239, 55242], [55292, 55295], 264 | [57344, 63743], [64046, 64047], [64110, 64111], [64218, 64255], [64263, 64274], 265 | [64280, 64284], [64434, 64466], [64830, 64847], [64912, 64913], [64968, 65007], 266 | [65020, 65135], [65277, 65295], [65306, 65312], [65339, 65344], [65371, 65381], 267 | [65471, 65473], [65480, 65481], [65488, 65489], [65496, 65497]]; 268 | for (i = 0; i < ranges.length; i++) { 269 | start = ranges[i][0]; 270 | end = ranges[i][1]; 271 | for (j = start; j <= end; j++) { 272 | result[j] = true; 273 | } 274 | } 275 | return result; 276 | })(); 277 | 278 | function splitQuery(query) { 279 | var result = []; 280 | var start = -1; 281 | for (var i = 0; i < query.length; i++) { 282 | if (splitChars[query.charCodeAt(i)]) { 283 | if (start !== -1) { 284 | result.push(query.slice(start, i)); 285 | start = -1; 286 | } 287 | } else if (start === -1) { 288 | start = i; 289 | } 290 | } 291 | if (start !== -1) { 292 | result.push(query.slice(start)); 293 | } 294 | return result; 295 | } 296 | 297 | 298 | -------------------------------------------------------------------------------- /readthedocs/build/html/_static/minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abranjith/pybrowser/8a4d435c7071e64e881f2c274fabd5cd7805ea34/readthedocs/build/html/_static/minus.png -------------------------------------------------------------------------------- /readthedocs/build/html/_static/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abranjith/pybrowser/8a4d435c7071e64e881f2c274fabd5cd7805ea34/readthedocs/build/html/_static/plus.png -------------------------------------------------------------------------------- /readthedocs/build/html/_static/pygments.css: -------------------------------------------------------------------------------- 1 | .highlight .hll { background-color: #ffffcc } 2 | .highlight { background: #f8f8f8; } 3 | .highlight .c { color: #8f5902; font-style: italic } /* Comment */ 4 | .highlight .err { color: #a40000; border: 1px solid #ef2929 } /* Error */ 5 | .highlight .g { color: #000000 } /* Generic */ 6 | .highlight .k { color: #004461; font-weight: bold } /* Keyword */ 7 | .highlight .l { color: #000000 } /* Literal */ 8 | .highlight .n { color: #000000 } /* Name */ 9 | .highlight .o { color: #582800 } /* Operator */ 10 | .highlight .x { color: #000000 } /* Other */ 11 | .highlight .p { color: #000000; font-weight: bold } /* Punctuation */ 12 | .highlight .ch { color: #8f5902; font-style: italic } /* Comment.Hashbang */ 13 | .highlight .cm { color: #8f5902; font-style: italic } /* Comment.Multiline */ 14 | .highlight .cp { color: #8f5902 } /* Comment.Preproc */ 15 | .highlight .cpf { color: #8f5902; font-style: italic } /* Comment.PreprocFile */ 16 | .highlight .c1 { color: #8f5902; font-style: italic } /* Comment.Single */ 17 | .highlight .cs { color: #8f5902; font-style: italic } /* Comment.Special */ 18 | .highlight .gd { color: #a40000 } /* Generic.Deleted */ 19 | .highlight .ge { color: #000000; font-style: italic } /* Generic.Emph */ 20 | .highlight .gr { color: #ef2929 } /* Generic.Error */ 21 | .highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 22 | .highlight .gi { color: #00A000 } /* Generic.Inserted */ 23 | .highlight .go { color: #888888 } /* Generic.Output */ 24 | .highlight .gp { color: #745334 } /* Generic.Prompt */ 25 | .highlight .gs { color: #000000; font-weight: bold } /* Generic.Strong */ 26 | .highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 27 | .highlight .gt { color: #a40000; font-weight: bold } /* Generic.Traceback */ 28 | .highlight .kc { color: #004461; font-weight: bold } /* Keyword.Constant */ 29 | .highlight .kd { color: #004461; font-weight: bold } /* Keyword.Declaration */ 30 | .highlight .kn { color: #004461; font-weight: bold } /* Keyword.Namespace */ 31 | .highlight .kp { color: #004461; font-weight: bold } /* Keyword.Pseudo */ 32 | .highlight .kr { color: #004461; font-weight: bold } /* Keyword.Reserved */ 33 | .highlight .kt { color: #004461; font-weight: bold } /* Keyword.Type */ 34 | .highlight .ld { color: #000000 } /* Literal.Date */ 35 | .highlight .m { color: #990000 } /* Literal.Number */ 36 | .highlight .s { color: #4e9a06 } /* Literal.String */ 37 | .highlight .na { color: #c4a000 } /* Name.Attribute */ 38 | .highlight .nb { color: #004461 } /* Name.Builtin */ 39 | .highlight .nc { color: #000000 } /* Name.Class */ 40 | .highlight .no { color: #000000 } /* Name.Constant */ 41 | .highlight .nd { color: #888888 } /* Name.Decorator */ 42 | .highlight .ni { color: #ce5c00 } /* Name.Entity */ 43 | .highlight .ne { color: #cc0000; font-weight: bold } /* Name.Exception */ 44 | .highlight .nf { color: #000000 } /* Name.Function */ 45 | .highlight .nl { color: #f57900 } /* Name.Label */ 46 | .highlight .nn { color: #000000 } /* Name.Namespace */ 47 | .highlight .nx { color: #000000 } /* Name.Other */ 48 | .highlight .py { color: #000000 } /* Name.Property */ 49 | .highlight .nt { color: #004461; font-weight: bold } /* Name.Tag */ 50 | .highlight .nv { color: #000000 } /* Name.Variable */ 51 | .highlight .ow { color: #004461; font-weight: bold } /* Operator.Word */ 52 | .highlight .w { color: #f8f8f8; text-decoration: underline } /* Text.Whitespace */ 53 | .highlight .mb { color: #990000 } /* Literal.Number.Bin */ 54 | .highlight .mf { color: #990000 } /* Literal.Number.Float */ 55 | .highlight .mh { color: #990000 } /* Literal.Number.Hex */ 56 | .highlight .mi { color: #990000 } /* Literal.Number.Integer */ 57 | .highlight .mo { color: #990000 } /* Literal.Number.Oct */ 58 | .highlight .sb { color: #4e9a06 } /* Literal.String.Backtick */ 59 | .highlight .sc { color: #4e9a06 } /* Literal.String.Char */ 60 | .highlight .sd { color: #8f5902; font-style: italic } /* Literal.String.Doc */ 61 | .highlight .s2 { color: #4e9a06 } /* Literal.String.Double */ 62 | .highlight .se { color: #4e9a06 } /* Literal.String.Escape */ 63 | .highlight .sh { color: #4e9a06 } /* Literal.String.Heredoc */ 64 | .highlight .si { color: #4e9a06 } /* Literal.String.Interpol */ 65 | .highlight .sx { color: #4e9a06 } /* Literal.String.Other */ 66 | .highlight .sr { color: #4e9a06 } /* Literal.String.Regex */ 67 | .highlight .s1 { color: #4e9a06 } /* Literal.String.Single */ 68 | .highlight .ss { color: #4e9a06 } /* Literal.String.Symbol */ 69 | .highlight .bp { color: #3465a4 } /* Name.Builtin.Pseudo */ 70 | .highlight .vc { color: #000000 } /* Name.Variable.Class */ 71 | .highlight .vg { color: #000000 } /* Name.Variable.Global */ 72 | .highlight .vi { color: #000000 } /* Name.Variable.Instance */ 73 | .highlight .il { color: #990000 } /* Literal.Number.Integer.Long */ -------------------------------------------------------------------------------- /readthedocs/build/html/_static/underscore.js: -------------------------------------------------------------------------------- 1 | // Underscore.js 1.3.1 2 | // (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc. 3 | // Underscore is freely distributable under the MIT license. 4 | // Portions of Underscore are inspired or borrowed from Prototype, 5 | // Oliver Steele's Functional, and John Resig's Micro-Templating. 6 | // For all details and documentation: 7 | // http://documentcloud.github.com/underscore 8 | (function(){function q(a,c,d){if(a===c)return a!==0||1/a==1/c;if(a==null||c==null)return a===c;if(a._chain)a=a._wrapped;if(c._chain)c=c._wrapped;if(a.isEqual&&b.isFunction(a.isEqual))return a.isEqual(c);if(c.isEqual&&b.isFunction(c.isEqual))return c.isEqual(a);var e=l.call(a);if(e!=l.call(c))return false;switch(e){case "[object String]":return a==String(c);case "[object Number]":return a!=+a?c!=+c:a==0?1/a==1/c:a==+c;case "[object Date]":case "[object Boolean]":return+a==+c;case "[object RegExp]":return a.source== 9 | c.source&&a.global==c.global&&a.multiline==c.multiline&&a.ignoreCase==c.ignoreCase}if(typeof a!="object"||typeof c!="object")return false;for(var f=d.length;f--;)if(d[f]==a)return true;d.push(a);var f=0,g=true;if(e=="[object Array]"){if(f=a.length,g=f==c.length)for(;f--;)if(!(g=f in a==f in c&&q(a[f],c[f],d)))break}else{if("constructor"in a!="constructor"in c||a.constructor!=c.constructor)return false;for(var h in a)if(b.has(a,h)&&(f++,!(g=b.has(c,h)&&q(a[h],c[h],d))))break;if(g){for(h in c)if(b.has(c, 10 | h)&&!f--)break;g=!f}}d.pop();return g}var r=this,G=r._,n={},k=Array.prototype,o=Object.prototype,i=k.slice,H=k.unshift,l=o.toString,I=o.hasOwnProperty,w=k.forEach,x=k.map,y=k.reduce,z=k.reduceRight,A=k.filter,B=k.every,C=k.some,p=k.indexOf,D=k.lastIndexOf,o=Array.isArray,J=Object.keys,s=Function.prototype.bind,b=function(a){return new m(a)};if(typeof exports!=="undefined"){if(typeof module!=="undefined"&&module.exports)exports=module.exports=b;exports._=b}else r._=b;b.VERSION="1.3.1";var j=b.each= 11 | b.forEach=function(a,c,d){if(a!=null)if(w&&a.forEach===w)a.forEach(c,d);else if(a.length===+a.length)for(var e=0,f=a.length;e2;a== 12 | null&&(a=[]);if(y&&a.reduce===y)return e&&(c=b.bind(c,e)),f?a.reduce(c,d):a.reduce(c);j(a,function(a,b,i){f?d=c.call(e,d,a,b,i):(d=a,f=true)});if(!f)throw new TypeError("Reduce of empty array with no initial value");return d};b.reduceRight=b.foldr=function(a,c,d,e){var f=arguments.length>2;a==null&&(a=[]);if(z&&a.reduceRight===z)return e&&(c=b.bind(c,e)),f?a.reduceRight(c,d):a.reduceRight(c);var g=b.toArray(a).reverse();e&&!f&&(c=b.bind(c,e));return f?b.reduce(g,c,d,e):b.reduce(g,c)};b.find=b.detect= 13 | function(a,c,b){var e;E(a,function(a,g,h){if(c.call(b,a,g,h))return e=a,true});return e};b.filter=b.select=function(a,c,b){var e=[];if(a==null)return e;if(A&&a.filter===A)return a.filter(c,b);j(a,function(a,g,h){c.call(b,a,g,h)&&(e[e.length]=a)});return e};b.reject=function(a,c,b){var e=[];if(a==null)return e;j(a,function(a,g,h){c.call(b,a,g,h)||(e[e.length]=a)});return e};b.every=b.all=function(a,c,b){var e=true;if(a==null)return e;if(B&&a.every===B)return a.every(c,b);j(a,function(a,g,h){if(!(e= 14 | e&&c.call(b,a,g,h)))return n});return e};var E=b.some=b.any=function(a,c,d){c||(c=b.identity);var e=false;if(a==null)return e;if(C&&a.some===C)return a.some(c,d);j(a,function(a,b,h){if(e||(e=c.call(d,a,b,h)))return n});return!!e};b.include=b.contains=function(a,c){var b=false;if(a==null)return b;return p&&a.indexOf===p?a.indexOf(c)!=-1:b=E(a,function(a){return a===c})};b.invoke=function(a,c){var d=i.call(arguments,2);return b.map(a,function(a){return(b.isFunction(c)?c||a:a[c]).apply(a,d)})};b.pluck= 15 | function(a,c){return b.map(a,function(a){return a[c]})};b.max=function(a,c,d){if(!c&&b.isArray(a))return Math.max.apply(Math,a);if(!c&&b.isEmpty(a))return-Infinity;var e={computed:-Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;b>=e.computed&&(e={value:a,computed:b})});return e.value};b.min=function(a,c,d){if(!c&&b.isArray(a))return Math.min.apply(Math,a);if(!c&&b.isEmpty(a))return Infinity;var e={computed:Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;bd?1:0}),"value")};b.groupBy=function(a,c){var d={},e=b.isFunction(c)?c:function(a){return a[c]};j(a,function(a,b){var c=e(a,b);(d[c]||(d[c]=[])).push(a)});return d};b.sortedIndex=function(a, 17 | c,d){d||(d=b.identity);for(var e=0,f=a.length;e>1;d(a[g])=0})})};b.difference=function(a){var c=b.flatten(i.call(arguments,1));return b.filter(a,function(a){return!b.include(c,a)})};b.zip=function(){for(var a=i.call(arguments),c=b.max(b.pluck(a,"length")),d=Array(c),e=0;e=0;d--)b=[a[d].apply(this,b)];return b[0]}}; 24 | b.after=function(a,b){return a<=0?b():function(){if(--a<1)return b.apply(this,arguments)}};b.keys=J||function(a){if(a!==Object(a))throw new TypeError("Invalid object");var c=[],d;for(d in a)b.has(a,d)&&(c[c.length]=d);return c};b.values=function(a){return b.map(a,b.identity)};b.functions=b.methods=function(a){var c=[],d;for(d in a)b.isFunction(a[d])&&c.push(d);return c.sort()};b.extend=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]=b[d]});return a};b.defaults=function(a){j(i.call(arguments, 25 | 1),function(b){for(var d in b)a[d]==null&&(a[d]=b[d])});return a};b.clone=function(a){return!b.isObject(a)?a:b.isArray(a)?a.slice():b.extend({},a)};b.tap=function(a,b){b(a);return a};b.isEqual=function(a,b){return q(a,b,[])};b.isEmpty=function(a){if(b.isArray(a)||b.isString(a))return a.length===0;for(var c in a)if(b.has(a,c))return false;return true};b.isElement=function(a){return!!(a&&a.nodeType==1)};b.isArray=o||function(a){return l.call(a)=="[object Array]"};b.isObject=function(a){return a===Object(a)}; 26 | b.isArguments=function(a){return l.call(a)=="[object Arguments]"};if(!b.isArguments(arguments))b.isArguments=function(a){return!(!a||!b.has(a,"callee"))};b.isFunction=function(a){return l.call(a)=="[object Function]"};b.isString=function(a){return l.call(a)=="[object String]"};b.isNumber=function(a){return l.call(a)=="[object Number]"};b.isNaN=function(a){return a!==a};b.isBoolean=function(a){return a===true||a===false||l.call(a)=="[object Boolean]"};b.isDate=function(a){return l.call(a)=="[object Date]"}; 27 | b.isRegExp=function(a){return l.call(a)=="[object RegExp]"};b.isNull=function(a){return a===null};b.isUndefined=function(a){return a===void 0};b.has=function(a,b){return I.call(a,b)};b.noConflict=function(){r._=G;return this};b.identity=function(a){return a};b.times=function(a,b,d){for(var e=0;e/g,">").replace(/"/g,""").replace(/'/g,"'").replace(/\//g,"/")};b.mixin=function(a){j(b.functions(a), 28 | function(c){K(c,b[c]=a[c])})};var L=0;b.uniqueId=function(a){var b=L++;return a?a+b:b};b.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var t=/.^/,u=function(a){return a.replace(/\\\\/g,"\\").replace(/\\'/g,"'")};b.template=function(a,c){var d=b.templateSettings,d="var __p=[],print=function(){__p.push.apply(__p,arguments);};with(obj||{}){__p.push('"+a.replace(/\\/g,"\\\\").replace(/'/g,"\\'").replace(d.escape||t,function(a,b){return"',_.escape("+ 29 | u(b)+"),'"}).replace(d.interpolate||t,function(a,b){return"',"+u(b)+",'"}).replace(d.evaluate||t,function(a,b){return"');"+u(b).replace(/[\r\n\t]/g," ")+";__p.push('"}).replace(/\r/g,"\\r").replace(/\n/g,"\\n").replace(/\t/g,"\\t")+"');}return __p.join('');",e=new Function("obj","_",d);return c?e(c,b):function(a){return e.call(this,a,b)}};b.chain=function(a){return b(a).chain()};var m=function(a){this._wrapped=a};b.prototype=m.prototype;var v=function(a,c){return c?b(a).chain():a},K=function(a,c){m.prototype[a]= 30 | function(){var a=i.call(arguments);H.call(a,this._wrapped);return v(c.apply(b,a),this._chain)}};b.mixin(b);j("pop,push,reverse,shift,sort,splice,unshift".split(","),function(a){var b=k[a];m.prototype[a]=function(){var d=this._wrapped;b.apply(d,arguments);var e=d.length;(a=="shift"||a=="splice")&&e===0&&delete d[0];return v(d,this._chain)}});j(["concat","join","slice"],function(a){var b=k[a];m.prototype[a]=function(){return v(b.apply(this._wrapped,arguments),this._chain)}});m.prototype.chain=function(){this._chain= 31 | true;return this};m.prototype.value=function(){return this._wrapped}}).call(this); 32 | -------------------------------------------------------------------------------- /readthedocs/build/html/_static/up-pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abranjith/pybrowser/8a4d435c7071e64e881f2c274fabd5cd7805ea34/readthedocs/build/html/_static/up-pressed.png -------------------------------------------------------------------------------- /readthedocs/build/html/_static/up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abranjith/pybrowser/8a4d435c7071e64e881f2c274fabd5cd7805ea34/readthedocs/build/html/_static/up.png -------------------------------------------------------------------------------- /readthedocs/build/html/genindex.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Index — pybrowser 0.0.1 documentation 11 | 12 | 13 | 14 | 15 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 |
39 |
40 |
41 | 42 | 43 |

Index

44 | 45 |
46 | 47 |
48 | 49 | 50 |
51 |
52 |
53 | 76 |
77 |
78 | 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /readthedocs/build/html/objects.inv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abranjith/pybrowser/8a4d435c7071e64e881f2c274fabd5cd7805ea34/readthedocs/build/html/objects.inv -------------------------------------------------------------------------------- /readthedocs/build/html/search.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Search — pybrowser 0.0.1 documentation 10 | 11 | 12 | 13 | 14 | 23 | 24 | 25 | 26 | 27 | 28 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 |
45 |
46 |
47 |
48 | 49 |

Search

50 |
51 | 52 |

53 | Please activate JavaScript to enable the search 54 | functionality. 55 |

56 |
57 |

58 | From here you can search these documents. Enter your search 59 | words into the box below and click "search". Note that the search 60 | function will automatically search for all of the words. Pages 61 | containing fewer words won't appear in the result list. 62 |

63 |
64 | 65 | 66 | 67 |
68 | 69 |
70 | 71 |
72 | 73 |
74 |
75 |
76 | 86 |
87 |
88 | 96 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /readthedocs/build/html/searchindex.js: -------------------------------------------------------------------------------- 1 | Search.setIndex({envversion:49,filenames:["index"],objects:{},objnames:{},objtypes:{},terms:{"abstract":0,"byte":0,"case":0,"default":0,"function":0,"goto":0,"import":0,"new":0,"return":0,"switch":0,"throw":0,"true":0,"try":0,"while":0,abiliti:0,abl:0,abov:0,abranjith:0,accept:0,access:0,action:0,activ:0,actual:0,add:0,add_cooki:0,add_remove_el:0,add_to_ospath:0,addit:0,advanc:0,after:0,afterward:0,again:0,alert:0,alia:0,all:0,all_selected_options_el:0,all_selected_options_text:0,all_selected_options_valu:0,allow:0,alreadi:0,also:0,although:0,ani:0,anyth:0,apart:0,appli:0,applic:0,applicabl:0,around:0,arriv:0,as_filenam:0,asp:0,assum:0,asynch:0,asynchron:0,attempt:0,attribut:0,automat:0,avail:0,avoid:0,awesom:0,back:0,background:0,bar:0,base:0,basic:0,becom:0,been:0,befor:0,behavior:0,behind:0,belong:0,below:0,binary_cont:0,block:0,bodi:0,box:0,bro:0,browser_nam:0,browser_opt:0,browserdriv:0,btnk:0,built:0,bullet:0,bunch:0,button:0,cach:0,call:0,callabl:0,can:0,care:0,categori:0,certain:0,chang:0,check:0,check_box:0,checkbox:0,cheeseshop:[],choic:0,chrome:0,chrome_download_url:0,chrome_home_url:0,chromedriv:0,chromedriver_version:0,chromium:0,class_name:0,classess:0,clear:0,click:0,clientheight:0,clientwidth:0,clone:0,close:0,code:0,com:0,come:0,command:0,common:0,commun:0,complet:0,condit:0,condition:0,confus:0,consolid:0,contain:0,content:0,context:0,cooki:0,correspodn:0,correspond:0,coupl:0,cours:0,creat:0,css:0,css_class:0,css_selector:0,current:0,data:0,deal:0,deciss:0,default_cont:0,default_logger_name:0,default_logger_path:0,defect:0,del_zipfil:0,delet:0,delete_all_cooki:0,delete_cooki:0,delimit:0,deriv:0,describ:0,deselect_al:0,design:0,develop:0,devicepixelratio:0,devicescalefactor:0,dict:0,dir:0,directli:0,directori:0,displai:0,div:0,doc:0,documentel:0,doesn:0,dollar:0,dollarrupe:0,don:0,done:0,double_click:0,download:0,downloaded_fil:0,drag:0,drag_and_drop_at:0,driver:0,driver_path:0,drivers_download_dir_name:0,drop:0,dropdown:0,due:0,dure:0,dynam:0,easi:0,edge:0,egg:0,element:0,enabl:0,end:0,enough:0,enter:0,equal:0,error:0,essenti:0,etc:0,evalu:0,even:0,exampl:0,except:0,exchang:0,execut:0,execute_script:0,experienc:0,explain:0,explicitli:0,expos:0,expose:0,fals:0,favorit:0,feel:0,femal:0,file:0,filenam:0,fill_and_submit_form:0,find:0,find_by_class:0,find_by_css_selector:0,find_by_id:0,find_by_xpath:0,firefox:0,firefox_binary_path:0,firefox_download_url:0,firefox_home_url:0,firefox_profile_path:0,firefoxdriver_version:0,first:0,fix:0,flag:0,folder:0,follow:0,foo:0,forc:0,fork:0,form:0,form_data:0,format:0,forward:0,found:0,frame:0,frame_nam:0,free:0,from:0,fullscreen_window:0,further:0,gener:0,get:0,get_logg:0,git:0,github:0,give:0,given:0,gone:0,good:0,googl:0,grid:0,guess:0,gui:0,hand:0,handl:0,handler:0,happen:0,hard:0,have:0,header:0,headless:0,height:0,henc:0,here:0,herokuapp:0,highli:0,highlight:0,hold:0,home:0,hood:0,hook:0,how:0,href:0,html:0,html_dir_name:0,http:0,http_proxi:0,hub:0,idea:0,identifi:0,ie_download_url:0,ie_home_url:0,iedriver:0,iedriver_version:0,if_displai:0,if_en:0,if_found:0,if_stal:0,if_vis:0,imag:0,immedi:0,incognito:0,info:0,inform:0,input:0,inr:0,insensit:0,inspir:0,instal:0,instanc:0,instanti:0,instead:0,interact:0,interest:0,interfac:0,internet:0,invok:0,is_check:0,is_displai:0,is_download_complet:0,is_en:0,is_found:0,is_request_don:0,is_select:0,is_stal:0,is_vis:0,isn:0,issu:0,itself:0,javascript:0,json:0,jsut:0,just:0,keep:0,kei:0,kind:0,know:0,known:0,latest:0,lazi:0,librari:0,licens:0,lighweight:0,like:0,line:0,link:0,link_text:0,list:0,load:0,locat:0,locator_typ:0,locator_valu:0,log_adapt:0,logger:0,logger_nam:0,logic:0,login:0,look:0,lxml:0,main:0,make:0,manag:0,mani:0,market:0,maximize_window:0,mean:0,mention:0,mere:0,meta:0,method:0,might:0,mind:0,minimize_window:0,mit:0,more:0,most:0,move:0,move_to_el:0,much:0,multipl:0,name:0,named_tupl:0,natur:0,navig:0,need:0,note:0,obj:0,observ:0,occur:0,occurr:0,ofcours:0,once:0,one:0,onli:0,open:0,opera:0,option:0,options_el:0,options_text:0,options_valu:0,org:0,other:0,other_el:0,out:0,overhead:0,own:0,packag:0,page:0,paramet:0,pars:0,part:0,partial_link_text:0,pass:0,password:0,path:0,peculiar:0,per:0,perform:0,php:0,php_form_complet:0,physic:0,pip:0,pipenv:0,pipfil:0,plai:0,pleas:0,plu:0,png:0,point:0,poll:0,post:0,precheck:0,present:0,pretti:0,previou:0,print:0,prior:0,problem:0,profil:0,properti:0,provid:0,proxi:0,pull:0,puppet:0,pybrowser_dir_name:0,pybrowser_home_dir_path:0,pypi:0,pyppet:0,pyppeteer_dir_name:0,pyppeteer_home:0,pyqueri:0,python:0,radio:0,rate:0,rather:0,raw:[],readi:0,realli:0,recommend:0,refer:0,refresh:0,regular:0,rel:0,rel_link:0,relat:0,releas:0,rememb:0,remot:0,remote_url:0,render:0,repres:0,request:0,requests_sess:0,resolv:0,resourc:0,respons:0,response_cod:0,response_encod:0,response_head:0,restrict:0,result:0,right:0,rpc:0,rpt:[],rubi:0,run:0,rupe:0,safari:0,sai:0,same:0,save:0,save_path:0,saw:0,screenshot:0,screenshot_on_except:0,screenshot_path:0,screenshots_dir_name:0,script:0,search:0,search_al:0,search_text:0,second:0,section:0,secur:0,see:0,select:0,select_by_indic:0,select_by_visible_text:0,selenium:0,send:0,session:0,set:0,setup:0,should:0,signific:0,similar:0,simpl:0,simpler:0,simplifi:0,sinc:0,size:0,skip:0,sleep:0,solut:0,some:0,some_cssclass:0,some_id:0,some_nam:0,some_text:0,some_us:0,someus:0,sourc:0,special:0,specif:0,stale:0,start:0,starter:0,store:0,str:0,string:0,stuff:0,style:0,submit:0,suggest:0,supersecretpassword:0,support:0,sure:0,switch_to:0,synonym:0,syntax:0,system:0,tackl:0,tag:0,tag_nam:0,tag_name:0,take:0,take_screenshot:0,templat:0,term:0,test:0,text:0,text_cont:0,textarea:0,than:0,them:0,thi:0,thing:0,though:0,time:0,titl:0,to_el:0,to_loc:0,todai:0,tomsmith:0,too:0,toolkit:0,total:0,tupl:0,two:0,type:0,typic:0,uncheck:0,under:0,underli:0,unselect:0,until:0,unzip:0,upgrad:0,upload:0,upon:0,url:0,url_onli:0,usag:0,usd:0,use:0,use_text:0,user:0,user_func:0,usernam:0,valid:0,valu:0,variou:0,version:0,via:0,virtualenv:0,visibl:0,visit:0,w3school:0,wai:0,wait:0,wait_for_el:0,wait_for_page_load:0,wait_for_stal:0,wait_tim:0,wan:0,want:0,watir:0,webbrows:0,webdriv:0,webpag:0,well:0,when:0,where:0,which:0,width:0,window:0,wish:0,without:0,won:0,wonder:0,work:0,would:0,www:0,xpath:0,yet:0,you:0,your:0,your_custom_driv:0,zip:0},titles:["Welcome to pybrowser’s documentation"],titleterms:{about:0,anoth:0,api:0,autom:0,browser:0,contribut:0,detail:0,document:0,environ:0,featur:0,guid:0,indice:0,installat:0,log:0,mode:0,non:0,object:0,project:0,pybrows:0,requir:0,tabl:0,variabl:0,web:0,welcom:0,why:0}}) -------------------------------------------------------------------------------- /readthedocs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /readthedocs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | # import os 16 | # import sys 17 | # sys.path.insert(0, os.path.abspath('.')) 18 | 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = 'pybrowser' 23 | copyright = '2019, ranjith' 24 | author = 'ranjith' 25 | 26 | # The short X.Y version 27 | version = '' 28 | # The full version, including alpha/beta/rc tags 29 | release = '0.0.1' 30 | 31 | 32 | # -- General configuration --------------------------------------------------- 33 | 34 | # If your documentation needs a minimal Sphinx version, state it here. 35 | # 36 | # needs_sphinx = '1.0' 37 | 38 | # Add any Sphinx extension module names here, as strings. They can be 39 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 40 | # ones. 41 | extensions = [ 42 | ] 43 | 44 | # Add any paths that contain templates here, relative to this directory. 45 | templates_path = ['_templates'] 46 | 47 | # The suffix(es) of source filenames. 48 | # You can specify multiple suffix as a list of string: 49 | # 50 | # source_suffix = ['.rst', '.md'] 51 | source_suffix = '.rst' 52 | 53 | # The master toctree document. 54 | master_doc = 'index' 55 | 56 | # The language for content autogenerated by Sphinx. Refer to documentation 57 | # for a list of supported languages. 58 | # 59 | # This is also used if you do content translation via gettext catalogs. 60 | # Usually you set "language" from the command line for these cases. 61 | language = None 62 | 63 | # List of patterns, relative to source directory, that match files and 64 | # directories to ignore when looking for source files. 65 | # This pattern also affects html_static_path and html_extra_path. 66 | exclude_patterns = [] 67 | 68 | # The name of the Pygments (syntax highlighting) style to use. 69 | pygments_style = None 70 | 71 | 72 | # -- Options for HTML output ------------------------------------------------- 73 | 74 | # The theme to use for HTML and HTML Help pages. See the documentation for 75 | # a list of builtin themes. 76 | # 77 | html_theme = 'alabaster' 78 | 79 | # Theme options are theme-specific and customize the look and feel of a theme 80 | # further. For a list of options available for each theme, see the 81 | # documentation. 82 | # 83 | # html_theme_options = {} 84 | 85 | # Add any paths that contain custom static files (such as style sheets) here, 86 | # relative to this directory. They are copied after the builtin static files, 87 | # so a file named "default.css" will overwrite the builtin "default.css". 88 | html_static_path = ['_static'] 89 | 90 | # Custom sidebar templates, must be a dictionary that maps document names 91 | # to template names. 92 | # 93 | # The default sidebars (for documents that don't match any pattern) are 94 | # defined by theme itself. Builtin themes are using these templates by 95 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 96 | # 'searchbox.html']``. 97 | # 98 | # html_sidebars = {} 99 | 100 | 101 | # -- Options for HTMLHelp output --------------------------------------------- 102 | 103 | # Output file base name for HTML help builder. 104 | htmlhelp_basename = 'pybrowserdoc' 105 | 106 | 107 | # -- Options for LaTeX output ------------------------------------------------ 108 | 109 | latex_elements = { 110 | # The paper size ('letterpaper' or 'a4paper'). 111 | # 112 | # 'papersize': 'letterpaper', 113 | 114 | # The font size ('10pt', '11pt' or '12pt'). 115 | # 116 | # 'pointsize': '10pt', 117 | 118 | # Additional stuff for the LaTeX preamble. 119 | # 120 | # 'preamble': '', 121 | 122 | # Latex figure (float) alignment 123 | # 124 | # 'figure_align': 'htbp', 125 | } 126 | 127 | # Grouping the document tree into LaTeX files. List of tuples 128 | # (source start file, target name, title, 129 | # author, documentclass [howto, manual, or own class]). 130 | latex_documents = [ 131 | (master_doc, 'pybrowser.tex', 'pybrowser Documentation', 132 | 'ranjith', 'manual'), 133 | ] 134 | 135 | 136 | # -- Options for manual page output ------------------------------------------ 137 | 138 | # One entry per manual page. List of tuples 139 | # (source start file, name, description, authors, manual section). 140 | man_pages = [ 141 | (master_doc, 'pybrowser', 'pybrowser Documentation', 142 | [author], 1) 143 | ] 144 | 145 | 146 | # -- Options for Texinfo output ---------------------------------------------- 147 | 148 | # Grouping the document tree into Texinfo files. List of tuples 149 | # (source start file, target name, title, author, 150 | # dir menu entry, description, category) 151 | texinfo_documents = [ 152 | (master_doc, 'pybrowser', 'pybrowser Documentation', 153 | author, 'pybrowser', 'One line description of project.', 154 | 'Miscellaneous'), 155 | ] 156 | 157 | 158 | # -- Options for Epub output ------------------------------------------------- 159 | 160 | # Bibliographic Dublin Core info. 161 | epub_title = project 162 | 163 | # The unique identifier of the text. This can be a ISBN number 164 | # or the project homepage. 165 | # 166 | # epub_identifier = '' 167 | 168 | # A unique identification for the text. 169 | # 170 | # epub_uid = '' 171 | 172 | # A list of files that should not be packed into the epub file. 173 | epub_exclude_files = ['search.html'] 174 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -i https://pypi.org/simple 2 | appdirs==1.4.3 3 | certifi==2019.3.9 4 | chardet==3.0.4 5 | cssselect==1.0.3 6 | idna==2.8 7 | lxml==4.3.3 8 | pyee==5.0.0 9 | pyppeteer==0.0.25 10 | pyquery==1.4.0 11 | requests>=2.21.0 12 | selenium==3.141.0 13 | tqdm==4.31.1 14 | urllib3>=1.24.2 15 | websockets==7.0 16 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Note: To use the 'upload' functionality of this file, you must: 5 | # $ pip install twine 6 | 7 | import io 8 | import os 9 | 10 | from setuptools import setup 11 | 12 | # Package meta-data. 13 | NAME = 'get-pybrowser' 14 | DESCRIPTION = 'Selenium based, user friendly Browser Automation API' 15 | URL = 'https://github.com/abranjith/pybrowser' 16 | EMAIL = 'abranjith@gmail.com' 17 | AUTHOR = 'ranjith' 18 | VERSION = '0.2.0' 19 | README_CONTENT_TYPE = 'text/markdown' 20 | 21 | # What packages are required for this module to be executed? 22 | REQUIRED = [ 23 | 'requests', 'selenium==3.141.0', 'pyquery==1.4.0', 'pyppeteer==0.0.25' 24 | ] 25 | 26 | #here = os.path.abspath(os.path.dirname(__file__)) 27 | 28 | # Import the README and use it as the long-description. 29 | with open("README.md", "r") as fh: 30 | long_description = fh.read() 31 | 32 | # Where the magic happens: 33 | setup( 34 | name=NAME, 35 | version=VERSION, 36 | description=DESCRIPTION, 37 | long_description=long_description, 38 | long_description_content_type=README_CONTENT_TYPE, 39 | author=AUTHOR, 40 | author_email=EMAIL, 41 | url=URL, 42 | python_requires='>=3.7.0', 43 | # If your package is a single module, use this instead of 'packages': 44 | packages=['pybrowser', 'pybrowser.elements', 'pybrowser.external'], 45 | install_requires=REQUIRED, 46 | include_package_data=True, 47 | license='MIT', 48 | classifiers=[ 49 | 'License :: OSI Approved :: MIT License', 50 | 'Programming Language :: Python', 51 | 'Programming Language :: Python :: 3.7', 52 | 'Programming Language :: Python :: Implementation :: CPython', 53 | 'Programming Language :: Python :: Implementation :: PyPy' 54 | ] 55 | ) -------------------------------------------------------------------------------- /tests/action_tests.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Ranjith' 2 | 3 | import unittest 4 | import sys 5 | sys.path.append("..\\pybrowser") 6 | from selenium.common.exceptions import NoSuchElementException 7 | from pybrowser import Browser 8 | 9 | class ListenerTests(unittest.TestCase): 10 | 11 | def test_iffound(self): 12 | with Browser(browser_name=Browser.CHROME, wait_time=1, headless=True) as b: 13 | b.goto("https://the-internet.herokuapp.com/") 14 | #no such element 15 | btn = b.button("byby") 16 | self.assertFalse(btn.is_found) 17 | with self.assertRaises(NoSuchElementException): 18 | btn.is_displayed 19 | with self.assertRaises(NoSuchElementException): 20 | btn.is_enabled 21 | with self.assertRaises(NoSuchElementException): 22 | btn.is_stale 23 | self.assertIsNone(btn.if_found.click()) 24 | 25 | def test_cachedprops(self): 26 | BLANK = "" 27 | with Browser(browser_name=Browser.FIREFOX, headless=True) as b: 28 | b.goto("https://the-internet.herokuapp.com/forgot_password") 29 | btn = b.button("form_submit") 30 | self.assertTrue(btn.is_found) 31 | self.assertTrue(btn.is_displayed) 32 | self.assertTrue(btn.is_enabled) 33 | self.assertFalse(btn.is_stale) 34 | #print(btn.action_obj.__dict__) 35 | self.assertEqual(btn.tag_name, "button") 36 | self.assertEqual(btn.id, "form_submit") 37 | self.assertEqual(btn.name, BLANK) 38 | self.assertEqual(btn.type, "submit") 39 | self.assertEqual(btn.css_classes, ["radius"]) 40 | self.assertEqual(btn.value, BLANK) 41 | self.assertEqual(btn.text, "Retrieve password") 42 | self.assertIsNone(btn.href) 43 | #print(btn.action_obj.__dict__) 44 | btn.refresh() 45 | self.assertEqual(btn.id, "form_submit") 46 | #print("post refresh", btn.action_obj.__dict__) 47 | 48 | 49 | if __name__ == "__main__": 50 | unittest.main() -------------------------------------------------------------------------------- /tests/browsernav_tests.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Ranjith' 2 | 3 | import unittest 4 | import sys 5 | sys.path.append("..\\pybrowser") 6 | from pybrowser import Browser 7 | from pybrowser.common_utils import file_exists 8 | 9 | class SampleTests(unittest.TestCase): 10 | def test_sample(self): 11 | with Browser(browser_name=Browser.CHROME, headless=True) as b: 12 | b.goto("https://www.google.com/") 13 | b.refresh() 14 | b.back() 15 | b.forward() 16 | self.assertEqual(200, b.response_code) 17 | hd = b.response_headers 18 | #print(hd) 19 | self.assertTrue("text/html" in hd['Content-Type']) 20 | b.input("name:=q").enter("news") 21 | b.button("name:=btnK").click() 22 | f = b.take_screenshot() 23 | self.assertTrue(file_exists(f)) 24 | ls = b.html().elements.links() 25 | self.assertTrue(len(ls) > 0) 26 | 27 | if __name__ == "__main__": 28 | unittest.main() -------------------------------------------------------------------------------- /tests/browsers_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import sys 3 | sys.path.append("..\\pybrowser") 4 | from pybrowser import Browser 5 | 6 | class BrowserTests(unittest.TestCase): 7 | 8 | #IE issues, so skipping :-\ 9 | @unittest.SkipTest 10 | def test_ie(self): 11 | with Browser(browser_name=Browser.IE, headless=True) as b: 12 | e = b.goto("https://the-internet.herokuapp.com/").link("xpath:=//*[@id='content']/ul/li[1]/a") 13 | self.assertTrue(e.is_found) 14 | #print(e.id) 15 | #print(e.href) 16 | e.highlight() 17 | #e.element.send_keys("") 18 | e.click() 19 | t = b.element("content").text 20 | #print(t) 21 | self.assertTrue("Also known as split testing" in t) 22 | 23 | def test_chrome(self): 24 | with Browser(browser_name=Browser.CHROME, headless=True) as b: 25 | b.goto("https://the-internet.herokuapp.com/").link("xpath:=//*[@id='content']/ul/li[1]/a").click() 26 | t = b.element("content").text 27 | #print(t) 28 | self.assertTrue("Also known as split testing" in t) 29 | 30 | def test_firefox(self): 31 | with Browser(browser_name=Browser.FIREFOX, headless=True) as b: 32 | b.goto("https://the-internet.herokuapp.com/").link("xpath:=//*[@id='content']/ul/li[1]/a").click() 33 | t = b.element("content").text 34 | #print(t) 35 | self.assertTrue("Also known as split testing" in t) 36 | 37 | def test_nobrowser(self): 38 | with Browser() as b: 39 | h = b.html(url="https://the-internet.herokuapp.com/") 40 | l = h.elements.links() 41 | #print(l) 42 | self.assertTrue(len(l) == 2) 43 | 44 | if __name__ == "__main__": 45 | unittest.main() -------------------------------------------------------------------------------- /tests/content_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import sys 3 | sys.path.append("..\\pybrowser") 4 | from pybrowser import Browser 5 | 6 | class ContentTests(unittest.TestCase): 7 | def test_contents(self): 8 | bro = Browser(browser_name=Browser.CHROME, headless=True) 9 | bro.goto("https://httpbin.org/get") 10 | c = bro.content() 11 | self.assertTrue(isinstance(c, str) and r"" in c) 12 | rc = bro.content(raw=True) 13 | self.assertTrue(isinstance(rc, bytes)) 14 | #print(bro.html()) #html 15 | self.assertTrue(bro.json['url'] == "https://httpbin.org/get") #json 16 | self.assertTrue(r"Content-Type" in bro.response_headers) 17 | self.assertTrue(bro.response_code == 200) 18 | #print(bro.response_encoding) 19 | bro.close() 20 | 21 | def test_html(self): 22 | with Browser(browser_name = Browser.CHROME, incognito=True, headless=True) as b: 23 | b.goto("https://the-internet.herokuapp.com/notification_message_rendered") 24 | self.assertTrue("The message displayed above the heading is a notification message" in b.html().text) 25 | rc = b.content(raw=True) 26 | self.assertTrue(isinstance(rc, bytes)) 27 | self.assertTrue(b.response_encoding == "utf-8") 28 | 29 | if __name__ == "__main__": 30 | unittest.main() -------------------------------------------------------------------------------- /tests/downloader_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | import sys 4 | from datetime import datetime 5 | sys.path.append("..\\pybrowser") 6 | from pybrowser.downloader import download_url, webdriver_downloader 7 | from pybrowser.common_utils import file_exists, rm_files 8 | 9 | class DownloaderTests(unittest.TestCase): 10 | 11 | def test_download_url(self): 12 | url = "https://the-internet.herokuapp.com/download/some-file.txt" 13 | home_dir = os.getenv('HOME') or os.path.expanduser(os.getenv('USERPROFILE')) 14 | to_dir = os.path.join(home_dir, "tmp") 15 | f = os.path.join(to_dir, "some-file.txt") 16 | #preset 17 | if file_exists(f): 18 | rm_files(f) 19 | #before 20 | self.assertFalse(file_exists(f)) 21 | start = datetime.now() 22 | end = start 23 | download_url(url, to_dir=to_dir, overwrite_existing=True) #async True by default 24 | #to test asynchronous download 25 | while not file_exists(f): 26 | end = datetime.now() 27 | total_secs = (end-start).total_seconds() 28 | self.assertTrue(total_secs > 0) 29 | 30 | def test_download_url_sync(self): 31 | fname = "some-file.txt" 32 | url = f"https://the-internet.herokuapp.com/download/{fname}" 33 | home_dir = os.getenv('HOME') or os.path.expanduser(os.getenv('USERPROFILE')) 34 | to_dir = os.path.join(home_dir, "tmp") 35 | f = os.path.join(to_dir, fname) 36 | #preset 37 | if file_exists(f): 38 | rm_files(f) 39 | #before 40 | self.assertFalse(file_exists(f)) 41 | download_url(url, to_dir=to_dir, asynch=False, overwrite_existing=True) 42 | self.assertTrue(file_exists(f)) 43 | 44 | def test_webdriver_download(self): 45 | home_dir = os.getenv('HOME') or os.path.expanduser(os.getenv('USERPROFILE')) 46 | to_dir = os.path.join(home_dir, "tmp") 47 | f = os.path.join(to_dir, "chromedriver.exe") 48 | #preset 49 | if file_exists(f): 50 | rm_files(f) 51 | #before 52 | self.assertFalse(file_exists(f)) 53 | start = datetime.now() 54 | end = start 55 | webdriver_downloader.download_chrome(to_dir=to_dir, asynch=True) 56 | #to test asynchronous download 57 | while not file_exists(f): 58 | end = datetime.now() 59 | total_secs = (end-start).total_seconds() 60 | self.assertTrue(total_secs > 0) 61 | 62 | if __name__ == "__main__": 63 | unittest.main() -------------------------------------------------------------------------------- /tests/element_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import sys 3 | sys.path.append("..\\pybrowser") 4 | from pybrowser import Browser 5 | 6 | class ElementTests(unittest.TestCase): 7 | def test_link(self): 8 | bro = Browser(browser_name=Browser.CHROME, headless=True) 9 | bro.goto("https://the-internet.herokuapp.com/").link("xpath:=//*[@id='content']/ul/li[1]/a").click() #phew! 10 | t = bro.element("content").text 11 | self.assertTrue("Also known as split testing" in t) 12 | bro.close() 13 | 14 | def test_highlight(self): 15 | with Browser(browser_name=Browser.FIREFOX, headless=True) as b: 16 | b.goto("https://the-internet.herokuapp.com/login") 17 | b.input("username").highlight() 18 | b.input("password").highlight() 19 | b.button("xpath:=//*[@id='login']/button").highlight() 20 | 21 | if __name__ == "__main__": 22 | unittest.main() -------------------------------------------------------------------------------- /tests/file_tests.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Ranjith' 2 | 3 | import unittest 4 | import os 5 | from datetime import datetime 6 | import time 7 | import sys 8 | sys.path.append("..\\pybrowser") 9 | from pybrowser import Browser 10 | from pybrowser.common_utils import file_exists, rm_files 11 | 12 | class FileTests(unittest.TestCase): 13 | 14 | #TODO - too many, keep tests atomic and simple 15 | def test_file_download_upload(self): 16 | with Browser(browser_name = Browser.CHROME, headless=True) as b: 17 | home_dir = os.getenv('HOME') or os.path.expanduser(os.getenv('USERPROFILE')) 18 | to_dir = os.path.join(home_dir, "tmp") 19 | fl = "some-file.txt" 20 | f = os.path.join(to_dir, fl) 21 | #preset 22 | if file_exists(f): 23 | rm_files(f) 24 | #before 25 | self.assertFalse(file_exists(f)) 26 | start = datetime.now() 27 | end = start 28 | b.goto("https://the-internet.herokuapp.com") 29 | u = b.link("PARTIAL_LINK_TEXT:=Download").url 30 | self.assertTrue(u, "https://the-internet.herokuapp.com/download") 31 | b.link("PARTIAL_LINK_TEXT:=Download").click() 32 | fd = b.file("PARTIAL_LINK_TEXT:=some-file.txt").download(directory=to_dir) 33 | while not file_exists(f): 34 | end = datetime.now() 35 | total_secs = (end-start).total_seconds() 36 | self.assertTrue(total_secs > 0) 37 | #wait for callback to happen 38 | time.sleep(1) 39 | self.assertTrue(len(fd.downloaded_files) > 0) 40 | self.assertTrue(fd.is_download_complete) 41 | b.back() 42 | u2 = b.link("PARTIAL_LINK_TEXT:=Upload").url 43 | self.assertTrue(u2, "https://the-internet.herokuapp.com/upload") 44 | b.link("PARTIAL_LINK_TEXT:=Upload").click() 45 | b.file("file-upload").upload(filename=f) 46 | b.button("file-submit").click() 47 | t = b.element("uploaded-files").text.strip() 48 | self.assertTrue(t, fl) 49 | 50 | if __name__ == "__main__": 51 | unittest.main() -------------------------------------------------------------------------------- /tests/form_tests.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Ranjith' 2 | 3 | import unittest 4 | import sys 5 | sys.path.append("..\\pybrowser") 6 | 7 | from pybrowser import Browser 8 | 9 | class FormTests(unittest.TestCase): 10 | 11 | def test_form(self): 12 | #bro = Browser(Browser.CHROME) 13 | form_data = [("username", "tomsmith"), ("password", "SuperSecretPassword!")] 14 | with Browser(browser_name=Browser.CHROME, headless=True) as b: 15 | b.goto("https://the-internet.herokuapp.com/login") 16 | b.form("login").fill_and_submit_form(form_data) 17 | cs = b.cookies 18 | #print(cs) 19 | self.assertTrue(len(cs) > 0) 20 | self.assertTrue("the-internet.herokuapp.com" in cs[0]['domain'] ) 21 | self.assertEqual(200, b.response_code) 22 | t = b.element("flash").text.strip() 23 | self.assertTrue("You logged into a secure area" in t) 24 | 25 | if __name__ == "__main__": 26 | unittest.main() -------------------------------------------------------------------------------- /tests/html_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import sys 3 | sys.path.append("..\\pybrowser") 4 | import warnings 5 | warnings.simplefilter("ignore") 6 | from pybrowser import Browser 7 | from pybrowser.common_utils import file_exists 8 | 9 | class ElementTests(unittest.TestCase): 10 | 11 | def test_search(self): 12 | with Browser(browser_name=Browser.CHROME, headless=True) as b: 13 | h = b.goto("http://dollarrupee.in/").html() 14 | search_text = "Current USD to INR exchange rate equals {} Rupees per 1 US Dollar" 15 | result = h.search(search_text) 16 | rs = [] 17 | if result: 18 | for r in result: 19 | rs.append(r) 20 | self.assertTrue(len(rs) > 0) 21 | self.assertTrue("" in rs[0]) 22 | 23 | def test_search1(self): 24 | with Browser() as b: 25 | h = b.html(url="http://dollarrupee.in/") 26 | search_text = "Current USD to INR exchange rate equals {} Rupees per 1 US Dollar" 27 | result = h.search(search_text, use_text=True) 28 | rs = [] 29 | if result: 30 | for r in result: 31 | rs.append(r) 32 | self.assertTrue(len(rs) > 0) 33 | 34 | def test_searchall_html(self): 35 | with Browser() as b: 36 | h = b.html(url="http://chromedriver.chromium.org/downloads") 37 | #search_text = "Latest Release: ChromeDriver {} " 38 | search_text = "ChromeDriver {} " 39 | result = h.search_all(search_text, use_text=True) 40 | rs = [] 41 | for r in result: 42 | for d in r: 43 | rs.append(d) 44 | #print(rs) 45 | self.assertTrue(len(rs) > 1) 46 | 47 | def test_links(self): 48 | with Browser() as b: 49 | h = b.html(url="http://google.com") 50 | ls = h.elements.links() 51 | #print(ls) 52 | self.assertTrue(len(ls) > 0) 53 | p = h.save() 54 | self.assertTrue(file_exists(p)) 55 | 56 | def test_elements(self): 57 | dp = "C:\\Users\Ranjith\\pybrowser\\browserdrivers" 58 | with Browser(browser_name=Browser.CHROME, headless=True, driver_path=dp) as b: 59 | h = b.goto("https://the-internet.herokuapp.com/forgot_password").html() 60 | e = h.elements.find_by_id('email') 61 | p = h.save() 62 | self.assertTrue(file_exists(p)) 63 | self.assertEqual(e.type, "text") 64 | self.assertEqual(e.name, "email") 65 | 66 | def test_render(self): 67 | with Browser() as bro: 68 | h = bro.html(url="https://css-tricks.com/ajax-load-container-contents/") 69 | h.render() 70 | e = h.elements.find_by_id('jp-relatedposts') 71 | d = dict(e.attrib) 72 | self.assertEqual(d['id'], "jp-relatedposts") 73 | self.assertEqual(d['class'], "jp-relatedposts") 74 | 75 | def test_render_script(self): 76 | with Browser() as bro: 77 | s = '''() => { 78 | return { 79 | width: document.documentElement.clientWidth, 80 | height: document.documentElement.clientHeight, 81 | deviceScaleFactor: window.devicePixelRatio, 82 | } 83 | }''' 84 | h = bro.html(url="https://the-internet.herokuapp.com/") 85 | r = h.render(script=s) 86 | self.assertTrue(r['width'] > 0) 87 | self.assertTrue(r['height'] > 0) 88 | self.assertTrue(r['deviceScaleFactor'] > 0) 89 | 90 | if __name__ == "__main__": 91 | unittest.main() -------------------------------------------------------------------------------- /tests/listener_tests.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Ranjith' 2 | 3 | import unittest 4 | import sys 5 | sys.path.append("..\\pybrowser") 6 | from selenium.common.exceptions import NoSuchElementException 7 | from pybrowser import Browser 8 | 9 | class ListenerTests(unittest.TestCase): 10 | def test_onexception(self): 11 | with Browser(browser_name=Browser.CHROME, headless=True, wait_time=1) as b: 12 | b.goto("https://the-internet.herokuapp.com/") 13 | #no such element 14 | with self.assertRaises(NoSuchElementException): 15 | b.button("byby").click() 16 | 17 | if __name__ == "__main__": 18 | unittest.main() -------------------------------------------------------------------------------- /tests/requests_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import sys 3 | sys.path.append("..\\pybrowser") 4 | from pybrowser import Browser 5 | import time 6 | import json 7 | 8 | class RequestTests(unittest.TestCase): 9 | def test_get(self): 10 | with Browser() as b: 11 | r = b.get(url="https://api.github.com/users/abranjith/repos") 12 | j = json.loads(r.response.content) 13 | print(j) 14 | self.assertTrue(len(j) > 0) 15 | 16 | def test_get_async(self): 17 | with Browser() as b: 18 | r = b.get(url="https://api.github.com/users/abranjith/repos", asynch=True) 19 | while not r.is_request_done: 20 | print("waiting for get to finish...") 21 | time.sleep(0.5) 22 | j = json.loads(r.content) 23 | self.assertTrue(len(j) > 0) 24 | 25 | if __name__ == "__main__": 26 | unittest.main() -------------------------------------------------------------------------------- /tests/screenshot_tests.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Ranjith' 2 | 3 | import unittest 4 | import os 5 | import sys 6 | sys.path.append("..\\pybrowser") 7 | from pybrowser import Browser 8 | from pybrowser.common_utils import file_exists 9 | 10 | class ElementTests(unittest.TestCase): 11 | 12 | def test_screenshots(self): 13 | home_dir = os.getenv('HOME') or os.path.expanduser(os.getenv('USERPROFILE')) 14 | bro = Browser(browser_name=Browser.CHROME, headless=True) 15 | bro.goto("https://www.google.com/") 16 | v = bro.button("name:=btnK").name 17 | self.assertEqual(v, "btnK") 18 | bro.input("name:=q").enter("sachin") 19 | bro.button("name:=btnK").click() 20 | f = bro.take_screenshot() 21 | self.assertTrue(file_exists(f)) 22 | bro.input("name:=q").enter("virat kohli").submit() 23 | f =bro.take_screenshot(os.path.join(home_dir, "tmp")) 24 | self.assertTrue(file_exists(f)) 25 | bro.input("name:=q").enter("ab de villiers").submit() 26 | f = bro.take_screenshot(os.path.join(home_dir, "tmp")) 27 | self.assertTrue(file_exists(f)) 28 | bro.input("name:=q").enter("dhoni").submit() 29 | f = bro.take_screenshot("googly_dhoni") 30 | self.assertTrue(file_exists(f)) 31 | bro.input("name:=q").enter("ganguly").submit() 32 | f = bro.take_screenshot("googly_ganguly.png") 33 | self.assertTrue(file_exists(f)) 34 | bro.close() 35 | 36 | if __name__ == "__main__": 37 | unittest.main() -------------------------------------------------------------------------------- /tests/test_only.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Ranjith' 2 | 3 | import unittest 4 | from selenium import webdriver 5 | from selenium.webdriver.common.by import By 6 | from selenium.webdriver.common.keys import Keys 7 | from selenium.webdriver.support.ui import WebDriverWait 8 | from selenium.webdriver.support import expected_conditions as EC 9 | from selenium.common.exceptions import * 10 | import sys 11 | import time 12 | sys.path.append("..\\pybrowser") 13 | from pybrowser import Browser 14 | 15 | class ElementTests(unittest.TestCase): 16 | 17 | @unittest.SkipTest 18 | def test1(self): 19 | #driver = webdriver.Chrome() 20 | driver = webdriver.Firefox() 21 | 22 | driver.get("https://www.google.com/") 23 | 24 | srch_box = WebDriverWait(driver, 5, 1, (ElementNotVisibleException)).until( 25 | EC.presence_of_element_located((By.NAME, "q")) 26 | ) 27 | srch_box.send_keys("sachin") 28 | srch_box.send_keys(Keys.ESCAPE) 29 | 30 | srch_btn = WebDriverWait(driver, 5, 1, (ElementNotVisibleException)).until( 31 | EC.presence_of_element_located((By.NAME, "btnK")) 32 | ) 33 | srch_btn.submit() 34 | # srch_btn.click() 35 | time.sleep(10) 36 | driver.close 37 | driver.quit() 38 | 39 | @unittest.SkipTest 40 | def test2(self): 41 | for k, v in sys.modules.items(): 42 | print(k,"===",v) 43 | 44 | 45 | @unittest.SkipTest 46 | def test3(self): 47 | driver1 = webdriver.Chrome() 48 | driver2 = webdriver.Chrome() 49 | 50 | @unittest.SkipTest 51 | def test4(self): 52 | b = Browser() 53 | driver = b.driver 54 | driver.get("https://the-internet.herokuapp.com/") 55 | ba = WebDriverWait(driver, 5, 1, (ElementNotVisibleException)).until( 56 | EC.presence_of_element_located((By.XPATH, "//*[@id='content']/ul/li[2]/a")) 57 | ) 58 | ba.click() 59 | h = driver.window_handles 60 | for i in h: 61 | print(i) 62 | b.close() 63 | 64 | def test5(self): 65 | with Browser(browser_name=Browser.CHROME, headless=True) as b: 66 | b.goto("https://www.google.com/") 67 | b.input("name:=q").enter("india news") 68 | b.button("name:=btnK").click() 69 | p = b.take_screenshot() 70 | print("path ", p) 71 | t = b.html().elements.links(url_only=True, images=False) 72 | for a in t: 73 | print(a) 74 | 75 | if __name__ == "__main__": 76 | unittest.main() 77 | --------------------------------------------------------------------------------