inital push

This commit is contained in:
Maxim Stewart 2020-04-10 17:41:42 -05:00
parent dc15d4956b
commit 2645506fc8
11 changed files with 290 additions and 0 deletions

28
README.md Normal file
View File

@ -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 . <your command file> ```
... when done ...
``` deactivate ```

5
requirements.txt Normal file
View File

@ -0,0 +1,5 @@
beautifulsoup4==4.9.0
pkg-resources==0.0.0
selenium==3.141.0
soupsieve==2.0
urllib3==1.25.8

36
src/__init__.py Normal file
View File

@ -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()

23
src/__main__.py Normal file
View File

@ -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) )

25
src/core/Context.py Normal file
View File

@ -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

5
src/core/__init__.py Normal file
View File

@ -0,0 +1,5 @@
from .mixins import ControlerMixin
from .utils import Logger
from .utils import Browser
from .Context import Context

View File

@ -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

View File

@ -0,0 +1 @@
from .ControlerMixin import ControlerMixin

37
src/core/utils/Browser.py Normal file
View File

@ -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

52
src/core/utils/Logger.py Normal file
View File

@ -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

View File

@ -0,0 +1,2 @@
from .Logger import Logger
from .Browser import Browser