From 1af5236b957107bd4e0b26fd39ccde2a31f60749 Mon Sep 17 00:00:00 2001 From: kirbylife Date: Mon, 4 Nov 2019 18:00:31 -0600 Subject: [PATCH] initial commit --- .gitignore | 129 ++++++++++++++++++++++++++++ iridium/__init__.py | 1 + iridium/iridium.py | 205 ++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 17 ++++ 4 files changed, 352 insertions(+) create mode 100644 .gitignore create mode 100644 iridium/__init__.py create mode 100644 iridium/iridium.py create mode 100644 pyproject.toml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..267ff05 --- /dev/null +++ b/.gitignore @@ -0,0 +1,129 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# Poetry Stuff +poetry.lock +*.egg-info/ \ No newline at end of file diff --git a/iridium/__init__.py b/iridium/__init__.py new file mode 100644 index 0000000..89e6352 --- /dev/null +++ b/iridium/__init__.py @@ -0,0 +1 @@ +from iridium import iridium diff --git a/iridium/iridium.py b/iridium/iridium.py new file mode 100644 index 0000000..fba8f33 --- /dev/null +++ b/iridium/iridium.py @@ -0,0 +1,205 @@ +try: + from subprocess import getoutput +except ModuleNotFoundError: + from commands import getoutput +from datetime import datetime +from os import system as run +from time import sleep +from uuid import uuid4 + +import clipboard +from requests_html import HTML +from xdo import Xdo + + +class Iridium: + def __init__(self, browser, *args, **kwargs): + self.xdo = Xdo() + start_url = kwargs.get("start_url", "about:blank") + private = kwargs.get("private") + self.browser_name = browser.split("/")[-1] + cmd = [browser] + if private: + if "FIREFOX" in self.browser_name.upper(): + cmd.append("--private-window") + else: + cmd.append("-incognito") + cmd = cmd + list(args) + ["&>/dev/null &"] + cmd = " ".join(cmd) + prev = self.__search(self.browser_name, sync=False) + run(cmd) + for _ in range(60): + sleep(1) + now = self.__search(self.browser_name) + ids = set(now) - set(prev) + if ids: + break + if len(ids) == 1: + self.id = list(ids)[0] + else: + self.ids = ids + self.id = None + sleep(2) + self.__key("Escape") + sleep(1) + if start_url: + self.__type(start_url) + self.__key("Return") + else: + self.__key("Return") + sleep(1) + + def __search(self, arg, type="class", sync=True): + cmd = "xdotool search {} --onlyvisible --{type} {arg}" + sync = "--sync" if sync else "" + out = getoutput(cmd.format(sync, type=type, arg=arg)) + out = out.split("\n") + return out + + def __focus(self): + if not self.is_running(): + raise Exception("{} is down".format(self.browser_name)) + run("xdotool windowactivate {}".format(self.id)) + + focus = __focus + + def __key(self, key, delay=20): + self.__focus() + run("xdotool key --delay {} {}".format(delay, key)) + + key = __key + + def __type(self, text, delay=30, old=False): + self.__focus() + sleep(0.1) + if old: + text = text.replace('"', r'\"') + run('xdotool type --delay {} "{}"'.format(delay, text)) + else: + temp = clipboard.paste() + clipboard.copy(text) + self.__key("Ctrl+v") + clipboard.copy(temp) + + _type = __type + + def is_running(self, counter=1): + processes = set(self.__search(self.browser_name)) + if not self.id: + remain = self.ids - processes + if len(remain) == 0: + return True + else: + self.id = list(self.ids - remain)[0] + del self.ids + result = self.id in processes + if not result and counter > 0: + sleep(1) + result = self.is_running(counter=counter - 1) + return result + + def get(self, url): + sleep(2) + self.__key("F6") + self.__type(url) + self.__key("Return") + + def refresh(self, force=False, cut_params=True): + self.__focus() + if force: + self.execute_script("window.location.href = window.location.href" + + (".split('?')[0];" if cut_params else ";")) + else: + self.__key("F5") + + def quit(self): + run("xdotool windowkill {}".format(self.id)) + + @property + def page_source(self): + self.__focus() + id_html = str(datetime.now()).replace(" ", "_") + self.__key("Ctrl+s") + sleep(2) + self.__key("Alt+n") + self.__type("/tmp/{}".format(id_html)) + self.__key("Return") + sleep(10) + try: + html = open("/tmp/{}.html".format(id_html), "rb") + except: + html = open("/tmp/{}.htm".format(id_html), "rb") + try: + html.seek(0) + out = html.read().decode("utf-8") + except UnicodeDecodeError: + html.seek(0) + out = html.read().decode("latin") + html.close() + run("rm -R /tmp/{}*".format(id_html)) + return out + + def execute_script(self, cmd): + self.__focus() + cmd = cmd.splitlines() + if "FIREFOX" in self.browser_name.upper(): + self.__key("Ctrl+Shift+k") + else: + self.__key("Ctrl+Shift+j") + sleep(4) + for line in cmd: + if not line.strip(): + continue + self.__type(line) + sleep(0.2) + self.__key("Return") + sleep(2) + self.__key("Ctrl+Shift+i") + + def get_element(self, css: str): + return IridiumNode(self, css) + + def click(self, css: str): + script = f'''document.querySelector('{css}').click()''' + self.execute_script(script) + + @property + def title(self): + return self.get_element("title").text() + + +class IridiumNode: + def __init__(self, driver: Iridium, selector: str): + self.driver = driver + self.uid = str(uuid4()) + driver.execute_script(f''' +if(document.querySelector('{selector}')) + document.querySelector('{selector}').setAttribute("iridium_id", "{self.uid}"); + ''') + self.xpath_selector = f'//*[@iridium_id="{self.uid}"]' + self.css_selector = f'[iridium_id="{self.uid}"]' + + def exists(self): + text = self.driver.page_source + html = HTML(html=text) + return bool(html.xpath(self.xpath_selector)) + + def click(self): + assert self.exists() + self.driver.click(self.css_selector) + + def text(self): + assert self.exists() + text = self.driver.page_source + html = HTML(html=text) + value = html.xpath(self.xpath_selector + "/text()", first=True) + return value + + def get_attribute(self, attr): + assert self.exists() + text = self.driver.page_source + html = HTML(html=text) + value = html.xpath(self.xpath_selector + "/@" + attr, first=True) + return value + + getAttribute = get_attribute diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d9b0d8c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,17 @@ +[tool.poetry] +name = "iridium" +version = "0.1.0" +description = "A diferent way to control a headless browser diferent to selenium" +authors = ["kirbylife "] + +[tool.poetry.dependencies] +python = "^3.6" +requests-html = "^0.10.0" +clipboard = "^0.0.4" +python-libxdo = { git = "https://github.com/rshk/python-libxdo.git" } + +[tool.poetry.dev-dependencies] + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api"