diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..4b41c8b --- /dev/null +++ b/.coveragerc @@ -0,0 +1,6 @@ +[report] +show_missing = True +skip_covered = True +omit = + tests/* + requirements.py diff --git a/.deepsource.toml b/.deepsource.toml index 25bc3d7..3ea082b 100644 --- a/.deepsource.toml +++ b/.deepsource.toml @@ -1,5 +1,7 @@ version = 1 +test_patterns = ["*/tests/**"] + [[analyzers]] name = "python" enabled = true diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 61a6703..787034b 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -5,13 +5,17 @@ name: CI Build on: workflow_dispatch: + schedule: + # Runs at 12am IST + - cron: '30 18 * * *' jobs: build: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} strategy: matrix: + os: [ubuntu-latest, macOS-latest] python-version: [3.8] steps: @@ -20,24 +24,36 @@ jobs: uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} + - name: Install Linux dependencies + if: runner.os == 'Linux' + run: | + wget -qO- https://deb.opera.com/archive.key | sudo apt-key add - + sudo add-apt-repository "deb [arch=i386,amd64] https://deb.opera.com/opera-stable/ stable non-free" + sudo apt-get update + sudo apt-get -y --no-install-recommends install opera-stable chromium-browser + opera --version + - name: Install Macos dependencies + if: startsWith(runner.os, 'macOS') + run: | + brew cask install chromium opera - name: Install dependencies run: | python -m pip install --upgrade pip - pip install flake8 pytest - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + pip install poetry flake8 + poetry install - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - name: Install Chrome and Firefox + - name: Run unittests run: | - sudo apt install google-chrome-stable - sudo apt-get install firefox + poetry run pytest - name: Make sure chrome crawler is working (Attempt to subscribe to 1 course) env: UDEMY_EMAIL: ${{ secrets.UDEMY_EMAIL }} UDEMY_PASSWORD: ${{ secrets.UDEMY_PASSWORD }} + CI_TEST: "True" run: | - python udemy_enroller_chrome.py \ No newline at end of file + poetry run python udemy_enroller_chrome.py diff --git a/.gitignore b/.gitignore index 578270b..f9925bc 100644 --- a/.gitignore +++ b/.gitignore @@ -231,3 +231,12 @@ poetry.lock # Cache files .course_cache + +# Ignore temporary test folder +test_tmp/ + +.idea/poetry.xml + +#ignore deepsource.toml + +.deepsource.toml \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a33d0a..baed986 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.0] - 2020-12-09 + +### Added + +- Fix for state selection for India +- Added deprecation warning to individual browser endpoint to move to uniform endpoint in preparation for a release in PyPi +- Moved from print to logging for better debug +- Added arguments to script runtime to select max pages and max retry before timeout (default 12) +- Fixed Firefox webdriver autoinstall +- Stop trying to enroll in courses that has any price tag attached to them +- Added unittests +- General performance improvement ## [0.3] - 2020-11-26 diff --git a/README.md b/README.md index 529f716..2b153c5 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,12 @@ Do you want to LEARN NEW STUFF for FREE? Don't worry, with the power of web-scraping and automation, this script will find the necessary Udemy Coupons -& enroll you for PAID UDEMY COURSES, ABSOLUTELY FREE! +& enroll you to PAID UDEMY COURSES, ABSOLUTELY FREE! The code scrapes course links and coupons from [tutorialbar.com](https://tutorialbar.com) -In case of any bugs or issues, **feel free to ping me on -[LinkedIn](https://www.linkedin.com/in/aapatre/) or -[Twitter](https://twitter.com/Antariksh_Patre)** +In case of any bugs or issues, please open an issue in github. Also, don't forget to **Fork & Star the repository if you like it!** @@ -25,13 +23,11 @@ Also, don't forget to **Fork & Star the repository if you like it!** ## **_Disclaimer & WARNINGS:_** -1. **IMPORTANT:** Make sure you **clear all saved Debit/Credit Card or any other - saved payment info from your Browser & your Udemy account** before using the - script! -2. **Use** this ONLY for **Educational Purposes!** By using this code you agree + +1. **Use** this ONLY for **Educational Purposes!** By using this code you agree that **I'm not responsible for any kind of trouble** caused by the code. -3. **Make sure web-scraping is legal in your region.** -4. This is **NOT a hacking script**, i.e., it can't enroll you for a specific +2. **Make sure web-scraping is legal in your region.** +3. This is **NOT a hacking script**, i.e., it can't enroll you for a specific course! Instead it finds courses that provide coupon links to make the transaction free and then LEGALLY enroll you to the course! @@ -43,11 +39,11 @@ Also, don't forget to **Fork & Star the repository if you like it!** **Required Python version:** [Python 3.8+](https://www.python.org/downloads/) -**You must have pip installed. Please look up how to install pip in your OS.** +**You must have pip or poetry installed. Please look up how to install them in your OS.** Download a release of this project or clone the repository then navigate to the folder where you placed the files on. Type `pip install -r requirements.txt` to -get all the requirements installed in one go. +get all the requirements installed in one go. Similar instructions applies for poetry. - **Webdrivers are now automatically installed! But here are some links in case you are using the vanilla script or the Safari Browser:** @@ -71,7 +67,7 @@ get all the requirements installed in one go. - Run the script and the cli will guide you through the settings required - Otherwise you can rename the following file - [sample_settings.yaml](sample_settings.yaml) to **settings.py** and edit it + [sample_settings.yaml](sample_settings.yaml) to **settings.yaml** and edit it using a text editor and insert your **Udemy registered email in the email section**, your **Udemy password in the password section**, and the **ZIP Code in the zipcode section (if you reside in the United States or any other region @@ -92,7 +88,7 @@ get all the requirements installed in one go. - **Has issues when run on custom kernel but works fine on vanilla OS:** - Firefox: - [udemy_enroller_firefox.py(requires manual driver installation)](https://github.com/aapatre/Automatic-Udemy-Course-Enroller-GET-PAID-UDEMY-COURSES-for-FREE/blob/master/udemy_enroller_firefox.py) + [udemy_enroller_firefox.py(might require manual driver installation)](https://github.com/aapatre/Automatic-Udemy-Course-Enroller-GET-PAID-UDEMY-COURSES-for-FREE/blob/master/udemy_enroller_firefox.py) - **Untested:** @@ -108,10 +104,19 @@ get all the requirements installed in one go. - Internet Explorer: [udemy_enroller_internet_explorer.py](https://github.com/aapatre/Automatic-Udemy-Course-Enroller-GET-PAID-UDEMY-COURSES-for-FREE/blob/master/udemy_enroller_internet_explorer.py) -3 . Run the chosen script in terminal like so: -`python udemy_enroller_firefox.py` +3 . The script can be passed arguments: +- `--help`: View full list of arguments available +- `--max-pages=`: Max number of pages to scrape from tutorialbar.com before exiting the script +- `--browser=`: Run with a specific browser +- `--cache-hits=`: If we hit the cache this number of times in a row we will exit the script + +4 . Run the chosen script in terminal like so: +- `python udemy_enroller_firefox.py` -4 . The bot starts scraping the course links from the first **All Courses** page + Or by using the generic script: +- `python udemy_enroller.py --browser=firefox` + +5 . The bot starts scraping the course links from the first **All Courses** page on [Tutorial Bar](https://www.tutorialbar.com/all-courses/page/1) and starts enrolling you to Udemy courses. After it has enrolled you to courses from the first page, it then moves to the next Tutorial Bar page and the cycle continues. @@ -142,9 +147,7 @@ to make those courses free. ### 3. How frequently should you run the script? -Daily, at least once! If you are using it for the first time, I recommend that -you allow it to scrape through all pages on Tutorial Bar (might take a few hours -since there are >500 pages on the site). I've painstakingly amassed over 4000 +Daily, at least once! I've painstakingly amassed over 4000 courses in the last four years! And out of those 4000, I've only paid for 4 of these courses. @@ -164,7 +167,7 @@ it will save your precious time too! :) ![](https://i.imgur.com/pwseilE.jpg) Relax! This happens when you run the script several times in a short interval of time. Solve the captcha, hit enter in the terminal window you are running the script from and allow the script to continue as normal. -Easy peasy lemon squeezy! 🍋🙃

+Easy peasy lemon squeezy! 🍋🙃 ### 6. The code compiles successfully but it's taking too long to work! IS there any way to fix that? @@ -175,10 +178,8 @@ retrieved in the Python console/shell, which may take a while. ### 7. Which is the best way to run the script? -It is recommended to run the script using Python's IDLE IDE. +It is recommended to run the script using your terminal and system python. -**Pro-tip:** Create a batch file, to launch the script instantly, using these -instructions: https://datatofish.com/batch-python-script/ ### 8. Which branch to commit against? @@ -194,6 +195,14 @@ and help us on what you want or talk to us about your proposed changes. ## Supporter +### Jetbrains + [![JetBrains](https://i.imgur.com/h2R018M.jpg)](https://jetbrains.com/?from=udemy-free-course-enroller) Thanks to [JetBrains](https://jetbrains.com/?from=udemy-free-course-enroller) for supporting us. They are the maker of world class IDE and developer tooling. If you think their product might help you, please support them. + +### GitBook + +[![Gitbook](https://i.imgur.com/OkuB14I.jpg)](https://gitbook.com) + +Thanks to [Gitbook](https://gitbook.com) for supporting us. Gitbook is the best place to track personal notes and ideas for teams. If you think their product might help you, please support them. \ No newline at end of file diff --git a/core/__init__.py b/core/__init__.py index 751a77d..cb59e9a 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -1,3 +1,9 @@ +import logging.config + +from .cache import CourseCache +from .driver_manager import ALL_VALID_BROWSER_STRINGS, DriverManager from .settings import Settings from .tutorialbar import TutorialBarScraper from .udemy import UdemyActions + +logging.config.fileConfig("logconfig.ini", disable_existing_loggers=False) diff --git a/core/cache.py b/core/cache.py index 492c47f..c3683e0 100644 --- a/core/cache.py +++ b/core/cache.py @@ -3,13 +3,13 @@ import os -class CourseCache(object): +class CourseCache: """ Basic cache to keep details on courses already scraped """ - def __init__(self): - self._file_name = ".course_cache" + def __init__(self, file_name=".course_cache"): + self._file_name = file_name self._cache = [] self._load_cache() diff --git a/core/driver_manager.py b/core/driver_manager.py new file mode 100644 index 0000000..b2b7506 --- /dev/null +++ b/core/driver_manager.py @@ -0,0 +1,92 @@ +import logging + +from selenium import webdriver +from selenium.webdriver.chrome.options import Options as ChromeOptions +from webdriver_manager.chrome import ChromeDriverManager +from webdriver_manager.firefox import GeckoDriverManager +from webdriver_manager.microsoft import EdgeChromiumDriverManager, IEDriverManager +from webdriver_manager.opera import OperaDriverManager +from webdriver_manager.utils import ChromeType + +VALID_FIREFOX_STRINGS = {"ff", "firefox"} +VALID_CHROME_STRINGS = {"chrome", "google-chrome"} +VALID_CHROMIUM_STRINGS = {"chromium"} +VALID_INTERNET_EXPLORER_STRINGS = {"internet_explorer", "ie"} +VALID_OPERA_STRINGS = {"opera"} +VALID_EDGE_STRINGS = {"edge"} + +ALL_VALID_BROWSER_STRINGS = ( + VALID_FIREFOX_STRINGS.union(VALID_CHROME_STRINGS) + .union(VALID_CHROMIUM_STRINGS) + .union(VALID_CHROMIUM_STRINGS) + .union(VALID_INTERNET_EXPLORER_STRINGS) + .union(VALID_OPERA_STRINGS) + .union(VALID_EDGE_STRINGS) +) + + +logger = logging.getLogger("udemy_enroller") + + +class DriverManager: + def __init__(self, browser: str, is_ci_build: bool = False): + self.driver = None + self.options = None + self.browser = browser + self.is_ci_build = is_ci_build + self._init_driver() + + def _init_driver(self): + """ + Initialize the correct web driver based on the users requested browser + + :return: None + """ + + if self.browser.lower() in VALID_CHROME_STRINGS: + if self.is_ci_build: + self.options = self._build_ci_options_chrome() + self.driver = webdriver.Chrome( + ChromeDriverManager().install(), options=self.options + ) + elif self.browser.lower() in VALID_CHROMIUM_STRINGS: + self.driver = webdriver.Chrome( + ChromeDriverManager(chrome_type=ChromeType.CHROMIUM).install() + ) + elif self.browser.lower() in VALID_EDGE_STRINGS: + self.driver = webdriver.Edge(EdgeChromiumDriverManager().install()) + elif self.browser.lower() in VALID_FIREFOX_STRINGS: + self.driver = webdriver.Firefox( + executable_path=GeckoDriverManager().install() + ) + elif self.browser.lower() in VALID_OPERA_STRINGS: + self.driver = webdriver.Opera( + executable_path=OperaDriverManager().install() + ) + elif self.browser.lower() in VALID_INTERNET_EXPLORER_STRINGS: + self.driver = webdriver.Ie(IEDriverManager().install()) + else: + raise ValueError("No matching browser found") + + # Maximize the browser + self.driver.maximize_window() + + @staticmethod + def _build_ci_options_chrome(): + """ + Build chrome options required to run in CI + + :return: + """ + # Having the user-agent with Headless param was always leading to robot check + user_agent = ( + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 " + "Safari/537.36" + ) + options = ChromeOptions() + # We need to run headless when using github CI + options.add_argument("--headless") + options.add_argument("user-agent={0}".format(user_agent)) + options.add_argument("--window-size=1325x744") + logger.info("This is a CI run") + return options diff --git a/core/settings.py b/core/settings.py index ce875da..a3bcf9b 100644 --- a/core/settings.py +++ b/core/settings.py @@ -1,25 +1,28 @@ import getpass +import logging import os.path from distutils.util import strtobool from typing import Dict, List from ruamel.yaml import YAML, dump +logger = logging.getLogger("udemy_enroller") + class Settings: """ Contains all logic related to the scripts settings """ - def __init__(self): + def __init__(self, settings_path="settings.yaml"): self.email = None self.password = None self.zip_code = None self.languages = [] self.categories = [] - self._settings_path = "settings.yaml" - self.is_ci_build = strtobool(os.environ.get("CI", "False")) + self._settings_path = settings_path + self.is_ci_build = strtobool(os.environ.get("CI_TEST", "False")) self._init_settings() def _init_settings(self) -> None: @@ -42,7 +45,7 @@ def _load_ci_settings(self): :return: """ - print("Loading CI settings") + logger.info("Loading CI settings") self.email = os.environ["UDEMY_EMAIL"] self.password = os.environ["UDEMY_PASSWORD"] @@ -56,7 +59,7 @@ def _load_user_settings(self) -> Dict: settings = None if os.path.isfile(self._settings_path): - print("Loading existing settings") + logger.info("Loading existing settings") with open(self._settings_path) as f: settings = yaml.load(f) udemy_settings = settings["udemy"] @@ -88,7 +91,7 @@ def _get_email(self) -> str: """ email = input("Please enter your udemy email address: ") if len(email) == 0: - print("You must provide your email") + logger.warning("You must provide your email") return self._get_email() return email @@ -100,7 +103,7 @@ def _get_password(self) -> str: """ password = getpass.getpass(prompt="Please enter your udemy password: ") if len(password) == 0: - print("You must provide your password") + logger.warning("You must provide your password") return self._get_password() return password @@ -148,7 +151,7 @@ def _save_settings(self) -> None: :return: """ - yaml_structure = dict() + yaml_structure = {} save_settings = input("Do you want to save settings for future use (Y/N): ") if save_settings.lower() == "y": yaml_structure["udemy"] = { @@ -161,6 +164,6 @@ def _save_settings(self) -> None: with open(self._settings_path, "w+") as f: dump(yaml_structure, stream=f) - print(f"Saved your settings in {self._settings_path}") + logger.info(f"Saved your settings in {self._settings_path}") else: - print("Not saving your settings as requested") + logger.info("Not saving your settings as requested") diff --git a/core/tutorialbar.py b/core/tutorialbar.py index c8e4fd9..9eea724 100644 --- a/core/tutorialbar.py +++ b/core/tutorialbar.py @@ -1,9 +1,12 @@ +import logging from multiprocessing.dummy import Pool from typing import List import requests from bs4 import BeautifulSoup +logger = logging.getLogger("udemy_enroller") + class TutorialBarScraper: """ @@ -13,10 +16,11 @@ class TutorialBarScraper: DOMAIN = "https://www.tutorialbar.com" AD_DOMAINS = ("https://amzn",) - def __init__(self): + def __init__(self, max_pages=None): self.current_page = 0 self.last_page = None self.links_per_page = 12 + self.max_pages = max_pages def run(self) -> List: """ @@ -25,20 +29,36 @@ def run(self) -> List: :return: list of udemy coupon links """ self.current_page += 1 - print("Please Wait: Getting the course list from tutorialbar.com...") + logger.info("Please Wait: Getting the course list from tutorialbar.com...") course_links = self.get_course_links( f"{self.DOMAIN}/all-courses/page/{self.current_page}/" ) - print(f"Page: {self.current_page} of {self.last_page} scraped") + logger.info(f"Page: {self.current_page} of {self.last_page} scraped") udemy_links = self.gather_udemy_course_links(course_links) filtered_udemy_links = self._filter_ad_domains(udemy_links) for counter, course in enumerate(filtered_udemy_links): - print(f"Received Link {counter + 1} : {course}") + logger.info(f"Received Link {counter + 1} : {course}") return filtered_udemy_links + def script_should_run(self) -> bool: + """ + Returns boolean of whether or not we should continue checking tutorialbar.com + + :return: + """ + + should_run = True + if self.max_pages is not None: + should_run = self.max_pages > self.current_page + if not should_run: + logger.info( + f"Stopping loop. We have reached max number of pages to scrape: {self.max_pages}" + ) + return should_run + def is_first_loop(self) -> bool: """ Simple check to see if this is the first time we have executed @@ -60,7 +80,7 @@ def _filter_ad_domains(self, udemy_links) -> List: if link.startswith(ad_domain): ad_links.add(link) if ad_links: - print(f"Removing ad links from courses: {ad_links}") + logger.info(f"Removing ad links from courses: {ad_links}") return list(set(udemy_links) - ad_links) def get_course_links(self, url: str) -> List: diff --git a/core/udemy.py b/core/udemy.py index 854a11c..ea5a886 100644 --- a/core/udemy.py +++ b/core/udemy.py @@ -1,3 +1,4 @@ +import logging import time from enum import Enum @@ -10,6 +11,8 @@ from core.exceptions import RobotException from core.settings import Settings +logger = logging.getLogger("udemy_enroller") + class UdemyStatus(Enum): """ @@ -60,10 +63,9 @@ def login(self, is_retry=False) -> None: ) self.login(is_retry=True) return - elif is_robot and is_retry: + if is_robot and is_retry: raise RobotException("I am a bot!") - else: - raise e + raise e else: # TODO: Verify successful login self.logged_in = True @@ -89,7 +91,7 @@ def redeem(self, url: str) -> str: ) if element_text not in self.settings.languages: - print(f"Course language not wanted: {element_text}") + logger.info(f"Course language not wanted: {element_text}") return UdemyStatus.UNWANTED_LANGUAGE.value if self.settings.categories: @@ -108,7 +110,7 @@ def redeem(self, url: str) -> str: if category in breadcrumbs: break else: - print("Skipping course as it does not have a wanted category") + logger.info("Skipping course as it does not have a wanted category") return UdemyStatus.UNWANTED_CATEGORY.value # Enroll Now 1 @@ -123,7 +125,7 @@ def redeem(self, url: str) -> str: "//div[starts-with(@class, 'buy-box--purchased-text-banner')]" ) if self.driver.find_elements_by_xpath(already_purchased_xpath): - print(f"Already enrolled in {course_name}") + logger.info(f"Already enrolled in {course_name}") return UdemyStatus.ENROLLED.value # Click to enroll in the course @@ -176,24 +178,43 @@ def redeem(self, url: str) -> str: # This logic should work for different locales and currencies _numbers = "".join(filter(lambda x: x if x.isdigit() else None, _price)) if _numbers.isdigit() and int(_numbers) > 0: - print(f"Skipping course as it now costs {_price}: {course_name}") + logger.info( + f"Skipping course as it now costs {_price}: {course_name}" + ) return UdemyStatus.EXPIRED.value + # Check if state/province element exists + billing_state_element_id = "billingAddressSecondarySelect" + billing_state_elements = self.driver.find_elements_by_id( + billing_state_element_id + ) + if billing_state_elements: + # If we are here it means a state/province element exists and needs to be filled + # Open the dropdown menu + billing_state_elements[0].click() + + # Pick the first element in the state/province dropdown + first_state_xpath = ( + "//select[@id='billingAddressSecondarySelect']//option[2]" + ) + element_present = EC.presence_of_element_located( + (By.XPATH, first_state_xpath) + ) + WebDriverWait(self.driver, 10).until(element_present).click() + # Hit the final Enroll now button - udemy_enroll_element_2 = self.driver.find_element_by_xpath(enroll_button_xpath) - udemy_enroll_element_2.click() + enroll_button_is_clickable = EC.element_to_be_clickable( + (By.XPATH, enroll_button_xpath) + ) + WebDriverWait(self.driver, 10).until(enroll_button_is_clickable).click() # Wait for success page to load success_element_class = "alert-success" - ( - WebDriverWait(self.driver, 10) - .until( - EC.presence_of_element_located((By.CLASS_NAME, success_element_class)) - ) - .text + WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located((By.CLASS_NAME, success_element_class)) ) - print(f"Successfully enrolled in: {course_name}") + logger.info(f"Successfully enrolled in: {course_name}") return UdemyStatus.ENROLLED.value def _check_if_robot(self) -> bool: diff --git a/core/utils.py b/core/utils.py index f350c66..6383083 100644 --- a/core/utils.py +++ b/core/utils.py @@ -1,3 +1,6 @@ +import logging +from typing import Union + from selenium.common.exceptions import ( NoSuchElementException, TimeoutException, @@ -5,21 +8,37 @@ ) from selenium.webdriver.remote.webdriver import WebDriver -from core import Settings, TutorialBarScraper, UdemyActions, exceptions -from core.cache import CourseCache +from core import CourseCache, Settings, TutorialBarScraper, UdemyActions, exceptions + +logger = logging.getLogger("udemy_enroller") -def redeem_courses(driver: WebDriver, settings: Settings): +def _redeem_courses( + driver: WebDriver, + settings: Settings, + max_pages: Union[int, None], + cache_hit_limit: int, +) -> None: """ Method to scrape courses from tutorialbar.com and enroll in them on udemy + :param WebDriver driver: Webdriver used to enroll in Udemy courses + :param Settings settings: Core settings used for Udemy + :param int max_pages: Max pages to scrape from tutorialbar.com + :param int cache_hit_limit: If we hit the cache this many times in a row we exit the script :return: """ cache = CourseCache() - tb_scraper = TutorialBarScraper() + tb_scraper = TutorialBarScraper(max_pages) udemy_actions = UdemyActions(driver, settings) udemy_actions.login() # login once outside while loop + + current_cache_hits = 0 + while True: + # Check if we should exit the loop + if not tb_scraper.script_should_run(): + break udemy_course_links = tb_scraper.run() for course_link in udemy_course_links: @@ -27,24 +46,71 @@ def redeem_courses(driver: WebDriver, settings: Settings): if course_link not in cache: status = udemy_actions.redeem(course_link) cache.add(course_link, status) + # Reset cache hit count as we haven't scraped this page before + current_cache_hits = 0 else: - print(f"In cache: {course_link}") + logger.info(f"In cache: {course_link}") + + # Increment the cache hit count since this link is in the cache + current_cache_hits += 1 + + # Exit the loop if we have reached the cache hit limit + if _reached_cache_hit_limit(cache_hit_limit, current_cache_hits): + return except NoSuchElementException as e: - print(e) + logger.error(e) except TimeoutException: - print(f"Timeout on link: {course_link}") - except WebDriverException as e: - print(f"Webdriver exception on link: {course_link}") - print(e) + logger.error(f"Timeout on link: {course_link}") + except WebDriverException: + logger.error(f"Webdriver exception on link: {course_link}") except KeyboardInterrupt: + logger.error("Exiting the script") raise except exceptions.RobotException as e: - print(e) - raise e + logger.error(e) + raise except Exception as e: - print(f"Unexpected exception: {e}") + logger.error(f"Unexpected exception: {e}") finally: if settings.is_ci_build: + logger.info("We have attempted to subscribe to 1 udemy course") + logger.info("Ending test") return - print("Moving on to the next page of the course list on tutorialbar.com") + logger.info("Moving on to the next page of the course list on tutorialbar.com") + + +def _reached_cache_hit_limit(cache_hit_limit, cache_hits) -> bool: + """ + Check if we have reached the cache hit limit + + :param int cache_hit_limit: Limit on the number of cache hits in a row to allow + :param int cache_hits: Current number of cache hits in a row + :return: + """ + reached_hit_limit = cache_hit_limit <= cache_hits + if reached_hit_limit: + logger.info(f"Hit cache {cache_hits} times in a row. Exiting script") + return reached_hit_limit + + +def redeem_courses( + driver: WebDriver, + settings: Settings, + max_pages: Union[int, None], + cache_hit_limit: int, +) -> None: + """ + Wrapper of _redeem_courses so we always close browser on completion + + :param WebDriver driver: Webdriver used to enroll in Udemy courses + :param Settings settings: Core settings used for Udemy + :param int max_pages: Max pages to scrape from tutorialbar.com + :param int cache_hit_limit: If we hit the cache this many times in a row we exit the script + :return: + """ + try: + _redeem_courses(driver, settings, max_pages, cache_hit_limit) + finally: + logger.info("Closing browser") + driver.quit() diff --git a/logconfig.ini b/logconfig.ini new file mode 100644 index 0000000..5e628ec --- /dev/null +++ b/logconfig.ini @@ -0,0 +1,36 @@ +[loggers] +keys=root,udemy_enroller + +[handlers] +keys=defaultHandler,consoleHandler + +[formatters] +keys=defaultFormatter,consoleFormatter + +[logger_root] +level=INFO +handlers=defaultHandler +qualname=root + +[logger_udemy_enroller] +level=INFO +handlers=defaultHandler,consoleHandler +qualname=udemy_enroller +propagate=0 + +[handler_defaultHandler] +class=FileHandler +formatter=defaultFormatter +args=("app.log", "a") + +[handler_consoleHandler] +class=StreamHandler +level=INFO +formatter=consoleFormatter +args=(sys.stdout,) + +[formatter_defaultFormatter] +format=%(asctime)s - %(name)s - %(levelname)s - %(module)s : %(message)s + +[formatter_consoleFormatter] +format=%(message)s \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index b8c32bf..54f9dc6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,11 @@ [tool.poetry] name = "automatic-udemy-course-enroller-get-paid-udemy-courses-for-free" -version = "0.1.0" +version = "0.3" description = "" -authors = ["fakeid30 "] +authors = [""] [tool.poetry.dependencies] -python = "^3.8.6" +python = "^3.8" selenium = "^3.141.0" requests = "^2.24.0" beautifulsoup4 = "^4.9.3" @@ -13,6 +13,10 @@ beautifulsoup4 = "^4.9.3" webdriver-manager = "^3.2.2" [tool.poetry.dev-dependencies] +black = "^20.8b1" +isort = "^5.6.4" +pytest = "^6.1.2" +pytest-cov = "^2.10.1" [build-system] requires = ["poetry-core>=1.0.0a5"] diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..ba21a73 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,5 @@ +[pytest] +addopts = + --cov=. --cov-report xml --cov-report term +testpaths = + tests diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..0f306f9 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,1482 @@ +import os +import shutil + +import pytest + + +@pytest.fixture(scope="session", autouse=True) +def test_file_dir(): + test_file_dir = "test_tmp" + # Try to delete directory in case it wasn't deleted after last test run + if os.path.isdir(test_file_dir): + shutil.rmtree(test_file_dir) + yield os.mkdir(test_file_dir) + # Delete directory after all tests completed + if os.path.isdir(test_file_dir): + shutil.rmtree(test_file_dir) + + +@pytest.fixture() +def tutorialbar_main_page(): + return ( + b' \r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\n\t\n\tAll Courses - Tutorial " + b'Bar\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\n\n\n\n\n\t\t\n\t\t\n\t\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n \n\n\n\n\n\n\n\n\n\n\n\n\t\t\n\t\t\r\n\r\n\t ' + b' \r\n\r\n
\r\n
\r\n ' + b' \r\n
\r\n
\r\n \r\n
\r\n
\r\n
\r\n \r\n \r\n ' + b'\r\n \r\n \r\n " + b"\r\n \r\n " + b"
\r\n
\r\n
\r\n \r\n\r\n
\r\n " + b'
\r\n \r\n\r\n
\r\n
\r\n \r\n
\r\n
\r\n
\r\n

All Courses

\r\n ' + b'\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t
\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\r\n\t \r\n\t
\r\n\t\t\r\n\t\t\t\t\t \r\n\t\t\t\t\t \r\n\t\t\t\t\t " + b'\r\n\t\t\t\t\t \r\n\t\t\t\t\t \r\n\t\t\t\t\t \r\n\t\t\t\t\t \r\n\t\t\t\t\t \r\n\t\t\t\t\t \r\n\t\t\t\t\t \r\n
\r\n
\r\n
\r\n \r\n ' + b"
\r\n
\r\n \r\n " + b'
\r\n \r\n EIQ2 Coaching for Improved '
+        b'Performance and Superior Results \r\n
\r\n
\r\n

EIQ2 Coaching for Improved Performance and Superior Results

\r\n ' + b' \r\n \r\n
\r\n ' + b' \r\n
\r\n ' + b" \t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\n\t\t \n\t\t\t\t \t\n\t \r\n " + b"
\r\n \r\n
\r\n \r\n
" + b' \r\n
\t\t\t\t\t \r\n\t\t\t\t\t \r\n\t\t\r\n\t\t\t\t \r\n\t\t\r\n\t
\r\n\t
\r\n\r\n\t\t
\n\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t" + b"\t\t
\n\t\t \r\n
" + b"\r\n
\r\n
\t\r\n \r\n \r\n " + b' \r\n \r\n " + b"
\r\n
\r\n \r\n\r\n\t\t\t\t\r\n\t \t\t\t\t
\r\n\t\t\t
\r\n\t\t\t\t\t\t\t\t\t
\r\n\t\t\t\t\t\t\r\n\t\t\t\t\t\t
\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t
Important Links
\r\n\t \t \t\t ' + b"\t\r\n\r\n\t\t\t\r\n\t
\t\t\t\t\t\t\t \r\n\t\t\t\t\t\t
\r\n\t\t\t\t\t\t
\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t " + b"\r\n\t\t\t\t\t\t
\r\n\t\t\t\t\t
\r\n\t\t\t\t\t\t\t\t\t\r\n\t\t\t
\t\r\n\t\t
\r\n\t\t\t\t" + b'
\r\n\t\t\t
\r\n\t\t\t\t
\r\n\t\t\t\t\t
\r\n\t\t\t\t\t\t\xc2\xa9 2020 TutorialBar.Com. All rights ' + b"reserved.\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t\t
\t\t\r\n\t\t\t\t
\r\n\t\t\t
\r\n\t\t\r\n\t\t\t\t\r\n
\r\n\r\n
Tutorial Bar
\n\n \n\n
Logo
\n \n\n\t\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\r\n\t\t\r\n\t\t\r\n\t\t\r\n" + ) + + +@pytest.fixture() +def tutorialbar_course_page(): + return ( + b'\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\n\t\n\t[100% OFF] Mindfulness Meditation For Pain Relief " + b'& Stress Management - Tutorial Bar\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\n\n\n\n\n\t\t\n\t\t\n\t\n\n\n\n\n\n\n\n\n\n\n\n \n\n\n\n\n\n\n\n\n\n\n\n\t\t\n\t\t\r\n\r\n\t ' + b' \r\n\r\n
\r\n
\r\n \r\n
\r\n
\r\n \r\n
\r\n
\r\n
\r\n \r\n \r\n ' + b'\r\n \r\n \r\n " + b"\r\n \r\n " + b"
\r\n
\r\n
\r\n \r\n\r\n
\r\n " + b'
\r\n \r\n\r\n \r\n
\r\n
\r\n \r\n
\r\n
\r\n ' + b'
\r\n \r\n
\r\n
\r\n ' + b' \r\n \r\n ' + b"

Mindfulness Meditation For Pain Relief & Stress Management

" + b' \r\n \r\n ' + b'
\r\n ' + b'
\r\n \t
\r\n\t \t
\r\n
\r\n ' + b'
\r\n \r\n ' + b" \r\n
\r\n " + b'
\r\n
\r\n\r\n\r\n
\r\n ' + b' \t\t\t\t\t\t\t\t\t\t \r\n\t\t\t\t\t\t \r\n \r\n ' + b" \r\n

Mindfulness Meditation For Pain Relief & Stress " + b'Management

\n
\nLearn How To ' + b"Use Professional Guided Mindfulness Meditation Sessions For Effective Pain Relief & Stress " + b'Management
\n

What you’ll ' + b'learn

\n
    \n
  • \n
    \n
    … How to relief your pain without ' + b'medication!
    \n
    \n
  • \n
  • \n
    \n
    … How to manage your stress on the long ' + b'term!
    \n
    \n
  • \n
  • \n
    \n
    … How to use guided meditations to balance ' + b'yourself!
    \n
    \n
  • \n
  • \n
    \n
    … How to use mindfulness to relax and calm ' + b'down!
    \n
    \n
  • \n
  • \n
    \n
    … How to use additional meditations for optimal ' + b'health!
    \n
    \n
  • \n
  • \n
    \n
    … How to apply practical tips for an optimal ' + b"meditation experience!
    \n
    \n
  • \n
\n
\n
\n
\n

Requirements

\n
    \n
  • \n
    \n
    no ' + b'prerequisites
    \n
    \n
  • \n
\n
\n
\n
\n

Who this ' + b'course is for:

\n
    \n
  • … People that want to know how to ' + b"relief your pain without medication!
  • \n
  • … People that want to know how to manage your stress on " + b"the long term!
  • \n
  • … People that want to know how to use guided meditations to balance " + b"yourself!
  • \n
  • … People that want to know how to use mindfulness to relax and calm " + b"down!
  • \n
  • … People that want to know how to use additional meditations for optimal " + b"health!
  • \n
  • … People that want to know how to apply practical tips for an optimal meditation " + b"experience!
  • \n
\n
\n\r\n
\r\n
\r\n
\r\n\r\n\r\n\r\n
\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\n\t\t \n\t\t\t\t\t
\n\t \t \t\n\t \t \t\t\t \t\t\t\n\t\t \t\n\t\t\t \t\t\t \tEnroll Now\t\t\t \t\t\t \t\t ' + b" \n\t\t \t\n\t \t\n\t\t \t\t\t\t\t\t\t\t \t\t\n\t\t \t " + b" \t \n\t
\n \t \t\t \t\n\t
\r\n\r\n\r\n
\r\n ' + b'\t
\r\n\t \t
\r\n \r\n\r\n \r\n\r\n\r\n \r\n \r\n\r\n\t
\r\n\t\t\t\t\t

\r\n\t \t
\r\n\r\n \r\n\r\n \t\t
\r\n\t\t\r\n\t\t
\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t ' + b'\r\n
\r\n
\r\n
\r\n ' + b" \r\n
\r\n
" + b'\r\n \r\n
\r\n \r\n ' + b'16 days; 16 hours; 16 '
+        b'screenplays! \r\n
\r\n
\r\n ' + b'

16 days; 16 hours; 16 ' + b"screenplays!

\r\n \r\n \r\n
\r\n
\r\n ' + b" \t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\n\t\t \n\t\t\t\t \t\n\t \r\n " + b"
\r\n \r\n
\r\n \r\n
" + b' \r\n
\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t \r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t \r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t \r\n\t\t
" + b" \r\n
\r\n
\r\n \r\n \r\n \r\n " + b"
\r\n
\r\n \r\n\r\n\t\t\t\t\r\n\t \t\t\t\t
\r\n\t\t\t
\r\n\t\t\t\t\t\t\t\t\t
\r\n\t\t\t\t\t\t\r\n\t\t\t\t\t\t
\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t
Important Links
\r\n\t \t \t\t ' + b"\t\r\n\r\n\t\t\t\r\n\t
\t\t\t\t\t\t\t \r\n\t\t\t\t\t\t
\r\n\t\t\t\t\t\t
\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t " + b"\r\n\t\t\t\t\t\t
\r\n\t\t\t\t\t
\r\n\t\t\t\t\t\t\t\t\t\r\n\t\t\t
\t\r\n\t\t
\r\n\t\t\t\t" + b'
\r\n\t\t\t
\r\n\t\t\t\t
\r\n\t\t\t\t\t
\r\n\t\t\t\t\t\t\xc2\xa9 2020 TutorialBar.Com. All rights ' + b"reserved.\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t\t
\t\t\r\n\t\t\t\t
\r\n\t\t\t
\r\n\t\t\r\n\t\t\t\t\r\n
\r\n\r\n
Tutorial Bar
\n\n \n\n
Logo
\n \n\n\t\n\n\n\n\n\n\n\n\n\n\r\n\t\t\r\n\t\t\r\n\t\t\r\n' + ) diff --git a/tests/core/__init__.py b/tests/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/core/test_cache.py b/tests/core/test_cache.py new file mode 100644 index 0000000..a2e04ba --- /dev/null +++ b/tests/core/test_cache.py @@ -0,0 +1,193 @@ +from datetime import datetime +from unittest import mock + +import pytest + +from core import CourseCache +from core.udemy import UdemyStatus + + +@pytest.mark.parametrize( + "cache_file_name,data_to_cache,expected_cached_data,urls_should_exist,urls_shouldnt_exist", + [ + ( + ".course_cache_1", + ( + ( + "https://www.udemy.com/course/python/?couponCode=25A92E01C0CB3718497B", + UdemyStatus.EXPIRED.value, + ), + ( + "https://www.udemy.com/course/short-sell/?couponCode=1NOV20", + UdemyStatus.ENROLLED.value, + ), + ( + "https://www.udemy.com/course/financial-accounting/?couponCode=1EEE181F6BB09A83190D", + UdemyStatus.ENROLLED.value, + ), + ( + "https://www.udemy.com/course/cycle/?couponCode=1EEE181F6BB09A83190D", + UdemyStatus.UNWANTED_LANGUAGE.value, + ), + ( + "https://www.udemy.com/course/art/?couponCode=1EEE181F6BB09A83190D", + UdemyStatus.UNWANTED_CATEGORY.value, + ), + ), + [ + { + "url": "https://www.udemy.com/course/python/?couponCode=25A92E01C0CB3718497B", + "status": UdemyStatus.EXPIRED.value, + "date": "2020-10-10T00:00:00", + }, + { + "url": "https://www.udemy.com/course/short-sell/?couponCode=1NOV20", + "status": UdemyStatus.ENROLLED.value, + "date": "2020-10-10T00:00:00", + }, + { + "url": "https://www.udemy.com/course/financial-accounting/?couponCode=1EEE181F6BB09A83190D", + "status": UdemyStatus.ENROLLED.value, + "date": "2020-10-10T00:00:00", + }, + { + "url": "https://www.udemy.com/course/cycle/?couponCode=1EEE181F6BB09A83190D", + "status": UdemyStatus.UNWANTED_LANGUAGE.value, + "date": "2020-10-10T00:00:00", + }, + { + "url": "https://www.udemy.com/course/art/?couponCode=1EEE181F6BB09A83190D", + "status": UdemyStatus.UNWANTED_CATEGORY.value, + "date": "2020-10-10T00:00:00", + }, + ], + [ + "https://www.udemy.com/course/python/?couponCode=25A92E01C0CB3718497B", + "https://www.udemy.com/course/short-sell/?couponCode=1NOV20", + "https://www.udemy.com/course/financial-accounting/?couponCode=1EEE181F6BB09A83190D", + "https://www.udemy.com/course/cycle/?couponCode=1EEE181F6BB09A83190D", + "https://www.udemy.com/course/art/?couponCode=1EEE181F6BB09A83190D", + ], + [ + "https://www.udemy.com/course/python1/?couponCode=25A92E01C0CB3718497B", + "https://www.udemy.com/course/new-course-1/?couponCode=19NOV20", + "https://www.udemy.com/course/new-course-2/?couponCode=25A92E01C0CB3718497B", + "https://www.udemy.com/course/new-course-3/?couponCode=10NOV20", + ], + ), + ], + ids=("Initialize cache and add data",), +) +@mock.patch("core.cache.datetime") +def test_cache( + mock_dt, + cache_file_name, + data_to_cache, + expected_cached_data, + urls_should_exist, + urls_shouldnt_exist, +): + mock_dt.datetime.utcnow = mock.Mock(return_value=datetime(2020, 10, 10)) + cc = CourseCache(f"test_tmp/{cache_file_name}") + for url, status in data_to_cache: + cc.add(url, status) + + assert cc._cache == expected_cached_data + for url in urls_should_exist: + assert url in cc + + for url in urls_shouldnt_exist: + assert url not in cc + + +@pytest.mark.parametrize( + "cache_file_name,data_to_cache,expected_cached_data,urls_should_exist,urls_shouldnt_exist", + [ + ( + ".course_cache_2", + ( + ( + "https://www.udemy.com/course/python/?couponCode=25A92E01C0CB3718497B", + UdemyStatus.EXPIRED.value, + ), + ( + "https://www.udemy.com/course/short-sell/?couponCode=1NOV20", + UdemyStatus.ENROLLED.value, + ), + ( + "https://www.udemy.com/course/financial-accounting/?couponCode=1EEE181F6BB09A83190D", + UdemyStatus.ENROLLED.value, + ), + ( + "https://www.udemy.com/course/cycle/?couponCode=1EEE181F6BB09A83190D", + UdemyStatus.UNWANTED_LANGUAGE.value, + ), + ( + "https://www.udemy.com/course/art/?couponCode=1EEE181F6BB09A83190D", + UdemyStatus.UNWANTED_CATEGORY.value, + ), + ), + [ + { + "url": "https://www.udemy.com/course/python/?couponCode=25A92E01C0CB3718497B", + "status": UdemyStatus.EXPIRED.value, + "date": "2020-11-10T00:00:00", + }, + { + "url": "https://www.udemy.com/course/short-sell/?couponCode=1NOV20", + "status": UdemyStatus.ENROLLED.value, + "date": "2020-11-10T00:00:00", + }, + { + "url": "https://www.udemy.com/course/financial-accounting/?couponCode=1EEE181F6BB09A83190D", + "status": UdemyStatus.ENROLLED.value, + "date": "2020-11-10T00:00:00", + }, + { + "url": "https://www.udemy.com/course/cycle/?couponCode=1EEE181F6BB09A83190D", + "status": UdemyStatus.UNWANTED_LANGUAGE.value, + "date": "2020-11-10T00:00:00", + }, + { + "url": "https://www.udemy.com/course/art/?couponCode=1EEE181F6BB09A83190D", + "status": UdemyStatus.UNWANTED_CATEGORY.value, + "date": "2020-11-10T00:00:00", + }, + ], + [ + "https://www.udemy.com/course/python/?couponCode=25A92E01C0CB3718497B", + "https://www.udemy.com/course/short-sell/?couponCode=1NOV20", + "https://www.udemy.com/course/financial-accounting/?couponCode=1EEE181F6BB09A83190D", + "https://www.udemy.com/course/cycle/?couponCode=1EEE181F6BB09A83190D", + "https://www.udemy.com/course/art/?couponCode=1EEE181F6BB09A83190D", + ], + [ + "https://www.udemy.com/course/python1/?couponCode=25A92E01C0CB3718497B", + "https://www.udemy.com/course/new-course-1/?couponCode=19NOV20", + "https://www.udemy.com/course/new-course-2/?couponCode=25A92E01C0CB3718497B", + "https://www.udemy.com/course/new-course-3/?couponCode=10NOV20", + ], + ), + ], + ids=("Initialize cache and add data",), +) +@mock.patch("core.cache.datetime") +def test_cache_load( + mock_dt, + cache_file_name, + data_to_cache, + expected_cached_data, + urls_should_exist, + urls_shouldnt_exist, +): + mock_dt.datetime.utcnow = mock.Mock(return_value=datetime(2020, 11, 10)) + # Create original cache and write some data to it + cc = CourseCache(f"test_tmp/{cache_file_name}") + for url, status in data_to_cache: + cc.add(url, status) + + assert cc._cache == expected_cached_data + + # Load from file when new instance created + next_run = CourseCache(f"test_tmp/{cache_file_name}") + assert next_run._cache == expected_cached_data diff --git a/tests/core/test_driver_manager.py b/tests/core/test_driver_manager.py new file mode 100644 index 0000000..50d5823 --- /dev/null +++ b/tests/core/test_driver_manager.py @@ -0,0 +1,120 @@ +from unittest import mock + +import pytest + +from core import DriverManager +from core.driver_manager import ( + ALL_VALID_BROWSER_STRINGS, + VALID_EDGE_STRINGS, + VALID_FIREFOX_STRINGS, + VALID_INTERNET_EXPLORER_STRINGS, + VALID_OPERA_STRINGS, +) + + +@pytest.mark.parametrize( + "browser_name", + [ + ("chrome"), + ("chromium"), + ("edge"), + ("firefox"), + ("opera"), + ("internet_explorer"), + ("tor"), + ], + ids=( + "create driver chrome", + "create driver chromium", + "create driver edge", + "create driver firefox", + "create driver opera", + "create driver internet_explorer", + "unsupported browser", + ), +) +@mock.patch("core.driver_manager.webdriver") +@mock.patch("core.driver_manager.ChromeDriverManager") +@mock.patch("core.driver_manager.GeckoDriverManager") +@mock.patch("core.driver_manager.EdgeChromiumDriverManager") +@mock.patch("core.driver_manager.IEDriverManager") +@mock.patch("core.driver_manager.OperaDriverManager") +@mock.patch("core.driver_manager.ChromeType") +def test_driver_manager_init( + _, + mock_opera_driver_manager, + mock_internet_explorer_driver_manager, + mock_edge_driver_manager, + mock_firefox_driver_manager, + mock_chrome_driver_manager, + mock_selenium_web_driver, + browser_name, +): + try: + dm = DriverManager(browser_name) + except ValueError: + assert browser_name not in ALL_VALID_BROWSER_STRINGS + else: + if browser_name in ("chrome",): + mock_selenium_web_driver.Chrome.assert_called_once_with( + mock_chrome_driver_manager().install(), options=None + ) + assert dm.driver == mock_selenium_web_driver.Chrome() + elif browser_name in ("chromium",): + mock_selenium_web_driver.Chrome.assert_called_once_with( + mock_chrome_driver_manager().install() + ) + assert dm.driver == mock_selenium_web_driver.Chrome() + elif browser_name in VALID_FIREFOX_STRINGS: + mock_selenium_web_driver.Firefox.assert_called_once_with( + executable_path=mock_firefox_driver_manager().install() + ) + assert dm.driver == mock_selenium_web_driver.Firefox() + elif browser_name in VALID_OPERA_STRINGS: + mock_selenium_web_driver.Opera.assert_called_once_with( + executable_path=mock_opera_driver_manager().install() + ) + assert dm.driver == mock_selenium_web_driver.Opera() + elif browser_name in VALID_EDGE_STRINGS: + mock_selenium_web_driver.Edge.assert_called_once_with( + mock_edge_driver_manager().install() + ) + assert dm.driver == mock_selenium_web_driver.Edge() + elif browser_name in VALID_INTERNET_EXPLORER_STRINGS: + mock_selenium_web_driver.Ie.assert_called_once_with( + mock_internet_explorer_driver_manager().install() + ) + assert dm.driver == mock_selenium_web_driver.Ie() + + +@pytest.mark.parametrize( + "browser_name,is_ci_build", + [ + ("chrome", True), + ("chrome", False), + ], + ids=("chrome is ci build", "chrome is not ci build"), +) +@mock.patch("core.driver_manager.webdriver") +@mock.patch("core.driver_manager.ChromeOptions") +@mock.patch("core.driver_manager.ChromeDriverManager") +@mock.patch("core.driver_manager.ChromeType") +def test_driver_manager_ci_build( + _, + mock_chrome_driver_manager, + mock_chrome_options, + mock_selenium_web_driver, + browser_name, + is_ci_build, +): + + dm = DriverManager(browser_name, is_ci_build=is_ci_build) + + if is_ci_build: + options = mock_chrome_options() + else: + options = None + mock_selenium_web_driver.Chrome.assert_called_once_with( + mock_chrome_driver_manager().install(), options=options + ) + assert dm.driver == mock_selenium_web_driver.Chrome() diff --git a/tests/core/test_settings.py b/tests/core/test_settings.py new file mode 100644 index 0000000..5fdcd12 --- /dev/null +++ b/tests/core/test_settings.py @@ -0,0 +1,186 @@ +import os +from unittest import mock + +import pytest +from ruamel.yaml import YAML + +from core import Settings + + +@pytest.mark.parametrize( + "email,password,zip_code,languages,categories,save,file_name", + [ + ( + "test4@mail.com", + "dskalksdl678", + "12345", + None, + None, + "Y", + "test_settings1.yaml", + ), + ( + "test6@mail.com", + "$6237556^^$", + "12345", + "English,French", + None, + "Y", + "test_settings2.yaml", + ), + ( + "test9@mail.com", + "$62371231236^^$", + "12345", + "English,French", + "Development,Art", + "Y", + "test_settings5.yaml", + ), + ( + "cultest8lzie@mail.com", + "43223*&6", + "12345", + None, + None, + "N", + "no_save_test_settings.yaml", + ), + ], + ids=( + "create settings all languages and save", + "create settings select languages and save", + "create settings select categories and save", + "create settings all languages and don't save", + ), +) +def test_settings(email, password, zip_code, languages, categories, save, file_name): + with mock.patch( + "builtins.input", side_effect=[email, zip_code, languages, categories, save] + ): + with mock.patch("getpass.getpass", return_value=password): + settings_path = f"test_tmp/{file_name}" + settings = Settings(settings_path) + assert settings.email == email + assert settings.password == password + assert settings.zip_code == zip_code + assert settings.languages == [] if languages is None else languages + assert settings.categories == [] if categories is None else categories + + if save.upper() == "Y": + yaml = YAML() + with open(settings_path) as f: + settings = yaml.load(f) + assert settings["udemy"]["email"] == email + assert settings["udemy"]["password"] == password + assert settings["udemy"]["zipcode"] == zip_code + assert ( + settings["udemy"]["languages"] == [] + if languages is None + else ",".join(languages) + ) + assert ( + settings["udemy"]["categories"] == [] + if categories is None + else categories + ) + # Load settings just created + Settings(settings_path) + else: + assert os.path.isdir(settings_path) is False + + +@pytest.mark.parametrize( + "email,password,zip_code,languages,categories,save,file_name", + [ + ( + "test9@mail.com", + "uherwh834", + "12345", + None, + None, + "Y", + "test_load_existing_settings1.yaml", + ), + ( + "test80@mail.com", + "jkajsdsad", + "None", + "Italian", + None, + "Y", + "test_load_existing_settings9.yaml", + ), + ( + "test10@mail.com", + "234sdfs", + "None", + "English", + "Development,Art", + "Y", + "test_load_existing_settings2.yaml", + ), + ( + "test11@mail.com", + "frtuhrfty234", + "788192", + "French,German", + None, + "Y", + "test_load_existing_settings3.yaml", + ), + ], + ids=( + "load existing settings no languages", + "load existing settings no categories", + "load existing settings no zipcode", + "load existing settings full", + ), +) +def test_load_existing_settings( + email, password, zip_code, languages, categories, save, file_name +): + with mock.patch( + "builtins.input", side_effect=[email, zip_code, languages, categories, save] + ): + with mock.patch("getpass.getpass", return_value=password): + settings_path = f"test_tmp/{file_name}" + Settings(settings_path) + + # Load existing settings + settings = Settings(settings_path) + assert settings.email == email + assert settings.password == password + assert settings.zip_code == zip_code + assert settings.languages == [] if languages is None else languages + assert settings.categories == [] if categories is None else categories + + +@pytest.mark.parametrize( + "is_ci_run,email,password", + [ + ( + True, + "username1@mail.com", + "password1", + ), + ( + False, + "username2@mail.com", + "password2", + ), + ], + ids=( + "is ci run", + "is not a ci run", + ), +) +@mock.patch.object(Settings, "_load_user_settings") +def test_load_ci_settings(_, monkeypatch, is_ci_run, email, password): + monkeypatch.setenv("CI_TEST", str(is_ci_run)) + monkeypatch.setenv("UDEMY_EMAIL", email) + monkeypatch.setenv("UDEMY_PASSWORD", password) + settings = Settings("") + if is_ci_run: + assert settings.email == email + assert settings.password == password diff --git a/tests/core/test_tutorialbar.py b/tests/core/test_tutorialbar.py new file mode 100644 index 0000000..0cc3929 --- /dev/null +++ b/tests/core/test_tutorialbar.py @@ -0,0 +1,84 @@ +from unittest import mock + +import pytest + +from core import TutorialBarScraper + + +@pytest.mark.parametrize( + "tutorialbar_course_page_link,tutorialbar_links,udemy_links", + [ + ( + "https://www.tutorialbar.com/all-courses/page/1/", + [ + "https://www.tutorialbar.com/course_1", + "https://www.tutorialbar.com/course_2", + ], + [ + "https://www.udemy.com/1?FREECOURSE", + "https://www.udemy.com/2?FREECOURSE", + ], + ), + ("https://www.tutorialbar.com/all-courses/page/1/", [], []), + ], + ids=("List of courses", "Empty courses"), +) +@mock.patch.object(TutorialBarScraper, "gather_udemy_course_links") +@mock.patch.object(TutorialBarScraper, "get_course_links") +def test_run( + mock_get_course_links, + mock_gather_udemy_course_links, + tutorialbar_course_page_link, + tutorialbar_links, + udemy_links, +): + mock_get_course_links.return_value = tutorialbar_links + mock_gather_udemy_course_links.return_value = udemy_links + tbs = TutorialBarScraper() + links = tbs.run() + + mock_get_course_links.assert_called_with(tutorialbar_course_page_link) + mock_gather_udemy_course_links.assert_called_with(tutorialbar_links) + for link in links: + assert link in udemy_links + + +@pytest.mark.parametrize( + "page_number,is_first_page", + [(1, True), (2, False)], + ids=( + "First Page", + "Not first page", + ), +) +def test_check_page_number(page_number, is_first_page): + tbs = TutorialBarScraper() + tbs.current_page = page_number + assert tbs.is_first_loop() == is_first_page + + +@mock.patch("core.tutorialbar.requests") +def test_get_course_links(mock_requests, tutorialbar_main_page): + url = "https://www.tutorialbar.com/main" + requests_response = mock.Mock() + requests_response.content = tutorialbar_main_page + mock_requests.get.return_value = requests_response + tbs = TutorialBarScraper() + tbs.current_page = 1 + links = tbs.get_course_links(url) + + assert tbs.last_page == "601" + assert links == [ + "https://www.tutorialbar.com/mindfulness-meditation-for-pain-relief-stress-management/", + "https://www.tutorialbar.com/become-a-crm-manager-overview-for-email-marketing-starters/", + "https://www.tutorialbar.com/superminds-the-future-of-artificial-intelligence-ai/", + "https://www.tutorialbar.com/invade-your-classroom-with-digital-robot-teachers-in-2020/", + "https://www.tutorialbar.com/introduction-au-machine-learning-python/", + "https://www.tutorialbar.com/comic-creation-for-entrepreneurs-2020-edition/", + "https://www.tutorialbar.com/delicious-japanese-language-for-foodies-jlpt-n5-jlpt-n4/", + "https://www.tutorialbar.com/sparring-tai-chi-chen-new-frame-routine-2-for-fitness/", + "https://www.tutorialbar.com/active-learning-using-games-in-education/", + "https://www.tutorialbar.com/eiq2-coaching-for-improved-performance-and-superior-results/", + "https://www.tutorialbar.com/quickbooks-pro-desktop-bookkeeping-business-easy-way/", + "https://www.tutorialbar.com/quickbooks-online-bank-feeds-credit-card-feeds-2020/", + ] diff --git a/tests/test_udemy_enroller.py b/tests/test_udemy_enroller.py new file mode 100644 index 0000000..9e478ca --- /dev/null +++ b/tests/test_udemy_enroller.py @@ -0,0 +1,50 @@ +import argparse +from unittest import mock + +import pytest + +from udemy_enroller import parse_args + + +@pytest.mark.parametrize( + "browser_cli,max_pages_cli,expected_browser,expected_max_pages,print_help", + [ + ("chrome", None, "chrome", None, False), + ("firefox", None, "firefox", None, False), + ("chromium", None, "chromium", None, False), + ("internet_explorer", None, "internet_explorer", None, False), + ("opera", None, "opera", None, False), + ("edge", None, "edge", None, False), + (None, None, None, None, True), + ("firefox", 10, "firefox", 10, False), + ], + ids=( + "Test chrome via cli", + "Test firefox via cli", + "Test chromium via cli", + "Test internet_explorer via cli", + "Test opera via cli", + "Test edge via cli", + "No browser selected print help", + "Pass max pages via cli", + ), +) +@mock.patch("argparse.ArgumentParser.print_help") +def test_argparse( + mock_print_help, + browser_cli, + max_pages_cli, + expected_browser, + expected_max_pages, + print_help, +): + with mock.patch( + "argparse.ArgumentParser.parse_args", + return_value=argparse.Namespace(browser=browser_cli, max_pages=max_pages_cli), + ): + args = parse_args() + if print_help: + assert mock_print_help.call_count == 1 + else: + assert args.browser == expected_browser + assert args.max_pages is expected_max_pages diff --git a/udemy_enroller.py b/udemy_enroller.py new file mode 100644 index 0000000..5e68366 --- /dev/null +++ b/udemy_enroller.py @@ -0,0 +1,77 @@ +# Install all the requirements by running requirements.py in IDLE or follow the alternate instructions at +# https://github.com/aapatre/Automatic-Udemy-Course-Enroller-GET-PAID-UDEMY-COURSES-for-FREE/ Make sure you have +# cleared all saved payment details on your Udemy account & the browser! +import argparse +from argparse import Namespace +from typing import Union + +from selenium.webdriver.remote.webdriver import WebDriver + +from core import ALL_VALID_BROWSER_STRINGS, DriverManager, Settings +from core.utils import redeem_courses + + +def run( + browser: str, + max_pages: Union[int, None], + cache_hit_limit: int, + driver: WebDriver = None, +): + """ + Run the udemy enroller script + + :param str browser: Name of the browser we want to create a driver for + :param int or None max_pages: Max number of pages to scrape from tutorialbar.com + :param int cache_hit_limit: If we hit the cache this many times in a row we exit the script + :param WebDriver driver: + :return: + """ + settings = Settings() + if driver is None: + dm = DriverManager(browser=browser, is_ci_build=settings.is_ci_build) + driver = dm.driver + redeem_courses(driver, settings, max_pages, cache_hit_limit) + + +def parse_args(browser=None, use_manual_driver=False) -> Namespace: + """ + Parse args from the CLI or use the args passed in + + :param str browser: Name of the browser we want to create a driver for + :param bool use_manual_driver: If True don't create a web driver using web driver manager + :return: Args to be used in the script + """ + parser = argparse.ArgumentParser(description="Udemy Enroller") + + parser.add_argument( + "--browser", + type=str, + default=browser, + choices=ALL_VALID_BROWSER_STRINGS, + help="Browser to use for Udemy Enroller", + ) + parser.add_argument( + "--max-pages", + type=int, + default=None, + help="Max pages to scrape from tutorialbar.com", + ) + parser.add_argument( + "--cache-hits", + type=int, + default=12, + help="If we hit the cache this number of times in a row we will exit the script", + ) + + args = parser.parse_args() + + if args.browser is None and not use_manual_driver: + parser.print_help() + else: + return args + + +if __name__ == "__main__": + args = parse_args() + if args: + run(args.browser, args.max_pages, args.cache_hits) diff --git a/udemy_enroller_chrome.py b/udemy_enroller_chrome.py index 0e5d2d6..a10a265 100644 --- a/udemy_enroller_chrome.py +++ b/udemy_enroller_chrome.py @@ -1,45 +1,15 @@ # Install all the requirements by running requirements.py in IDLE or follow the alternate instructions at # https://github.com/aapatre/Automatic-Udemy-Course-Enroller-GET-PAID-UDEMY-COURSES-for-FREE/ Make sure you have # cleared all saved payment details on your Udemy account & the browser! -from selenium import webdriver -from webdriver_manager.chrome import ChromeDriverManager +import warnings -from core import Settings -from core.utils import redeem_courses +from udemy_enroller import parse_args, run -settings = Settings() - -chrome_options = None -if settings.is_ci_build: - from selenium.webdriver.chrome.options import Options - - # Having the user-agent with Headless param was always leading to robot check - user_agent = ( - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 " - "Safari/537.36" +if __name__ == "__main__": + browser = "chrome" + warnings.warn( + f"Please use `udemy_enroller.py --browser={browser}` as this script will be removed soon", + DeprecationWarning, ) - chrome_options = Options() - # We need to run headless when using github CI - chrome_options.add_argument("--headless") - chrome_options.add_argument("user-agent={0}".format(user_agent)) - chrome_options.add_argument("--window-size=1325x744") - print("This is a CI run") - -driver = webdriver.Chrome(ChromeDriverManager().install(), options=chrome_options) - -# Maximizes the browser window since Udemy has a responsive design and the code only works -driver.maximize_window() -# in the maximized layout - -try: - redeem_courses(driver, settings) - if settings.is_ci_build: - print("We have attempted to subscribe to 1 udemy course") - print("Ending test") -except KeyboardInterrupt: - print("Exiting the script") -except Exception as e: - print("Error: {}".format(e)) -finally: - print("Closing browser") - driver.quit() + args = parse_args(browser) + run(args.browser, args.max_pages, args.cache_hits) diff --git a/udemy_enroller_chromium.py b/udemy_enroller_chromium.py index ea31d8f..bc21d33 100644 --- a/udemy_enroller_chromium.py +++ b/udemy_enroller_chromium.py @@ -1,29 +1,15 @@ # Install all the requirements by running requirements.py in IDLE or follow the alternate instructions at # https://github.com/aapatre/Automatic-Udemy-Course-Enroller-GET-PAID-UDEMY-COURSES-for-FREE/ Make sure you have # cleared all saved payment details on your Udemy account & the browser! -from selenium import webdriver -from webdriver_manager.chrome import ChromeDriverManager -from webdriver_manager.utils import ChromeType - -from core import Settings -from core.utils import redeem_courses - -settings = Settings() - -driver = webdriver.Chrome( - ChromeDriverManager(chrome_type=ChromeType.CHROMIUM).install() -) - -# Maximizes the browser window since Udemy has a responsive design and the code only works -driver.maximize_window() -# in the maximized layout - -try: - redeem_courses(driver, settings) -except KeyboardInterrupt: - print("Exiting the script") -except Exception as e: - print("Error: {}".format(e)) -finally: - print("Closing browser") - driver.quit() +import warnings + +from udemy_enroller import parse_args, run + +if __name__ == "__main__": + browser = "chromium" + warnings.warn( + f"Please use `udemy_enroller.py --browser={browser}` as this script will be removed soon", + DeprecationWarning, + ) + args = parse_args(browser) + run(args.browser, args.max_pages, args.cache_hits) diff --git a/udemy_enroller_edge.py b/udemy_enroller_edge.py index d61ce11..e33eecf 100644 --- a/udemy_enroller_edge.py +++ b/udemy_enroller_edge.py @@ -1,26 +1,15 @@ # Install all the requirements by running requirements.py in IDLE or follow the alternate instructions at # https://github.com/aapatre/Automatic-Udemy-Course-Enroller-GET-PAID-UDEMY-COURSES-for-FREE/ Make sure you have # cleared all saved payment details on your Udemy account & the browser! -from selenium import webdriver -from webdriver_manager.microsoft import EdgeChromiumDriverManager - -from core import Settings -from core.utils import redeem_courses - -settings = Settings() - -driver = webdriver.Edge(EdgeChromiumDriverManager().install()) - -# Maximizes the browser window since Udemy has a responsive design and the code only works -driver.maximize_window() -# in the maximized layout - -try: - redeem_courses(driver, settings) -except KeyboardInterrupt: - print("Exiting the script") -except Exception as e: - print("Error: {}".format(e)) -finally: - print("Closing browser") - driver.quit() +import warnings + +from udemy_enroller import parse_args, run + +if __name__ == "__main__": + browser = "edge" + warnings.warn( + f"Please use `udemy_enroller.py --browser={browser}` as this script will be removed soon", + DeprecationWarning, + ) + args = parse_args(browser) + run(args.browser, args.max_pages, args.cache_hits) diff --git a/udemy_enroller_firefox.py b/udemy_enroller_firefox.py index a18a8c0..5d5d770 100644 --- a/udemy_enroller_firefox.py +++ b/udemy_enroller_firefox.py @@ -2,26 +2,15 @@ # https://github.com/aapatre/Automatic-Udemy-Course-Enroller-GET-PAID-UDEMY-COURSES-for-FREE/ Make sure you have # cleared all saved payment details on your Udemy account & the browser! For firefox you need to manually install the # driver on Arch Linux (sudo pacman -S geckodriver). Untested on other platforms. -from selenium import webdriver -from webdriver_manager.firefox import GeckoDriverManager +import warnings -from core import Settings -from core.utils import redeem_courses +from udemy_enroller import parse_args, run -settings = Settings() - -driver = webdriver.Firefox(executable_path=GeckoDriverManager().install()) - -# Maximizes the browser window since Udemy has a responsive design and the -# code only works in the maximized layout -driver.maximize_window() - -try: - redeem_courses(driver, settings) -except KeyboardInterrupt: - print("Exiting the script") -except Exception as e: - print("Error: {}".format(e)) -finally: - print("Closing browser") - driver.quit() +if __name__ == "__main__": + browser = "firefox" + warnings.warn( + f"Please use `udemy_enroller.py --browser={browser}` as this script will be removed soon", + DeprecationWarning, + ) + args = parse_args(browser) + run(args.browser, args.max_pages, args.cache_hits) diff --git a/udemy_enroller_internet_explorer.py b/udemy_enroller_internet_explorer.py index 5aecd8c..d1c8fa5 100644 --- a/udemy_enroller_internet_explorer.py +++ b/udemy_enroller_internet_explorer.py @@ -1,27 +1,15 @@ # Install all the requirements by running requirements.py in IDLE or follow the alternate instructions at # https://github.com/aapatre/Automatic-Udemy-Course-Enroller-GET-PAID-UDEMY-COURSES-for-FREE/ Make sure you have # cleared all saved payment details on your Udemy account & the browser! -from selenium import webdriver -from webdriver_manager.microsoft import IEDriverManager - -from core import Settings -from core.utils import redeem_courses - -settings = Settings() - -driver = webdriver.Ie(IEDriverManager().install()) - -# Maximizes the browser window since Udemy has a responsive design and the code only works -driver.maximize_window() - -# in the maximized layout - -try: - redeem_courses(driver, settings) -except KeyboardInterrupt: - print("Exiting the script") -except Exception as e: - print("Error: {}".format(e)) -finally: - print("Closing browser") - driver.quit() +import warnings + +from udemy_enroller import parse_args, run + +if __name__ == "__main__": + browser = "internet_explorer" + warnings.warn( + f"Please use `udemy_enroller.py --browser={browser}` as this script will be removed soon", + DeprecationWarning, + ) + args = parse_args(browser) + run(args.browser, args.max_pages, args.cache_hits) diff --git a/udemy_enroller_opera.py b/udemy_enroller_opera.py index 9cff2ba..8970d4a 100644 --- a/udemy_enroller_opera.py +++ b/udemy_enroller_opera.py @@ -1,26 +1,15 @@ # Install all the requirements by running requirements.py in IDLE or follow the alternate instructions at # https://github.com/aapatre/Automatic-Udemy-Course-Enroller-GET-PAID-UDEMY-COURSES-for-FREE/ Make sure you have # cleared all saved payment details on your Udemy account & the browser! -from selenium import webdriver -from webdriver_manager.opera import OperaDriverManager - -from core import Settings -from core.utils import redeem_courses - -settings = Settings() - -driver = webdriver.Opera(executable_path=OperaDriverManager().install()) - -# Maximizes the browser window since Udemy has a responsive design and the code only works -driver.maximize_window() -# in the maximized layout - -try: - redeem_courses(driver, settings) -except KeyboardInterrupt: - print("Exiting the script") -except Exception as e: - print("Error: {}".format(e)) -finally: - print("Closing browser") - driver.quit() +import warnings + +from udemy_enroller import parse_args, run + +if __name__ == "__main__": + browser = "opera" + warnings.warn( + f"Please use `udemy_enroller.py --browser={browser}` as this script will be removed soon", + DeprecationWarning, + ) + args = parse_args(browser) + run(args.browser, args.max_pages, args.cache_hits) diff --git a/udemy_enroller_vanilla.py b/udemy_enroller_vanilla.py index 2398d0e..8e52539 100644 --- a/udemy_enroller_vanilla.py +++ b/udemy_enroller_vanilla.py @@ -4,33 +4,31 @@ from selenium import webdriver from core import Settings -from core.utils import redeem_courses +from udemy_enroller import parse_args, run -settings = Settings() """### **Enter the path/location of your webdriver** By default, the webdriver for Microsoft Edge browser has been chosen in the code below. Also, enter the location of your webdriver. """ -# On windows you need the r (raw string) in front of the string to deal with backslashes. -# Replace this string with the path for your webdriver -path = r"..location\msedgedriver.exe" -driver = webdriver.Edge( - path -) # webdriver.Chrome(path) for Google Chrome, webdriver.Firefox(path) for Mozilla Firefox, webdriver.Edge( -# path) for Microsoft Edge, webdriver.Safari(path) for Apple Safari - -# Maximizes the browser window since Udemy has a responsive design and the code only works -driver.maximize_window() -# in the maximized layout - -try: - redeem_courses(driver, settings) -except KeyboardInterrupt: - print("Exiting the script") -except Exception as e: - print("Error: {}".format(e)) -finally: - print("Closing browser") - driver.quit() + +if __name__ == "__main__": + args = parse_args(use_manual_driver=True) + + settings = Settings() + # On windows you need the r (raw string) in front of the string to deal with backslashes. + # Replace this string with the path for your webdriver + + path = r"..location\msedgedriver.exe" + driver = webdriver.Edge(path) + # driver = webdriver.Chrome(path) # Uncomment for Google Chrome driver + # driver = webdriver.Firefox(path) # Uncomment for Mozilla Firefox driver + # driver = webdriver.Edge(path) # Uncomment for Microsoft Edge driver + # driver = webdriver.Safari(path) # Uncomment for Apple Safari driver + + # Maximizes the browser window since Udemy has a responsive design and the code only works + # in the maximized layout + driver.maximize_window() + + run(args.browser, args.max_pages, args.cache_hits, driver=driver)