From 2645506fc8c5b6aa3fa0ff0e921cd9f259b0d702 Mon Sep 17 00:00:00 2001 From: Maxim Stewart Date: Fri, 10 Apr 2020 17:41:42 -0500 Subject: [PATCH] inital push --- README.md | 28 ++++++++++++ requirements.txt | 5 ++ src/__init__.py | 36 +++++++++++++++ src/__main__.py | 23 ++++++++++ src/core/Context.py | 25 ++++++++++ src/core/__init__.py | 5 ++ src/core/mixins/ControlerMixin.py | 76 +++++++++++++++++++++++++++++++ src/core/mixins/__init__.py | 1 + src/core/utils/Browser.py | 37 +++++++++++++++ src/core/utils/Logger.py | 52 +++++++++++++++++++++ src/core/utils/__init__.py | 2 + 11 files changed, 290 insertions(+) create mode 100644 README.md create mode 100644 requirements.txt create mode 100644 src/__init__.py create mode 100644 src/__main__.py create mode 100644 src/core/Context.py create mode 100644 src/core/__init__.py create mode 100644 src/core/mixins/ControlerMixin.py create mode 100644 src/core/mixins/__init__.py create mode 100644 src/core/utils/Browser.py create mode 100644 src/core/utils/Logger.py create mode 100644 src/core/utils/__init__.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..f0b822f --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# Selenium Automation Template +A template to get you setup to pass a command file and run commands. + +# Note(s) +This is designed with using Python mixins to inherit functionality from discrete classes that have discrete methods. The only "global" variable should be in the Context class. + +!Important! BUILD OUT THE LOGIC FIRST AS THIS IS JUST TO GET STARTED! + + +# Dependencies +``` sudo apt install python3 python-pip libgirepository1.0-dev ``` + + +# Setup + +*** Change directory to the src. + +``` python3 -m venv ./venv ``` + +``` source venv/bin/activate ``` + +``` pip install -r requirements.txt ``` + +``` python . ``` + +... when done ... + +``` deactivate ``` diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ef97744 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +beautifulsoup4==4.9.0 +pkg-resources==0.0.0 +selenium==3.141.0 +soupsieve==2.0 +urllib3==1.25.8 diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..0b2493e --- /dev/null +++ b/src/__init__.py @@ -0,0 +1,36 @@ +#!/usr/bin/python3 + +# Python imports +import sys, os, json + + +# Application imports +from core import Context + + +class Main(Context): + def __init__(self, args): + super().__init__(args) + + try: + + with open(args.file) as f: + self.logger.debug("Opened command file...") + # Fill out your logic for parsing a command file... + # Then call the "call_method" methid to run a command against that logic. + pass + + if "true" in args.persist.lower(): + input("Press 'Enter' key to close the browser...") + + except Exception as e: + self.logger.debug(e, exec_info=True) + + self.driver.quit() + sys.exit(0) + + + def call_method(self, method_name, data = None): + mName = str(method_name) + method = getattr(self, mName, lambda data: "No valid key passed...\nkey= " + mName + "\nargs= " + data) + return method(data) if data else method() diff --git a/src/__main__.py b/src/__main__.py new file mode 100644 index 0000000..26d3978 --- /dev/null +++ b/src/__main__.py @@ -0,0 +1,23 @@ + +# Python imports +import argparse + +# Application imports +from __init__ import Main + + +if __name__ == "__main__": + try: + parser = argparse.ArgumentParser() + # Add long and short arguments + parser.add_argument("--file", "-f", help="The instructions file to use if any.") + parser.add_argument("--browser", "-b", default="firefox", help="ie, chrome, firefox (Optional)") + parser.add_argument("--headless", "-hm", default=False, help="Run browser in headless mode.") + parser.add_argument("--persist", "-p", default=False, help="Keep browser open after run. (Optional)") + + + # Read arguments (If any...) + args = parser.parse_args() + main = Main(args) + except Exception as e: + print( repr(e) ) diff --git a/src/core/Context.py b/src/core/Context.py new file mode 100644 index 0000000..1a1fa2c --- /dev/null +++ b/src/core/Context.py @@ -0,0 +1,25 @@ +# Python imports + + +# Lib imports + +# Application imports +from .utils import Logger, Browser +from .mixins import ControlerMixin + + +class Context(ControlerMixin): + """ + The Context class consumes mixins to add functionality as needed. + """ + def __init__(self, args): + """ + Construct a new 'Context' object which pulls in mixins. + :param args: The terminal passed arguments + + :return: returns nothing + """ + self.logger = Logger().get_logger("MAIN") + browser = Browser() + self.driver = browser.get_browser(args.browser, args.headless) # The browser driver + self.url = "" # The url we are pointing to diff --git a/src/core/__init__.py b/src/core/__init__.py new file mode 100644 index 0000000..065d76b --- /dev/null +++ b/src/core/__init__.py @@ -0,0 +1,5 @@ +from .mixins import ControlerMixin + +from .utils import Logger +from .utils import Browser +from .Context import Context diff --git a/src/core/mixins/ControlerMixin.py b/src/core/mixins/ControlerMixin.py new file mode 100644 index 0000000..a2b1d16 --- /dev/null +++ b/src/core/mixins/ControlerMixin.py @@ -0,0 +1,76 @@ +# Python imports +import os + +# Lib imports +from selenium.webdriver.common.keys import Keys + +# Application imports + + +class ControlerMixin: + """ + The ControlerMixin has methods to manage interaction with the browser. + These get called from the _init__.Main class and ran. + """ + + def getImage(self): + """ + Get image of page we are on. + + :return: no return data + """ + folder = self.domain + if not os.path.exists(folder): + os.mkdir(folder) + + i = 0 + name = folder + "_" + str(i) + ".png" + toFile = folder + "/" + fName + while os.path.exists(file): + i += 1 + name = folder + "__" + i + ".png" + toFile = folder + "/" + fName + + self.logger.debug("Screenshot File Path/Name: " + toFile) + self.driver.save_screenshot(toFile) + + + def createXPath(self, data): + """ + Don't call directly. + + :return: created xpath string + """ + + keys = data.keys() + attribList = [] + xpathStr = "" + queryCount = 0 + + if "elm" in keys: + xpathStr = "//" + data["elm"] + "[" + queryCount += 1 + else: + xpathStr = "//" + data["elm"] + "[" + + if "id" in keys: + attribList.append("@id='" + data["id"] + "'") + queryCount += 1 + if "class" in keys: + attribList.append("@class='" + data["class"] + "'") + queryCount += 1 + if "type" in keys: + attribList.append("@type='" + data["type"] + "'") + queryCount += 1 + if "value" in keys: + attribList.append("@value='" + data["value"] + "'") + queryCount += 1 + + if len(attribList) > 1: + xpathStr += " and ".join(attribList) + else: + xpathStr += attribList[0] + + xpathStr += "]" + self.logger.debug("Generated XPath: " + xpathStr) + return xpathStr diff --git a/src/core/mixins/__init__.py b/src/core/mixins/__init__.py new file mode 100644 index 0000000..be71ddc --- /dev/null +++ b/src/core/mixins/__init__.py @@ -0,0 +1 @@ +from .ControlerMixin import ControlerMixin diff --git a/src/core/utils/Browser.py b/src/core/utils/Browser.py new file mode 100644 index 0000000..600384b --- /dev/null +++ b/src/core/utils/Browser.py @@ -0,0 +1,37 @@ +# Python imports + +# Lib imports +from selenium import webdriver +from selenium.webdriver.firefox.options import Options as FOptions + + +# Application imports + + +class Browser: + """ + The Browser allows us to bring in selenium driver (a.k.a browser) related objects + """ + def get_browser(self, browserType = None, headless = None): + """ + Construct new selenium driver (a.k.a browser object) + Sets the "self.driver" in Context object. + :note: Should consider creating methods per browser type. + :param browserType: The browser we want to use + :param headless: If we have a gui or not + """ + driver = None + _log_path = "./core/logs/webdriver.log" + + if "firefox" in browserType: + _options = FOptions() + profile = webdriver.FirefoxProfile() + + profile.accept_untrusted_certs = True + if headless: + _options.add_aregument("--headless") + + driver = webdriver.Firefox(options=_options, firefox_profile=profile, log_path=_log_path) + + + return driver diff --git a/src/core/utils/Logger.py b/src/core/utils/Logger.py new file mode 100644 index 0000000..e385c53 --- /dev/null +++ b/src/core/utils/Logger.py @@ -0,0 +1,52 @@ +# Python imports +import os, logging + +# Application imports + + +class Logger: + def __init__(self): + pass + + def get_logger(self, loggerName = "NO_LOGGER_NAME_PASSED", createFile = True): + """ + Create a new logging object and return it. + :note: + NOSET # Don't know the actual log level of this... (defaulting or litterally none?) + Log Levels (From least to most) + Type Value + CRITICAL 50 + ERROR 40 + WARNING 30 + INFO 20 + DEBUG 10 + :param loggerName: Sets the name of the logger object. (Used in log lines) + :param createFile: Whether we create a log file or just pump to terminal + + :return: returns the logging object we created + """ + + globalLogLvl = logging.DEBUG # Keep this at highest so that handlers can filter to their desired levels + chLogLevel = logging.CRITICAL # Prety musch the only one we change ever + fhLogLevel = logging.DEBUG + log = logging.getLogger(loggerName) + + # Set our log output styles + fFormatter = logging.Formatter('[%(asctime)s] %(pathname)s:%(lineno)d %(levelname)s - %(message)s', '%m-%d %H:%M:%S') + cFormatter = logging.Formatter('%(pathname)s:%(lineno)d] %(levelname)s - %(message)s') + + ch = logging.StreamHandler() + ch.setLevel(level=chLogLevel) + ch.setFormatter(cFormatter) + log.addHandler(ch) + + if createFile: + folder = "core/logs" + file = folder + "/twitter-bot.log" + + fh = logging.FileHandler(file) + fh.setLevel(level=fhLogLevel) + fh.setFormatter(fFormatter) + log.addHandler(fh) + + return log diff --git a/src/core/utils/__init__.py b/src/core/utils/__init__.py new file mode 100644 index 0000000..cf7de47 --- /dev/null +++ b/src/core/utils/__init__.py @@ -0,0 +1,2 @@ +from .Logger import Logger +from .Browser import Browser