From d6a5c3cace29913977ce582ab2a06465defd2e75 Mon Sep 17 00:00:00 2001 From: cullzie Date: Tue, 17 Nov 2020 13:31:56 +0000 Subject: [PATCH 01/44] Adding dev dependencies and unittests --- .coveragerc | 5 +++ .gitignore | 5 ++- core/settings.py | 4 +-- pyproject.toml | 8 +++-- pytest.ini | 5 +++ tests/__init__.py | 0 tests/conftest.py | 15 +++++++++ tests/core/__init__.py | 0 tests/core/test_settings.py | 67 +++++++++++++++++++++++++++++++++++++ 9 files changed, 104 insertions(+), 5 deletions(-) create mode 100644 .coveragerc create mode 100644 pytest.ini create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/core/__init__.py create mode 100644 tests/core/test_settings.py diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..ccfedc8 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,5 @@ +[report] +show_missing = True +skip_covered = True +omit = + tests/* diff --git a/.gitignore b/.gitignore index cce4bfc..80e919e 100644 --- a/.gitignore +++ b/.gitignore @@ -224,4 +224,7 @@ Pipfile.lock settings.yaml -poetry.lock \ No newline at end of file +poetry.lock + +# Ignore temporary test folder +test_tmp/ diff --git a/core/settings.py b/core/settings.py index c53582b..4e07b7c 100644 --- a/core/settings.py +++ b/core/settings.py @@ -13,13 +13,13 @@ 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._settings_path = "settings.yaml" + self._settings_path = settings_path self.is_ci_build = strtobool(os.environ.get("CI", "False")) self._init_settings() diff --git a/pyproject.toml b/pyproject.toml index b8c32bf..2f56069 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,10 +2,10 @@ name = "automatic-udemy-course-enroller-get-paid-udemy-courses-for-free" version = "0.1.0" 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..d8a10e3 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,5 @@ +[pytest] +addopts = + --cov=. --cov-report html --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..5db1065 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,15 @@ +import os +import pytest +import shutil + + +@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) 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_settings.py b/tests/core/test_settings.py new file mode 100644 index 0000000..47b277c --- /dev/null +++ b/tests/core/test_settings.py @@ -0,0 +1,67 @@ +import os +import pytest + +from core import Settings + +from unittest import mock +from ruamel.yaml import YAML + + +@pytest.mark.parametrize( + "email,password,zip_code,languages,save,file_name", + [("test4@mail.com", "dskalksdl678", "12345", None, "Y", "test_settings1.yaml"), + ("test6@mail.com", "$6237556^^$", "12345", "English,French", "Y", "test_settings2.yaml"), + ("cultest8lzie@mail.com", "43223*&6", "12345", None, "N", "no_save_test_settings.yaml")], + ids=( + "create settings all languages and save", + "create settings select languages and save", + "create settings all languages and don't save" + ) +) +def test_settings(email, password, zip_code, languages, save, file_name): + with mock.patch("builtins.input", side_effect=[email, zip_code, languages, 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 + + 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) + # Load settings just created + Settings(settings_path) + else: + assert os.path.isdir(settings_path) is False + + +@pytest.mark.parametrize( + "email,password,zip_code,languages,save,file_name", + [("test9@mail.com", "uherwh834", "12345", None, "Y", "test_load_existing_settings1.yaml"), + ("test10@mail.com", "234sdfs", "None", "English", "Y", "test_load_existing_settings2.yaml"), + ("test11@mail.com", "frtuhrfty234", "788192", "French,German", "Y", "test_load_existing_settings3.yaml")], + ids=( + "load existing settings no languages", + "load existing settings no zipcode", + "load existing settings full", + ) +) +def test_load_existing_settings(email, password, zip_code, languages, save, file_name): + with mock.patch("builtins.input", side_effect=[email, zip_code, languages, 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 From ea231ce6167963c6804bec4848ca09f0ca466292 Mon Sep 17 00:00:00 2001 From: cullzie Date: Tue, 17 Nov 2020 16:06:04 +0000 Subject: [PATCH 02/44] Bump version in pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2f56069..7f376fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "automatic-udemy-course-enroller-get-paid-udemy-courses-for-free" -version = "0.1.0" +version = "0.2.2" description = "" authors = [""] From b460b528f508e7d916b2072efa09e9c2ebfa558d Mon Sep 17 00:00:00 2001 From: cullzie Date: Tue, 17 Nov 2020 16:10:50 +0000 Subject: [PATCH 03/44] Run unittests in CI --- .github/workflows/python-package.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 61a6703..392ab01 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -23,14 +23,17 @@ jobs: - 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: Run unittests + run: | + poetry run pytest - name: Install Chrome and Firefox run: | sudo apt install google-chrome-stable From a6b748da14b14c7274691177e8f06efa669b2b71 Mon Sep 17 00:00:00 2001 From: cullzie Date: Tue, 17 Nov 2020 16:20:41 +0000 Subject: [PATCH 04/44] Remove browser installs and update CI env variable --- .github/workflows/python-package.yml | 7 ++----- core/settings.py | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 392ab01..de38f45 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -34,13 +34,10 @@ jobs: - name: Run unittests run: | poetry run pytest - - name: Install Chrome and Firefox - run: | - sudo apt install google-chrome-stable - sudo apt-get install firefox - 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/core/settings.py b/core/settings.py index 4e07b7c..b63eab7 100644 --- a/core/settings.py +++ b/core/settings.py @@ -20,7 +20,7 @@ def __init__(self, settings_path="settings.yaml"): self.languages = [] self._settings_path = settings_path - self.is_ci_build = strtobool(os.environ.get("CI", "False")) + self.is_ci_build = strtobool(os.environ.get("CI_TEST", "False")) self._init_settings() def _init_settings(self) -> None: From e6405a9f0a0ddef1c9aa27614ed003cd4781f250 Mon Sep 17 00:00:00 2001 From: cullzie Date: Tue, 17 Nov 2020 16:35:25 +0000 Subject: [PATCH 05/44] Extract more logic to core --- core/utils.py | 20 +++++++++++++++++++- udemy_enroller_chrome.py | 15 ++------------- udemy_enroller_chromium.py | 12 ++---------- udemy_enroller_edge.py | 12 ++---------- udemy_enroller_firefox.py | 12 +----------- udemy_enroller_internet_explorer.py | 13 ++----------- udemy_enroller_opera.py | 12 ++---------- udemy_enroller_vanilla.py | 19 +++++++------------ 8 files changed, 37 insertions(+), 78 deletions(-) diff --git a/core/utils.py b/core/utils.py index 9b5effb..a3f9872 100644 --- a/core/utils.py +++ b/core/utils.py @@ -8,7 +8,7 @@ from core import Settings, TutorialBarScraper, UdemyActions, exceptions -def redeem_courses(driver: WebDriver, settings: Settings): +def _redeem_courses(driver: WebDriver, settings: Settings): """ Method to scrape courses from tutorialbar.com and enroll in them on udemy @@ -31,6 +31,7 @@ def redeem_courses(driver: WebDriver, settings: Settings): print(f"Webdriver exception on link: {course_link}") print(e) except KeyboardInterrupt: + print("Exiting the script") raise except exceptions.RobotException as e: print(e) @@ -39,6 +40,23 @@ def redeem_courses(driver: WebDriver, settings: Settings): print(f"Unexpected exception: {e}") finally: if settings.is_ci_build: + print("We have attempted to subscribe to 1 udemy course") + print("Ending test") return print("Moving on to the next page of the course list on tutorialbar.com") + + +def redeem_courses(driver, settings) -> None: + """ + Wrapper of _redeem_courses so we always close browser on completion + + :param driver: + :param settings: + :return: + """ + try: + _redeem_courses(driver, settings) + finally: + print("Closing browser") + driver.quit() diff --git a/udemy_enroller_chrome.py b/udemy_enroller_chrome.py index ae4f31e..365f2e0 100644 --- a/udemy_enroller_chrome.py +++ b/udemy_enroller_chrome.py @@ -28,18 +28,7 @@ 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 +driver.maximize_window() -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() +redeem_courses(driver, settings) diff --git a/udemy_enroller_chromium.py b/udemy_enroller_chromium.py index c46e0ba..f326ed5 100644 --- a/udemy_enroller_chromium.py +++ b/udemy_enroller_chromium.py @@ -14,15 +14,7 @@ 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 +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() +redeem_courses(driver, settings) diff --git a/udemy_enroller_edge.py b/udemy_enroller_edge.py index d61ce11..c026d37 100644 --- a/udemy_enroller_edge.py +++ b/udemy_enroller_edge.py @@ -12,15 +12,7 @@ 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 +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() +redeem_courses(driver, settings) diff --git a/udemy_enroller_firefox.py b/udemy_enroller_firefox.py index df3917e..c04a4e1 100644 --- a/udemy_enroller_firefox.py +++ b/udemy_enroller_firefox.py @@ -14,14 +14,4 @@ # 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() +redeem_courses(driver, settings) diff --git a/udemy_enroller_internet_explorer.py b/udemy_enroller_internet_explorer.py index 5aecd8c..9dcf44a 100644 --- a/udemy_enroller_internet_explorer.py +++ b/udemy_enroller_internet_explorer.py @@ -12,16 +12,7 @@ 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 +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() +redeem_courses(driver, settings) diff --git a/udemy_enroller_opera.py b/udemy_enroller_opera.py index 9cff2ba..76f78e8 100644 --- a/udemy_enroller_opera.py +++ b/udemy_enroller_opera.py @@ -12,15 +12,7 @@ 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 +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() +redeem_courses(driver, settings) diff --git a/udemy_enroller_vanilla.py b/udemy_enroller_vanilla.py index 2398d0e..415f2ba 100644 --- a/udemy_enroller_vanilla.py +++ b/udemy_enroller_vanilla.py @@ -18,19 +18,14 @@ 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 +) +# 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 -driver.maximize_window() # 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() +redeem_courses(driver, settings) From a958b00ef0f275ca53fb9db77ed9b4e2b2958d8e Mon Sep 17 00:00:00 2001 From: cullzie Date: Sat, 21 Nov 2020 22:29:13 +0000 Subject: [PATCH 06/44] Adding more unittests --- .coveragerc | 1 + tests/conftest.py | 1466 ++++++++++++++++++++++++++++++++ tests/core/test_settings.py | 93 +- tests/core/test_tutorialbar.py | 83 ++ udemy_enroller_firefox.py | 1 + 5 files changed, 1634 insertions(+), 10 deletions(-) create mode 100644 tests/core/test_tutorialbar.py diff --git a/.coveragerc b/.coveragerc index ccfedc8..4b41c8b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -3,3 +3,4 @@ show_missing = True skip_covered = True omit = tests/* + requirements.py diff --git a/tests/conftest.py b/tests/conftest.py index 5db1065..60daa09 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,3 +13,1469 @@ def 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/test_settings.py b/tests/core/test_settings.py index 47b277c..f1a6519 100644 --- a/tests/core/test_settings.py +++ b/tests/core/test_settings.py @@ -9,14 +9,30 @@ @pytest.mark.parametrize( "email,password,zip_code,languages,save,file_name", - [("test4@mail.com", "dskalksdl678", "12345", None, "Y", "test_settings1.yaml"), - ("test6@mail.com", "$6237556^^$", "12345", "English,French", "Y", "test_settings2.yaml"), - ("cultest8lzie@mail.com", "43223*&6", "12345", None, "N", "no_save_test_settings.yaml")], + [ + ("test4@mail.com", "dskalksdl678", "12345", None, "Y", "test_settings1.yaml"), + ( + "test6@mail.com", + "$6237556^^$", + "12345", + "English,French", + "Y", + "test_settings2.yaml", + ), + ( + "cultest8lzie@mail.com", + "43223*&6", + "12345", + None, + "N", + "no_save_test_settings.yaml", + ), + ], ids=( "create settings all languages and save", "create settings select languages and save", - "create settings all languages and don't save" - ) + "create settings all languages and don't save", + ), ) def test_settings(email, password, zip_code, languages, save, file_name): with mock.patch("builtins.input", side_effect=[email, zip_code, languages, save]): @@ -35,7 +51,11 @@ def test_settings(email, password, zip_code, languages, save, file_name): 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"]["languages"] == [] + if languages is None + else ",".join(languages) + ) # Load settings just created Settings(settings_path) else: @@ -44,14 +64,37 @@ def test_settings(email, password, zip_code, languages, save, file_name): @pytest.mark.parametrize( "email,password,zip_code,languages,save,file_name", - [("test9@mail.com", "uherwh834", "12345", None, "Y", "test_load_existing_settings1.yaml"), - ("test10@mail.com", "234sdfs", "None", "English", "Y", "test_load_existing_settings2.yaml"), - ("test11@mail.com", "frtuhrfty234", "788192", "French,German", "Y", "test_load_existing_settings3.yaml")], + [ + ( + "test9@mail.com", + "uherwh834", + "12345", + None, + "Y", + "test_load_existing_settings1.yaml", + ), + ( + "test10@mail.com", + "234sdfs", + "None", + "English", + "Y", + "test_load_existing_settings2.yaml", + ), + ( + "test11@mail.com", + "frtuhrfty234", + "788192", + "French,German", + "Y", + "test_load_existing_settings3.yaml", + ), + ], ids=( "load existing settings no languages", "load existing settings no zipcode", "load existing settings full", - ) + ), ) def test_load_existing_settings(email, password, zip_code, languages, save, file_name): with mock.patch("builtins.input", side_effect=[email, zip_code, languages, save]): @@ -65,3 +108,33 @@ def test_load_existing_settings(email, password, zip_code, languages, save, file assert settings.password == password assert settings.zip_code == zip_code assert settings.languages == [] if languages is None else languages + + +@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..79d5f46 --- /dev/null +++ b/tests/core/test_tutorialbar.py @@ -0,0 +1,83 @@ +import pytest + +from core import TutorialBarScraper + +from unittest import mock + + +@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) + assert links == 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/udemy_enroller_firefox.py b/udemy_enroller_firefox.py index c04a4e1..b131845 100644 --- a/udemy_enroller_firefox.py +++ b/udemy_enroller_firefox.py @@ -12,6 +12,7 @@ driver = webdriver.Firefox() # Maximizes the browser window since Udemy has a responsive design and the code only works +# in the maximized layout driver.maximize_window() redeem_courses(driver, settings) From f722d0088aed6d020c7cf12844715db8fb396634 Mon Sep 17 00:00:00 2001 From: cullzie Date: Sun, 22 Nov 2020 09:56:08 +0000 Subject: [PATCH 07/44] Fix settings tests after latest merge --- tests/core/test_settings.py | 60 ++++++++++++++++++++++++++++++++----- 1 file changed, 53 insertions(+), 7 deletions(-) diff --git a/tests/core/test_settings.py b/tests/core/test_settings.py index f1a6519..a9d9a26 100644 --- a/tests/core/test_settings.py +++ b/tests/core/test_settings.py @@ -8,22 +8,41 @@ @pytest.mark.parametrize( - "email,password,zip_code,languages,save,file_name", + "email,password,zip_code,languages,categories,save,file_name", [ - ("test4@mail.com", "dskalksdl678", "12345", None, "Y", "test_settings1.yaml"), + ( + "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", ), @@ -31,11 +50,14 @@ 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, save, file_name): - with mock.patch("builtins.input", side_effect=[email, zip_code, languages, 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) @@ -43,6 +65,7 @@ def test_settings(email, password, zip_code, languages, save, file_name): 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() @@ -56,6 +79,11 @@ def test_settings(email, password, zip_code, languages, save, file_name): 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: @@ -63,21 +91,32 @@ def test_settings(email, password, zip_code, languages, save, file_name): @pytest.mark.parametrize( - "email,password,zip_code,languages,save,file_name", + "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", ), @@ -86,18 +125,24 @@ def test_settings(email, password, zip_code, languages, save, file_name): "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, save, file_name): - with mock.patch("builtins.input", side_effect=[email, zip_code, languages, save]): +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) @@ -108,6 +153,7 @@ def test_load_existing_settings(email, password, zip_code, languages, save, file 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( From 2a892e29bd72511c637790bf3239bcac971e21e0 Mon Sep 17 00:00:00 2001 From: cullzie Date: Sun, 22 Nov 2020 16:31:50 +0000 Subject: [PATCH 08/44] Adding unittests for cache --- core/__init__.py | 1 + core/cache.py | 4 +- core/utils.py | 3 +- tests/conftest.py | 3 +- tests/core/test_cache.py | 193 +++++++++++++++++++++++++++++++++ tests/core/test_settings.py | 6 +- tests/core/test_tutorialbar.py | 4 +- 7 files changed, 204 insertions(+), 10 deletions(-) create mode 100644 tests/core/test_cache.py diff --git a/core/__init__.py b/core/__init__.py index 751a77d..9b4f6dc 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -1,3 +1,4 @@ from .settings import Settings from .tutorialbar import TutorialBarScraper from .udemy import UdemyActions +from .cache import CourseCache diff --git a/core/cache.py b/core/cache.py index 492c47f..e07c42a 100644 --- a/core/cache.py +++ b/core/cache.py @@ -8,8 +8,8 @@ class CourseCache(object): 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/utils.py b/core/utils.py index b6f6708..dd7afd3 100644 --- a/core/utils.py +++ b/core/utils.py @@ -5,8 +5,7 @@ ) from selenium.webdriver.remote.webdriver import WebDriver -from core import Settings, TutorialBarScraper, UdemyActions, exceptions -from core.cache import CourseCache +from core import Settings, TutorialBarScraper, UdemyActions, CourseCache, exceptions def _redeem_courses(driver: WebDriver, settings: Settings): diff --git a/tests/conftest.py b/tests/conftest.py index 60daa09..0f306f9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,8 @@ import os -import pytest import shutil +import pytest + @pytest.fixture(scope="session", autouse=True) def test_file_dir(): 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_settings.py b/tests/core/test_settings.py index a9d9a26..5fdcd12 100644 --- a/tests/core/test_settings.py +++ b/tests/core/test_settings.py @@ -1,11 +1,11 @@ import os +from unittest import mock + import pytest +from ruamel.yaml import YAML from core import Settings -from unittest import mock -from ruamel.yaml import YAML - @pytest.mark.parametrize( "email,password,zip_code,languages,categories,save,file_name", diff --git a/tests/core/test_tutorialbar.py b/tests/core/test_tutorialbar.py index 79d5f46..d3680f3 100644 --- a/tests/core/test_tutorialbar.py +++ b/tests/core/test_tutorialbar.py @@ -1,9 +1,9 @@ +from unittest import mock + import pytest from core import TutorialBarScraper -from unittest import mock - @pytest.mark.parametrize( "tutorialbar_course_page_link,tutorialbar_links,udemy_links", From 4a313e37347f9aac3a64bad8975952abc4af9267 Mon Sep 17 00:00:00 2001 From: fakeid30 Date: Fri, 27 Nov 2020 00:21:47 +0600 Subject: [PATCH 09/44] Update README --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 846ec18..1053a83 100644 --- a/README.md +++ b/README.md @@ -191,3 +191,7 @@ Take a look at our and help us on what you want or talk to us about your proposed changes. --- + +## Supporters + +Thanks to [Jetbrains](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. From a81ed8e69d41d894d22fff5304c1716479a3830c Mon Sep 17 00:00:00 2001 From: Fake ID Date: Fri, 27 Nov 2020 00:34:08 +0600 Subject: [PATCH 10/44] Added JetBrains --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1053a83..7827b3f 100644 --- a/README.md +++ b/README.md @@ -194,4 +194,6 @@ and help us on what you want or talk to us about your proposed changes. ## Supporters -Thanks to [Jetbrains](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. +[![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. From 7dc4d9c1d5559f7a3d56d73ad63a7930accedb6a Mon Sep 17 00:00:00 2001 From: Fake ID Date: Fri, 27 Nov 2020 00:34:43 +0600 Subject: [PATCH 11/44] Fixed typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7827b3f..94c5f68 100644 --- a/README.md +++ b/README.md @@ -192,7 +192,7 @@ and help us on what you want or talk to us about your proposed changes. --- -## Supporters +## Supporter [![JetBrains](https://i.imgur.com/h2R018M.jpg)](https://jetbrains.com/?from=udemy-free-course-enroller) From 15909939c02e084c8e9ddf5a08be8add10d3f6df Mon Sep 17 00:00:00 2001 From: cullzie Date: Fri, 27 Nov 2020 15:46:14 +0000 Subject: [PATCH 12/44] Bump version in pyproject --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7f376fa..54f9dc6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "automatic-udemy-course-enroller-get-paid-udemy-courses-for-free" -version = "0.2.2" +version = "0.3" description = "" authors = [""] From 7d5f1530ee584724cee43c8257119bbb76c5bc50 Mon Sep 17 00:00:00 2001 From: cullzie Date: Fri, 27 Nov 2020 16:35:01 +0000 Subject: [PATCH 13/44] Fix failing unittest --- tests/core/test_tutorialbar.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/core/test_tutorialbar.py b/tests/core/test_tutorialbar.py index d3680f3..0cc3929 100644 --- a/tests/core/test_tutorialbar.py +++ b/tests/core/test_tutorialbar.py @@ -39,7 +39,8 @@ def test_run( mock_get_course_links.assert_called_with(tutorialbar_course_page_link) mock_gather_udemy_course_links.assert_called_with(tutorialbar_links) - assert links == udemy_links + for link in links: + assert link in udemy_links @pytest.mark.parametrize( From 320f4f3d5aaf4ef99a5fea5a2a430f4bb707e809 Mon Sep 17 00:00:00 2001 From: Fake ID Date: Sat, 28 Nov 2020 13:44:43 +0600 Subject: [PATCH 14/44] Revert "Merge pull request #2 from aapatre/develop" From f7562141cd410a70688214829c7655ae47755635 Mon Sep 17 00:00:00 2001 From: fakeid30 Date: Sat, 28 Nov 2020 14:07:48 +0600 Subject: [PATCH 15/44] Updated README & gitignore, removed deepsource --- .deepsource.toml | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 .deepsource.toml diff --git a/.deepsource.toml b/.deepsource.toml deleted file mode 100644 index 25bc3d7..0000000 --- a/.deepsource.toml +++ /dev/null @@ -1,8 +0,0 @@ -version = 1 - -[[analyzers]] -name = "python" -enabled = true - - [analyzers.meta] - runtime_version = "3.x.x" From 332b729377e76601a7392523c73bdf561b426a70 Mon Sep 17 00:00:00 2001 From: fakeid30 Date: Sat, 28 Nov 2020 14:08:30 +0600 Subject: [PATCH 16/44] Updated README & Gitignore, removed deepsource --- .gitignore | 2 ++ README.md | 32 ++++++++++++-------------------- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/.gitignore b/.gitignore index 98dadcb..3faa195 100644 --- a/.gitignore +++ b/.gitignore @@ -234,3 +234,5 @@ poetry.lock # Ignore temporary test folder test_tmp/ + +.idea/poetry.xml diff --git a/README.md b/README.md index 529f716..81dcbf0 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:** @@ -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:** @@ -142,9 +138,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 +158,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 +169,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? From 6a11c2823ecf625dc55e724b29813590852f6066 Mon Sep 17 00:00:00 2001 From: fakeid30 Date: Sun, 29 Nov 2020 14:21:19 +0600 Subject: [PATCH 17/44] Update gitignore --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 3faa195..f926aa5 100644 --- a/.gitignore +++ b/.gitignore @@ -236,3 +236,7 @@ poetry.lock test_tmp/ .idea/poetry.xml + +#ignore deepsource.toml + +deepsource.toml \ No newline at end of file From 95e09bb25b2cefa27f99b14e9e594a9dc46fb9ce Mon Sep 17 00:00:00 2001 From: fakeid30 Date: Sun, 29 Nov 2020 14:50:19 +0600 Subject: [PATCH 18/44] fixed deepsource ignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index f926aa5..f9925bc 100644 --- a/.gitignore +++ b/.gitignore @@ -239,4 +239,4 @@ test_tmp/ #ignore deepsource.toml -deepsource.toml \ No newline at end of file +.deepsource.toml \ No newline at end of file From 5b814c40c8b7594cbd71a8dc7bf96836cf4a0ec8 Mon Sep 17 00:00:00 2001 From: Fake ID Date: Sun, 29 Nov 2020 15:11:02 +0600 Subject: [PATCH 19/44] Create .deepsource.toml --- .deepsource.toml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .deepsource.toml diff --git a/.deepsource.toml b/.deepsource.toml new file mode 100644 index 0000000..3ea082b --- /dev/null +++ b/.deepsource.toml @@ -0,0 +1,10 @@ +version = 1 + +test_patterns = ["*/tests/**"] + +[[analyzers]] +name = "python" +enabled = true + + [analyzers.meta] + runtime_version = "3.x.x" From 797eae3d9198f67e8cd02f68f25f99b9ad3252f9 Mon Sep 17 00:00:00 2001 From: Fake ID Date: Sun, 29 Nov 2020 15:13:22 +0600 Subject: [PATCH 20/44] Update settings.py --- core/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/settings.py b/core/settings.py index ca276e0..ff84a50 100644 --- a/core/settings.py +++ b/core/settings.py @@ -148,7 +148,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"] = { From d01b063064a4c3c3e334ce4320a809904b148aef Mon Sep 17 00:00:00 2001 From: Fake ID Date: Sun, 29 Nov 2020 15:15:32 +0600 Subject: [PATCH 21/44] Unnecessary else / elif used after return --- core/udemy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/udemy.py b/core/udemy.py index 854a11c..1005afd 100644 --- a/core/udemy.py +++ b/core/udemy.py @@ -60,7 +60,7 @@ 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 From a106c4f103f26a2ca0c54c87c16c3d18a0f94da2 Mon Sep 17 00:00:00 2001 From: Fake ID Date: Sun, 29 Nov 2020 15:16:54 +0600 Subject: [PATCH 22/44] Useless inheritance from object --- core/cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/cache.py b/core/cache.py index e07c42a..fbc398e 100644 --- a/core/cache.py +++ b/core/cache.py @@ -3,7 +3,7 @@ import os -class CourseCache(object): +class CourseCache(): """ Basic cache to keep details on courses already scraped """ From 06f4fa959b3adcd3d095ca188d53d029704003b8 Mon Sep 17 00:00:00 2001 From: Fake ID Date: Sun, 29 Nov 2020 15:18:47 +0600 Subject: [PATCH 23/44] Unnecessary else/elif used after raise --- core/udemy.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/udemy.py b/core/udemy.py index 1005afd..7c3a3e6 100644 --- a/core/udemy.py +++ b/core/udemy.py @@ -62,8 +62,7 @@ def login(self, is_retry=False) -> None: return 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 From 3a6c1ccd5f8bff78eb4d84160aa869036c6d3750 Mon Sep 17 00:00:00 2001 From: Fake ID Date: Mon, 30 Nov 2020 00:46:39 +0600 Subject: [PATCH 24/44] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 81dcbf0..d1122d4 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ get all the requirements installed in one go. Similar instructions applies for p - 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 From ae7c0f7c5a3704c267669e275a9408242a059498 Mon Sep 17 00:00:00 2001 From: cullzie Date: Mon, 30 Nov 2020 13:41:40 +0000 Subject: [PATCH 25/44] Adding max_pages argument to scripts --- core/driver_manager.py | 73 +++++++++++++++++++++++++++++ core/tutorialbar.py | 18 ++++++- core/utils.py | 12 +++-- udemy_enroller.py | 44 +++++++++++++++++ udemy_enroller_chrome.py | 33 ++----------- udemy_enroller_chromium.py | 20 ++------ udemy_enroller_edge.py | 17 ++----- udemy_enroller_firefox.py | 17 ++----- udemy_enroller_internet_explorer.py | 17 ++----- udemy_enroller_opera.py | 17 ++----- udemy_enroller_vanilla.py | 2 +- 11 files changed, 167 insertions(+), 103 deletions(-) create mode 100644 core/driver_manager.py create mode 100644 udemy_enroller.py diff --git a/core/driver_manager.py b/core/driver_manager.py new file mode 100644 index 0000000..96125cb --- /dev/null +++ b/core/driver_manager.py @@ -0,0 +1,73 @@ +from selenium import webdriver +from webdriver_manager.chrome import ChromeDriverManager +from webdriver_manager.microsoft import EdgeChromiumDriverManager +from webdriver_manager.firefox import GeckoDriverManager +from webdriver_manager.opera import OperaDriverManager +from webdriver_manager.microsoft import IEDriverManager +from webdriver_manager.utils import ChromeType + + +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 ("chrome", "google-chrome"): + 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 ("chromium",): + self.driver = webdriver.Chrome( + ChromeDriverManager(chrome_type=ChromeType.CHROMIUM).install() + ) + elif self.browser.lower() in ("edge",): + self.driver = webdriver.Edge(EdgeChromiumDriverManager().install()) + elif self.browser.lower() in ("firefox", "ff"): + self.driver = webdriver.Firefox( + executable_path=GeckoDriverManager().install() + ) + elif self.browser.lower() in ("opera",): + self.driver = webdriver.Opera( + executable_path=OperaDriverManager().install() + ) + elif self.browser.lower() in ("internet_explorer", "ie"): + 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: + """ + 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" + ) + options = Options() + # 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") + print("This is a CI run") + return options diff --git a/core/tutorialbar.py b/core/tutorialbar.py index c8e4fd9..6ff69f4 100644 --- a/core/tutorialbar.py +++ b/core/tutorialbar.py @@ -13,10 +13,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: """ @@ -39,6 +40,21 @@ def run(self) -> List: 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: + print(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 diff --git a/core/utils.py b/core/utils.py index dd7afd3..2caee23 100644 --- a/core/utils.py +++ b/core/utils.py @@ -8,17 +8,20 @@ from core import Settings, TutorialBarScraper, UdemyActions, CourseCache, exceptions -def _redeem_courses(driver: WebDriver, settings: Settings): +def _redeem_courses(driver: WebDriver, settings: Settings, max_pages): """ Method to scrape courses from tutorialbar.com and enroll in them on udemy :return: """ cache = CourseCache() - tb_scraper = TutorialBarScraper() + tb_scraper = TutorialBarScraper(max_pages) udemy_actions = UdemyActions(driver, settings) udemy_actions.login() # login once outside while loop 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: @@ -52,16 +55,17 @@ def _redeem_courses(driver: WebDriver, settings: Settings): print("Moving on to the next page of the course list on tutorialbar.com") -def redeem_courses(driver, settings) -> None: +def redeem_courses(driver, settings, max_pages) -> None: """ Wrapper of _redeem_courses so we always close browser on completion :param driver: :param settings: + :param max_pages: :return: """ try: - _redeem_courses(driver, settings) + _redeem_courses(driver, settings, max_pages) finally: print("Closing browser") driver.quit() diff --git a/udemy_enroller.py b/udemy_enroller.py new file mode 100644 index 0000000..0d898f8 --- /dev/null +++ b/udemy_enroller.py @@ -0,0 +1,44 @@ +# 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 core.driver_manager import DriverManager + +from core import Settings +from core.utils import redeem_courses + + +def run(browser, max_pages): + settings = Settings() + dm = DriverManager(browser=browser, is_ci_build=settings.is_ci_build) + redeem_courses(dm.driver, settings, max_pages) + + +def parse_args(browser=None): + parser = argparse.ArgumentParser(description="Udemy Enroller") + + parser.add_argument( + "--browser", + type=str, + default=browser, + help="Browser to use for Udemy Enroller", + ) + parser.add_argument( + "--max_pages", + type=int, + default=None, + help="Max pages to scrape from tutorialbar.com", + ) + + args = parser.parse_args() + + if args.browser is None: + parser.print_help() + else: + return args + + +if __name__ == "__main__": + args = parse_args() + if args: + run(args.browser, args.max_pages) diff --git a/udemy_enroller_chrome.py b/udemy_enroller_chrome.py index 25b4a27..cb0e9c5 100644 --- a/udemy_enroller_chrome.py +++ b/udemy_enroller_chrome.py @@ -1,34 +1,9 @@ # 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 udemy_enroller import run, parse_args -from core import Settings -from core.utils import redeem_courses -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" - ) - 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 -# in the maximized layout -driver.maximize_window() - -redeem_courses(driver, settings) +if __name__ == "__main__": + args = parse_args("chrome") + run(args.browser, args.max_pages) diff --git a/udemy_enroller_chromium.py b/udemy_enroller_chromium.py index 024434c..ae63d32 100644 --- a/udemy_enroller_chromium.py +++ b/udemy_enroller_chromium.py @@ -1,21 +1,9 @@ # 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 udemy_enroller import run, parse_args -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 -# in the maximized layout -driver.maximize_window() - -redeem_courses(driver, settings) +if __name__ == "__main__": + args = parse_args("chromium") + run(args.browser, args.max_pages) diff --git a/udemy_enroller_edge.py b/udemy_enroller_edge.py index c026d37..e0c16e6 100644 --- a/udemy_enroller_edge.py +++ b/udemy_enroller_edge.py @@ -1,18 +1,9 @@ # 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 udemy_enroller import run, parse_args -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 -# in the maximized layout -driver.maximize_window() - -redeem_courses(driver, settings) +if __name__ == "__main__": + args = parse_args("edge") + run(args.browser, args.max_pages) diff --git a/udemy_enroller_firefox.py b/udemy_enroller_firefox.py index e1d531d..e565a59 100644 --- a/udemy_enroller_firefox.py +++ b/udemy_enroller_firefox.py @@ -2,18 +2,9 @@ # 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 +from udemy_enroller import run, parse_args -from core import Settings -from core.utils import redeem_courses -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() - -redeem_courses(driver, settings) +if __name__ == "__main__": + args = parse_args("firefox") + run(args.browser, args.max_pages) diff --git a/udemy_enroller_internet_explorer.py b/udemy_enroller_internet_explorer.py index 9dcf44a..081e8f4 100644 --- a/udemy_enroller_internet_explorer.py +++ b/udemy_enroller_internet_explorer.py @@ -1,18 +1,9 @@ # 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 udemy_enroller import run, parse_args -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 -# in the maximized layout -driver.maximize_window() - -redeem_courses(driver, settings) +if __name__ == "__main__": + args = parse_args("internet_explorer") + run(args.browser, args.max_pages) diff --git a/udemy_enroller_opera.py b/udemy_enroller_opera.py index 76f78e8..07036df 100644 --- a/udemy_enroller_opera.py +++ b/udemy_enroller_opera.py @@ -1,18 +1,9 @@ # 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 udemy_enroller import run, parse_args -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 -# in the maximized layout -driver.maximize_window() - -redeem_courses(driver, settings) +if __name__ == "__main__": + args = parse_args("opera") + run(args.browser, args.max_pages) diff --git a/udemy_enroller_vanilla.py b/udemy_enroller_vanilla.py index 415f2ba..b024fba 100644 --- a/udemy_enroller_vanilla.py +++ b/udemy_enroller_vanilla.py @@ -28,4 +28,4 @@ # in the maximized layout driver.maximize_window() -redeem_courses(driver, settings) +redeem_courses(driver, settings, max_pages=None) From d883875f58ff9ba53dbeaf5a078ad97333f94f1e Mon Sep 17 00:00:00 2001 From: cullzie Date: Mon, 30 Nov 2020 13:49:02 +0000 Subject: [PATCH 26/44] Allow use of manual webdriver path --- udemy_enroller.py | 12 +++++++----- udemy_enroller_vanilla.py | 39 +++++++++++++++++++++------------------ 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/udemy_enroller.py b/udemy_enroller.py index 0d898f8..8e3ea4f 100644 --- a/udemy_enroller.py +++ b/udemy_enroller.py @@ -8,13 +8,15 @@ from core.utils import redeem_courses -def run(browser, max_pages): +def run(browser, max_pages, driver=None): settings = Settings() - dm = DriverManager(browser=browser, is_ci_build=settings.is_ci_build) - redeem_courses(dm.driver, settings, max_pages) + if driver is None: + dm = DriverManager(browser=browser, is_ci_build=settings.is_ci_build) + driver = dm.driver + redeem_courses(driver, settings, max_pages) -def parse_args(browser=None): +def parse_args(browser=None, use_manual_driver=False): parser = argparse.ArgumentParser(description="Udemy Enroller") parser.add_argument( @@ -32,7 +34,7 @@ def parse_args(browser=None): args = parser.parse_args() - if args.browser is None: + if args.browser is None and not use_manual_driver: parser.print_help() else: return args diff --git a/udemy_enroller_vanilla.py b/udemy_enroller_vanilla.py index b024fba..35b1b56 100644 --- a/udemy_enroller_vanilla.py +++ b/udemy_enroller_vanilla.py @@ -4,28 +4,31 @@ from selenium import webdriver from core import Settings -from core.utils import redeem_courses +from udemy_enroller import run, parse_args -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 -) -# 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() - -redeem_courses(driver, settings, max_pages=None) + +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, driver=driver) From 53e09254a56ca86e5f3fd16c2c98beb1bcd9d1ee Mon Sep 17 00:00:00 2001 From: cullzie Date: Mon, 30 Nov 2020 14:11:45 +0000 Subject: [PATCH 27/44] Add valid browser options to argparse --- core/driver_manager.py | 28 ++++++++++++++++++++++------ udemy_enroller.py | 3 ++- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/core/driver_manager.py b/core/driver_manager.py index 96125cb..317a51e 100644 --- a/core/driver_manager.py +++ b/core/driver_manager.py @@ -6,6 +6,22 @@ from webdriver_manager.microsoft import IEDriverManager 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) +) + class DriverManager: def __init__(self, browser: str, is_ci_build: bool = False): @@ -22,27 +38,27 @@ def _init_driver(self): :return: None """ - if self.browser.lower() in ("chrome", "google-chrome"): + 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 ("chromium",): + elif self.browser.lower() in VALID_CHROMIUM_STRINGS: self.driver = webdriver.Chrome( ChromeDriverManager(chrome_type=ChromeType.CHROMIUM).install() ) - elif self.browser.lower() in ("edge",): + elif self.browser.lower() in VALID_EDGE_STRINGS: self.driver = webdriver.Edge(EdgeChromiumDriverManager().install()) - elif self.browser.lower() in ("firefox", "ff"): + elif self.browser.lower() in VALID_FIREFOX_STRINGS: self.driver = webdriver.Firefox( executable_path=GeckoDriverManager().install() ) - elif self.browser.lower() in ("opera",): + elif self.browser.lower() in VALID_OPERA_STRINGS: self.driver = webdriver.Opera( executable_path=OperaDriverManager().install() ) - elif self.browser.lower() in ("internet_explorer", "ie"): + elif self.browser.lower() in VALID_INTERNET_EXPLORER_STRINGS: self.driver = webdriver.Ie(IEDriverManager().install()) else: raise ValueError("No matching browser found") diff --git a/udemy_enroller.py b/udemy_enroller.py index 8e3ea4f..7dd728d 100644 --- a/udemy_enroller.py +++ b/udemy_enroller.py @@ -2,7 +2,7 @@ # 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 core.driver_manager import DriverManager +from core.driver_manager import DriverManager, ALL_VALID_BROWSER_STRINGS from core import Settings from core.utils import redeem_courses @@ -23,6 +23,7 @@ def parse_args(browser=None, use_manual_driver=False): "--browser", type=str, default=browser, + choices=ALL_VALID_BROWSER_STRINGS, help="Browser to use for Udemy Enroller", ) parser.add_argument( From 94817b9a3365fb6a629ac4e8e053742c67a51f6f Mon Sep 17 00:00:00 2001 From: cullzie Date: Mon, 30 Nov 2020 14:26:02 +0000 Subject: [PATCH 28/44] Changing parser arg to max-pages --- udemy_enroller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/udemy_enroller.py b/udemy_enroller.py index 7dd728d..aa07bd1 100644 --- a/udemy_enroller.py +++ b/udemy_enroller.py @@ -27,7 +27,7 @@ def parse_args(browser=None, use_manual_driver=False): help="Browser to use for Udemy Enroller", ) parser.add_argument( - "--max_pages", + "--max-pages", type=int, default=None, help="Max pages to scrape from tutorialbar.com", From 2dbbcf4c7ca63c0b8164b9ac370a2276d12107c5 Mon Sep 17 00:00:00 2001 From: cullzie Date: Mon, 30 Nov 2020 14:55:09 +0000 Subject: [PATCH 29/44] Adding more OS tests --- .github/workflows/python-package.yml | 15 ++++++++++++++- pytest.ini | 2 +- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index de38f45..8b023b8 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -9,9 +9,10 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} strategy: matrix: + os: [ubuntu-latest, macOS-latest] python-version: [3.8] steps: @@ -20,6 +21,18 @@ 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 diff --git a/pytest.ini b/pytest.ini index d8a10e3..ba21a73 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,5 +1,5 @@ [pytest] addopts = - --cov=. --cov-report html --cov-report term + --cov=. --cov-report xml --cov-report term testpaths = tests From 9685d25f3876f5dcb583e89d318c6c8c2653efaf Mon Sep 17 00:00:00 2001 From: cullzie Date: Mon, 30 Nov 2020 15:44:51 +0000 Subject: [PATCH 30/44] Black and isort --- core/__init__.py | 2 +- core/cache.py | 2 +- core/driver_manager.py | 3 +-- core/tutorialbar.py | 5 +++-- core/utils.py | 2 +- udemy_enroller.py | 2 +- udemy_enroller_chrome.py | 3 +-- udemy_enroller_chromium.py | 3 +-- udemy_enroller_edge.py | 3 +-- udemy_enroller_firefox.py | 3 +-- udemy_enroller_internet_explorer.py | 3 +-- udemy_enroller_opera.py | 3 +-- udemy_enroller_vanilla.py | 2 +- 13 files changed, 15 insertions(+), 21 deletions(-) diff --git a/core/__init__.py b/core/__init__.py index 9b4f6dc..19677b7 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -1,4 +1,4 @@ +from .cache import CourseCache from .settings import Settings from .tutorialbar import TutorialBarScraper from .udemy import UdemyActions -from .cache import CourseCache diff --git a/core/cache.py b/core/cache.py index fbc398e..c3683e0 100644 --- a/core/cache.py +++ b/core/cache.py @@ -3,7 +3,7 @@ import os -class CourseCache(): +class CourseCache: """ Basic cache to keep details on courses already scraped """ diff --git a/core/driver_manager.py b/core/driver_manager.py index 317a51e..86094bc 100644 --- a/core/driver_manager.py +++ b/core/driver_manager.py @@ -1,9 +1,8 @@ from selenium import webdriver from webdriver_manager.chrome import ChromeDriverManager -from webdriver_manager.microsoft import EdgeChromiumDriverManager from webdriver_manager.firefox import GeckoDriverManager +from webdriver_manager.microsoft import EdgeChromiumDriverManager, IEDriverManager from webdriver_manager.opera import OperaDriverManager -from webdriver_manager.microsoft import IEDriverManager from webdriver_manager.utils import ChromeType VALID_FIREFOX_STRINGS = {"ff", "firefox"} diff --git a/core/tutorialbar.py b/core/tutorialbar.py index 6ff69f4..206ad2e 100644 --- a/core/tutorialbar.py +++ b/core/tutorialbar.py @@ -51,10 +51,11 @@ def script_should_run(self) -> bool: if self.max_pages is not None: should_run = self.max_pages > self.current_page if not should_run: - print(f"Stopping loop. We have reached max number of pages to scrape: {self.max_pages}") + print( + 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 diff --git a/core/utils.py b/core/utils.py index 2caee23..48f58f4 100644 --- a/core/utils.py +++ b/core/utils.py @@ -5,7 +5,7 @@ ) from selenium.webdriver.remote.webdriver import WebDriver -from core import Settings, TutorialBarScraper, UdemyActions, CourseCache, exceptions +from core import CourseCache, Settings, TutorialBarScraper, UdemyActions, exceptions def _redeem_courses(driver: WebDriver, settings: Settings, max_pages): diff --git a/udemy_enroller.py b/udemy_enroller.py index aa07bd1..46dd2f0 100644 --- a/udemy_enroller.py +++ b/udemy_enroller.py @@ -2,9 +2,9 @@ # 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 core.driver_manager import DriverManager, ALL_VALID_BROWSER_STRINGS from core import Settings +from core.driver_manager import ALL_VALID_BROWSER_STRINGS, DriverManager from core.utils import redeem_courses diff --git a/udemy_enroller_chrome.py b/udemy_enroller_chrome.py index cb0e9c5..6db9cdf 100644 --- a/udemy_enroller_chrome.py +++ b/udemy_enroller_chrome.py @@ -1,8 +1,7 @@ # 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 udemy_enroller import run, parse_args - +from udemy_enroller import parse_args, run if __name__ == "__main__": args = parse_args("chrome") diff --git a/udemy_enroller_chromium.py b/udemy_enroller_chromium.py index ae63d32..a1ee781 100644 --- a/udemy_enroller_chromium.py +++ b/udemy_enroller_chromium.py @@ -1,8 +1,7 @@ # 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 udemy_enroller import run, parse_args - +from udemy_enroller import parse_args, run if __name__ == "__main__": args = parse_args("chromium") diff --git a/udemy_enroller_edge.py b/udemy_enroller_edge.py index e0c16e6..4955f7d 100644 --- a/udemy_enroller_edge.py +++ b/udemy_enroller_edge.py @@ -1,8 +1,7 @@ # 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 udemy_enroller import run, parse_args - +from udemy_enroller import parse_args, run if __name__ == "__main__": args = parse_args("edge") diff --git a/udemy_enroller_firefox.py b/udemy_enroller_firefox.py index e565a59..63a76e9 100644 --- a/udemy_enroller_firefox.py +++ b/udemy_enroller_firefox.py @@ -2,8 +2,7 @@ # 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 udemy_enroller import run, parse_args - +from udemy_enroller import parse_args, run if __name__ == "__main__": args = parse_args("firefox") diff --git a/udemy_enroller_internet_explorer.py b/udemy_enroller_internet_explorer.py index 081e8f4..bfe934b 100644 --- a/udemy_enroller_internet_explorer.py +++ b/udemy_enroller_internet_explorer.py @@ -1,8 +1,7 @@ # 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 udemy_enroller import run, parse_args - +from udemy_enroller import parse_args, run if __name__ == "__main__": args = parse_args("internet_explorer") diff --git a/udemy_enroller_opera.py b/udemy_enroller_opera.py index 07036df..c673be4 100644 --- a/udemy_enroller_opera.py +++ b/udemy_enroller_opera.py @@ -1,8 +1,7 @@ # 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 udemy_enroller import run, parse_args - +from udemy_enroller import parse_args, run if __name__ == "__main__": args = parse_args("opera") diff --git a/udemy_enroller_vanilla.py b/udemy_enroller_vanilla.py index 35b1b56..18e3a65 100644 --- a/udemy_enroller_vanilla.py +++ b/udemy_enroller_vanilla.py @@ -4,7 +4,7 @@ from selenium import webdriver from core import Settings -from udemy_enroller import run, parse_args +from udemy_enroller import parse_args, run """### **Enter the path/location of your webdriver** By default, the webdriver for Microsoft Edge browser has been chosen in the code below. From 33049842bf8535c838721fa8a90061009b83aa5a Mon Sep 17 00:00:00 2001 From: cullzie Date: Mon, 30 Nov 2020 16:28:48 +0000 Subject: [PATCH 31/44] Updating docs and README --- README.md | 14 +++++++++++--- core/utils.py | 19 ++++++++++++++----- udemy_enroller.py | 23 +++++++++++++++++++++-- 3 files changed, 46 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index d1122d4..85cf490 100644 --- a/README.md +++ b/README.md @@ -104,10 +104,18 @@ get all the requirements installed in one go. Similar instructions applies for p - 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 -4 . The bot starts scraping the course links from the first **All Courses** page +4 . Run the chosen script in terminal like so: +- `python udemy_enroller_firefox.py` + + 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. diff --git a/core/utils.py b/core/utils.py index 48f58f4..3327a59 100644 --- a/core/utils.py +++ b/core/utils.py @@ -1,3 +1,5 @@ +from typing import Union + from selenium.common.exceptions import ( NoSuchElementException, TimeoutException, @@ -8,10 +10,15 @@ from core import CourseCache, Settings, TutorialBarScraper, UdemyActions, exceptions -def _redeem_courses(driver: WebDriver, settings: Settings, max_pages): +def _redeem_courses( + driver: WebDriver, settings: Settings, max_pages: Union[int, None] +) -> 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 :return: """ cache = CourseCache() @@ -55,13 +62,15 @@ def _redeem_courses(driver: WebDriver, settings: Settings, max_pages): print("Moving on to the next page of the course list on tutorialbar.com") -def redeem_courses(driver, settings, max_pages) -> None: +def redeem_courses( + driver: WebDriver, settings: Settings, max_pages: Union[int, None] +) -> None: """ Wrapper of _redeem_courses so we always close browser on completion - :param driver: - :param settings: - :param max_pages: + :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 :return: """ try: diff --git a/udemy_enroller.py b/udemy_enroller.py index 46dd2f0..3cc11bb 100644 --- a/udemy_enroller.py +++ b/udemy_enroller.py @@ -2,13 +2,25 @@ # 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 Settings from core.driver_manager import ALL_VALID_BROWSER_STRINGS, DriverManager from core.utils import redeem_courses -def run(browser, max_pages, driver=None): +def run(browser: str, max_pages: Union[int, None], 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 WebDriver driver: + :return: + """ settings = Settings() if driver is None: dm = DriverManager(browser=browser, is_ci_build=settings.is_ci_build) @@ -16,7 +28,14 @@ def run(browser, max_pages, driver=None): redeem_courses(driver, settings, max_pages) -def parse_args(browser=None, use_manual_driver=False): +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( From eff47589e5338d51cb7f6c0a24f6042b7603657e Mon Sep 17 00:00:00 2001 From: cullzie Date: Mon, 30 Nov 2020 16:01:50 +0000 Subject: [PATCH 32/44] Added logging --- core/__init__.py | 6 +++++- core/settings.py | 15 +++++++++------ core/tutorialbar.py | 11 +++++++---- core/udemy.py | 15 ++++++++++----- core/utils.py | 33 ++++++++++++++++++--------------- logconfig.ini | 36 ++++++++++++++++++++++++++++++++++++ udemy_enroller_chrome.py | 6 +++++- 7 files changed, 90 insertions(+), 32 deletions(-) create mode 100644 logconfig.ini diff --git a/core/__init__.py b/core/__init__.py index 9b4f6dc..6d97eb2 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -1,4 +1,8 @@ +import logging.config + +from .cache import CourseCache from .settings import Settings from .tutorialbar import TutorialBarScraper from .udemy import UdemyActions -from .cache import CourseCache + +logging.config.fileConfig("logconfig.ini", disable_existing_loggers=False) diff --git a/core/settings.py b/core/settings.py index ca276e0..be8f65e 100644 --- a/core/settings.py +++ b/core/settings.py @@ -1,10 +1,13 @@ 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: """ @@ -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 @@ -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..d284c3d 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: """ @@ -25,17 +28,17 @@ 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 @@ -60,7 +63,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..1d4164f 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): """ @@ -89,7 +92,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 +111,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 +126,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,7 +179,9 @@ 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 # Hit the final Enroll now button @@ -193,7 +198,7 @@ def redeem(self, url: str) -> str: .text ) - 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 dd7afd3..a233084 100644 --- a/core/utils.py +++ b/core/utils.py @@ -1,3 +1,5 @@ +import logging + from selenium.common.exceptions import ( NoSuchElementException, TimeoutException, @@ -5,7 +7,9 @@ ) from selenium.webdriver.remote.webdriver import WebDriver -from core import Settings, TutorialBarScraper, UdemyActions, CourseCache, exceptions +from core import CourseCache, Settings, TutorialBarScraper, UdemyActions, exceptions + +logger = logging.getLogger("udemy_enroller") def _redeem_courses(driver: WebDriver, settings: Settings): @@ -27,29 +31,28 @@ def _redeem_courses(driver: WebDriver, settings: Settings): status = udemy_actions.redeem(course_link) cache.add(course_link, status) else: - print(f"In cache: {course_link}") + logger.info(f"In cache: {course_link}") 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: - print("Exiting the script") + 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: - print("We have attempted to subscribe to 1 udemy course") - print("Ending test") + 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 redeem_courses(driver, settings) -> None: @@ -63,5 +66,5 @@ def redeem_courses(driver, settings) -> None: try: _redeem_courses(driver, settings) finally: - print("Closing browser") + 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/udemy_enroller_chrome.py b/udemy_enroller_chrome.py index 25b4a27..a1d7004 100644 --- a/udemy_enroller_chrome.py +++ b/udemy_enroller_chrome.py @@ -1,12 +1,16 @@ # 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 logging + from selenium import webdriver from webdriver_manager.chrome import ChromeDriverManager from core import Settings from core.utils import redeem_courses +logger = logging.getLogger("udemy_enroller") + settings = Settings() chrome_options = None @@ -23,7 +27,7 @@ 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") + logger.info("This is a CI run") driver = webdriver.Chrome(ChromeDriverManager().install(), options=chrome_options) From 0207f453699f92f7c4c69810b3cfe0be524733ad Mon Sep 17 00:00:00 2001 From: cullzie Date: Tue, 1 Dec 2020 20:39:40 +0000 Subject: [PATCH 33/44] Adding unittests --- core/__init__.py | 1 + core/driver_manager.py | 5 +- tests/core/test_driver_manager.py | 120 ++++++++++++++++++++++++++++++ tests/test_udemy_enroller.py | 50 +++++++++++++ udemy_enroller.py | 3 +- 5 files changed, 174 insertions(+), 5 deletions(-) create mode 100644 tests/core/test_driver_manager.py create mode 100644 tests/test_udemy_enroller.py diff --git a/core/__init__.py b/core/__init__.py index 19677b7..e679ead 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -1,4 +1,5 @@ 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 diff --git a/core/driver_manager.py b/core/driver_manager.py index 86094bc..756868d 100644 --- a/core/driver_manager.py +++ b/core/driver_manager.py @@ -1,4 +1,5 @@ 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 @@ -72,14 +73,12 @@ def _build_ci_options_chrome(): :return: """ - 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" ) - options = Options() + options = ChromeOptions() # We need to run headless when using github CI options.add_argument("--headless") options.add_argument("user-agent={0}".format(user_agent)) 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/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 index 3cc11bb..65c8164 100644 --- a/udemy_enroller.py +++ b/udemy_enroller.py @@ -7,8 +7,7 @@ from selenium.webdriver.remote.webdriver import WebDriver -from core import Settings -from core.driver_manager import ALL_VALID_BROWSER_STRINGS, DriverManager +from core import ALL_VALID_BROWSER_STRINGS, DriverManager, Settings from core.utils import redeem_courses From bc24751ad62b865a5798874e0d42b50e8eb93266 Mon Sep 17 00:00:00 2001 From: fakeid30 Date: Wed, 2 Dec 2020 14:04:29 +0600 Subject: [PATCH 34/44] Added schedule run to the CI workflow --- .github/workflows/python-package.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 8b023b8..787034b 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -5,6 +5,9 @@ name: CI Build on: workflow_dispatch: + schedule: + # Runs at 12am IST + - cron: '30 18 * * *' jobs: build: From 30b6c389d2c80db3a137bd0e6c7760a32a9b50be Mon Sep 17 00:00:00 2001 From: fakeid30 Date: Wed, 2 Dec 2020 14:10:51 +0600 Subject: [PATCH 35/44] removed comment from workflow --- .github/workflows/python-package.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 787034b..317bdfd 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -6,7 +6,6 @@ name: CI Build on: workflow_dispatch: schedule: - # Runs at 12am IST - cron: '30 18 * * *' jobs: From 33e4d5d3d89deed5b6b13b1c824b3f78400b684f Mon Sep 17 00:00:00 2001 From: fakeid30 Date: Wed, 2 Dec 2020 14:10:51 +0600 Subject: [PATCH 36/44] Revert "removed comment from workflow" This reverts commit 30b6c389d2c80db3a137bd0e6c7760a32a9b50be. --- .github/workflows/python-package.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 317bdfd..787034b 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -6,6 +6,7 @@ name: CI Build on: workflow_dispatch: schedule: + # Runs at 12am IST - cron: '30 18 * * *' jobs: From 66cc689b9d0f9b920813381e7b17eb7cc24e9e52 Mon Sep 17 00:00:00 2001 From: cullzie Date: Wed, 2 Dec 2020 08:49:30 +0000 Subject: [PATCH 37/44] Removing final print statements after merge --- core/__init__.py | 1 + core/driver_manager.py | 7 ++++++- core/tutorialbar.py | 2 +- core/utils.py | 1 - 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/core/__init__.py b/core/__init__.py index cec74bd..cb59e9a 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -1,4 +1,5 @@ import logging.config + from .cache import CourseCache from .driver_manager import ALL_VALID_BROWSER_STRINGS, DriverManager from .settings import Settings diff --git a/core/driver_manager.py b/core/driver_manager.py index 756868d..b2b7506 100644 --- a/core/driver_manager.py +++ b/core/driver_manager.py @@ -1,3 +1,5 @@ +import logging + from selenium import webdriver from selenium.webdriver.chrome.options import Options as ChromeOptions from webdriver_manager.chrome import ChromeDriverManager @@ -23,6 +25,9 @@ ) +logger = logging.getLogger("udemy_enroller") + + class DriverManager: def __init__(self, browser: str, is_ci_build: bool = False): self.driver = None @@ -83,5 +88,5 @@ def _build_ci_options_chrome(): options.add_argument("--headless") options.add_argument("user-agent={0}".format(user_agent)) options.add_argument("--window-size=1325x744") - print("This is a CI run") + logger.info("This is a CI run") return options diff --git a/core/tutorialbar.py b/core/tutorialbar.py index a177ea0..9eea724 100644 --- a/core/tutorialbar.py +++ b/core/tutorialbar.py @@ -54,7 +54,7 @@ def script_should_run(self) -> bool: if self.max_pages is not None: should_run = self.max_pages > self.current_page if not should_run: - print( + logger.info( f"Stopping loop. We have reached max number of pages to scrape: {self.max_pages}" ) return should_run diff --git a/core/utils.py b/core/utils.py index 9a3f95f..79c2a2a 100644 --- a/core/utils.py +++ b/core/utils.py @@ -10,7 +10,6 @@ from core import CourseCache, Settings, TutorialBarScraper, UdemyActions, exceptions - logger = logging.getLogger("udemy_enroller") From 5b42c7367981d01870df1d76b138254e61d63977 Mon Sep 17 00:00:00 2001 From: cullzie Date: Wed, 2 Dec 2020 11:10:06 +0000 Subject: [PATCH 38/44] Adding exit script after X cache hits in a row --- core/utils.py | 39 ++++++++++++++++++++++++++--- udemy_enroller.py | 18 ++++++++++--- udemy_enroller_chrome.py | 2 +- udemy_enroller_chromium.py | 2 +- udemy_enroller_edge.py | 2 +- udemy_enroller_firefox.py | 2 +- udemy_enroller_internet_explorer.py | 2 +- udemy_enroller_opera.py | 2 +- udemy_enroller_vanilla.py | 2 +- 9 files changed, 58 insertions(+), 13 deletions(-) diff --git a/core/utils.py b/core/utils.py index 79c2a2a..54e6a04 100644 --- a/core/utils.py +++ b/core/utils.py @@ -14,7 +14,10 @@ def _redeem_courses( - driver: WebDriver, settings: Settings, max_pages: Union[int, None] + 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 @@ -22,12 +25,16 @@ def _redeem_courses( :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(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(): @@ -39,8 +46,12 @@ def _redeem_courses( 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: logger.info(f"In cache: {course_link}") + # Increment the cache hit count since this link is in the cache + current_cache_hits += 1 except NoSuchElementException as e: logger.error(e) except TimeoutException: @@ -61,11 +72,32 @@ def _redeem_courses( logger.info("Ending test") return + # Exit the loop if we have reached the cache hit limit + if _reached_cache_hit_limit(cache_hit_limit, current_cache_hits): + return + 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] + 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 @@ -73,10 +105,11 @@ def redeem_courses( :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) + _redeem_courses(driver, settings, max_pages, cache_hit_limit) finally: logger.info("Closing browser") driver.quit() diff --git a/udemy_enroller.py b/udemy_enroller.py index 65c8164..5e68366 100644 --- a/udemy_enroller.py +++ b/udemy_enroller.py @@ -11,12 +11,18 @@ from core.utils import redeem_courses -def run(browser: str, max_pages: Union[int, None], driver: WebDriver = None): +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: """ @@ -24,7 +30,7 @@ def run(browser: str, max_pages: Union[int, None], driver: WebDriver = None): if driver is None: dm = DriverManager(browser=browser, is_ci_build=settings.is_ci_build) driver = dm.driver - redeem_courses(driver, settings, max_pages) + redeem_courses(driver, settings, max_pages, cache_hit_limit) def parse_args(browser=None, use_manual_driver=False) -> Namespace: @@ -50,6 +56,12 @@ def parse_args(browser=None, use_manual_driver=False) -> Namespace: 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() @@ -62,4 +74,4 @@ def parse_args(browser=None, use_manual_driver=False) -> Namespace: if __name__ == "__main__": args = parse_args() if args: - run(args.browser, args.max_pages) + run(args.browser, args.max_pages, args.cache_hits) diff --git a/udemy_enroller_chrome.py b/udemy_enroller_chrome.py index 6db9cdf..1abbe9f 100644 --- a/udemy_enroller_chrome.py +++ b/udemy_enroller_chrome.py @@ -5,4 +5,4 @@ if __name__ == "__main__": args = parse_args("chrome") - run(args.browser, args.max_pages) + run(args.browser, args.max_pages, args.cache_hits) diff --git a/udemy_enroller_chromium.py b/udemy_enroller_chromium.py index a1ee781..4a5fc05 100644 --- a/udemy_enroller_chromium.py +++ b/udemy_enroller_chromium.py @@ -5,4 +5,4 @@ if __name__ == "__main__": args = parse_args("chromium") - run(args.browser, args.max_pages) + run(args.browser, args.max_pages, args.cache_hits) diff --git a/udemy_enroller_edge.py b/udemy_enroller_edge.py index 4955f7d..ae9cb94 100644 --- a/udemy_enroller_edge.py +++ b/udemy_enroller_edge.py @@ -5,4 +5,4 @@ if __name__ == "__main__": args = parse_args("edge") - run(args.browser, args.max_pages) + run(args.browser, args.max_pages, args.cache_hits) diff --git a/udemy_enroller_firefox.py b/udemy_enroller_firefox.py index 63a76e9..a49ff18 100644 --- a/udemy_enroller_firefox.py +++ b/udemy_enroller_firefox.py @@ -6,4 +6,4 @@ if __name__ == "__main__": args = parse_args("firefox") - run(args.browser, args.max_pages) + run(args.browser, args.max_pages, args.cache_hits) diff --git a/udemy_enroller_internet_explorer.py b/udemy_enroller_internet_explorer.py index bfe934b..8c67fec 100644 --- a/udemy_enroller_internet_explorer.py +++ b/udemy_enroller_internet_explorer.py @@ -5,4 +5,4 @@ if __name__ == "__main__": args = parse_args("internet_explorer") - run(args.browser, args.max_pages) + run(args.browser, args.max_pages, args.cache_hits) diff --git a/udemy_enroller_opera.py b/udemy_enroller_opera.py index c673be4..ac5e391 100644 --- a/udemy_enroller_opera.py +++ b/udemy_enroller_opera.py @@ -5,4 +5,4 @@ if __name__ == "__main__": args = parse_args("opera") - run(args.browser, args.max_pages) + run(args.browser, args.max_pages, args.cache_hits) diff --git a/udemy_enroller_vanilla.py b/udemy_enroller_vanilla.py index 18e3a65..8e52539 100644 --- a/udemy_enroller_vanilla.py +++ b/udemy_enroller_vanilla.py @@ -31,4 +31,4 @@ # in the maximized layout driver.maximize_window() - run(args.browser, args.max_pages, driver=driver) + run(args.browser, args.max_pages, args.cache_hits, driver=driver) From aa7ba2d9ff0836dc66129224211ad38ee9fd210e Mon Sep 17 00:00:00 2001 From: cullzie Date: Wed, 2 Dec 2020 12:27:16 +0000 Subject: [PATCH 39/44] Update README and check if limit reached on every loop --- README.md | 1 + core/utils.py | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 85cf490..ac9a5e0 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,7 @@ get all the requirements installed in one go. Similar instructions applies for p - `--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` diff --git a/core/utils.py b/core/utils.py index 54e6a04..6383083 100644 --- a/core/utils.py +++ b/core/utils.py @@ -50,8 +50,13 @@ def _redeem_courses( current_cache_hits = 0 else: 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: logger.error(e) except TimeoutException: @@ -72,10 +77,6 @@ def _redeem_courses( logger.info("Ending test") return - # Exit the loop if we have reached the cache hit limit - if _reached_cache_hit_limit(cache_hit_limit, current_cache_hits): - return - logger.info("Moving on to the next page of the course list on tutorialbar.com") From a87c83b85ec20ff324834acad0066b3111fab0bb Mon Sep 17 00:00:00 2001 From: cullzie Date: Wed, 2 Dec 2020 17:21:29 +0000 Subject: [PATCH 40/44] Add deprecation warning to scripts --- udemy_enroller_chrome.py | 9 ++++++++- udemy_enroller_chromium.py | 9 ++++++++- udemy_enroller_edge.py | 9 ++++++++- udemy_enroller_firefox.py | 9 ++++++++- udemy_enroller_internet_explorer.py | 9 ++++++++- udemy_enroller_opera.py | 9 ++++++++- 6 files changed, 48 insertions(+), 6 deletions(-) diff --git a/udemy_enroller_chrome.py b/udemy_enroller_chrome.py index 1abbe9f..a10a265 100644 --- a/udemy_enroller_chrome.py +++ b/udemy_enroller_chrome.py @@ -1,8 +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! +import warnings + from udemy_enroller import parse_args, run if __name__ == "__main__": - args = parse_args("chrome") + browser = "chrome" + 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_chromium.py b/udemy_enroller_chromium.py index 4a5fc05..bc21d33 100644 --- a/udemy_enroller_chromium.py +++ b/udemy_enroller_chromium.py @@ -1,8 +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! +import warnings + from udemy_enroller import parse_args, run if __name__ == "__main__": - args = parse_args("chromium") + 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 ae9cb94..e33eecf 100644 --- a/udemy_enroller_edge.py +++ b/udemy_enroller_edge.py @@ -1,8 +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! +import warnings + from udemy_enroller import parse_args, run if __name__ == "__main__": - args = parse_args("edge") + 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 a49ff18..5d5d770 100644 --- a/udemy_enroller_firefox.py +++ b/udemy_enroller_firefox.py @@ -2,8 +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. +import warnings + from udemy_enroller import parse_args, run if __name__ == "__main__": - args = parse_args("firefox") + 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 8c67fec..d1c8fa5 100644 --- a/udemy_enroller_internet_explorer.py +++ b/udemy_enroller_internet_explorer.py @@ -1,8 +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! +import warnings + from udemy_enroller import parse_args, run if __name__ == "__main__": - args = parse_args("internet_explorer") + 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 ac5e391..8970d4a 100644 --- a/udemy_enroller_opera.py +++ b/udemy_enroller_opera.py @@ -1,8 +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! +import warnings + from udemy_enroller import parse_args, run if __name__ == "__main__": - args = parse_args("opera") + 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) From e639ff5c838d76a9ac8a2e061a28113c3fe2ee63 Mon Sep 17 00:00:00 2001 From: cullzie Date: Fri, 4 Dec 2020 19:11:40 +0000 Subject: [PATCH 41/44] Adding state check to enroller --- core/udemy.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/core/udemy.py b/core/udemy.py index 40f6eb6..12f737c 100644 --- a/core/udemy.py +++ b/core/udemy.py @@ -183,6 +183,25 @@ def redeem(self, url: str) -> str: ) 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() From 6959fd78c2e381e1fb4c4c6eaf181e07d681fae7 Mon Sep 17 00:00:00 2001 From: cullzie Date: Fri, 4 Dec 2020 22:57:43 +0000 Subject: [PATCH 42/44] Wait for enroll button to be clickable after selecting state/province --- core/udemy.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/core/udemy.py b/core/udemy.py index 12f737c..ea5a886 100644 --- a/core/udemy.py +++ b/core/udemy.py @@ -203,17 +203,15 @@ def redeem(self, url: str) -> str: 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)) ) logger.info(f"Successfully enrolled in: {course_name}") From 91b7ec467051d8875ed835bea8b4bb44c83f39b2 Mon Sep 17 00:00:00 2001 From: fakeid30 Date: Wed, 9 Dec 2020 12:29:14 +0600 Subject: [PATCH 43/44] Added sponsor note for gitbook --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index ac9a5e0..2b153c5 100644 --- a/README.md +++ b/README.md @@ -195,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 From a613365cd4615bdaff34c7bc11c5078dea0ad3bc Mon Sep 17 00:00:00 2001 From: fakeid30 Date: Wed, 9 Dec 2020 12:35:46 +0600 Subject: [PATCH 44/44] Added changes to changelog --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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