From fff3b1e5dfe95eed454868d2d055ca487790ff1c Mon Sep 17 00:00:00 2001 From: cullzie Date: Thu, 29 Sep 2022 21:04:07 +0100 Subject: [PATCH 01/27] Log package and os details on debug --- udemy_enroller/cli.py | 43 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/udemy_enroller/cli.py b/udemy_enroller/cli.py index 26d95ad..3f3b9c8 100644 --- a/udemy_enroller/cli.py +++ b/udemy_enroller/cli.py @@ -1,8 +1,12 @@ import argparse import logging +import platform +import sys from argparse import Namespace from typing import Tuple, Union +from pkg_resources import DistributionNotFound, get_distribution + from udemy_enroller import ALL_VALID_BROWSER_STRINGS, DriverManager, Settings from udemy_enroller.logging import get_logger from udemy_enroller.runner import redeem_courses, redeem_courses_ui @@ -22,6 +26,40 @@ def enable_debug_logging() -> None: logger.info(f"Enabled debug logging") +def log_package_details() -> None: + """ + Log details of the package. + + :return: None + """ + try: + distribution = get_distribution("udemy_enroller") + if distribution: + logger.debug(f"Name: {distribution.project_name}") + logger.debug(f"Version: {distribution.version}") + logger.debug(f"Location: {distribution.location}") + except DistributionNotFound: + logger.debug("Not installed on python env.") + + +def log_python_version(): + """ + Log version of python in use. + + :return: None + """ + logger.debug(f"Python: {sys.version}") + + +def log_os_version(): + """ + Log version of the OS. + + :return: None + """ + logger.debug(f"OS: {platform.platform()}") + + def determine_if_scraper_enabled( freebiesglobal_enabled: bool, tutorialbar_enabled: bool, @@ -147,7 +185,7 @@ def parse_args() -> Namespace: "--max-pages", type=int, default=5, - help=f"Max pages to scrape from sites (if pagination exists) (Default is 5)", + help="Max pages to scrape from sites (if pagination exists) (Default is 5)", ) parser.add_argument( @@ -180,6 +218,9 @@ def main(): if args: if args.debug: enable_debug_logging() + log_package_details() + log_python_version() + log_os_version() ( freebiesglobal_enabled, tutorialbar_enabled, From 58d365b370c016afb9c1caa8e9dcf613b8112c57 Mon Sep 17 00:00:00 2001 From: cullzie Date: Thu, 23 Jun 2022 21:58:50 +0100 Subject: [PATCH 02/27] Coding improvements --- .github/workflows/python-package.yml | 2 +- README.md | 5 +---- pyproject.toml | 4 ++-- udemy_enroller.py => run_enroller.py | 0 udemy_enroller/__init__.py | 2 +- udemy_enroller/cli.py | 6 ++---- udemy_enroller/driver_manager.py | 2 +- udemy_enroller/{http.py => http_utils.py} | 4 ++-- udemy_enroller/{logging.py => logger.py} | 0 udemy_enroller/runner.py | 2 +- udemy_enroller/scrapers/coursevania.py | 12 ++++++------ udemy_enroller/scrapers/discudemy.py | 10 +++++----- udemy_enroller/scrapers/freebiesglobal.py | 10 +++++----- udemy_enroller/scrapers/tutorialbar.py | 10 +++++----- udemy_enroller/settings.py | 2 +- udemy_enroller/udemy_rest.py | 2 +- udemy_enroller/udemy_ui.py | 10 +++++++--- 17 files changed, 41 insertions(+), 42 deletions(-) rename udemy_enroller.py => run_enroller.py (100%) rename udemy_enroller/{http.py => http_utils.py} (87%) rename udemy_enroller/{logging.py => logger.py} (100%) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 9432e68..7833d47 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -53,4 +53,4 @@ jobs: UDEMY_PASSWORD: ${{ secrets.UDEMY_PASSWORD }} CI_TEST: "True" run: | - poetry run python udemy_enroller.py --browser=chrome --debug + poetry run python run_enroller.py --browser=chrome --debug diff --git a/README.md b/README.md index 6f7ae03..c877adb 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,6 @@ [![forthebadge](https://forthebadge.com/images/badges/made-with-python.svg)](https://forthebadge.com) [![forthebadge](https://forthebadge.com/images/badges/it-works-why.svg)](https://forthebadge.com) -* ALPHA IS A PRE DEVELOPMENT BRANCH, DO NOT EXPECT USER FACING ISSUES TO BE ADDRESSED IN THIS BRANCH! - - * Udemy Coupon Grabber & Course Enroller: Grab FREE Coupons! Do you want to LEARN NEW STUFF for FREE? Don't worry, with the power of @@ -19,7 +16,7 @@ The code scrapes course links and coupons from: - [tutorialbar.com](https://tutorialbar.com) - [discudemy.com](https://discudemy.com) - [coursevania.com](https://coursevania.com) - - [freebiesglobal.com](https://freebiesglobal.com) -> _New_ + - [freebiesglobal.com](https://freebiesglobal.com) In case of any bugs or issues, please open an issue in github. diff --git a/pyproject.toml b/pyproject.toml index fee30f8..2039b5b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,8 @@ [tool.poetry] -name = "automatic-udemy-course-enroller-get-paid-udemy-courses-for-free" +name = "udemy-enroller" version = "4.1.2" description = "" -authors = [""] +authors = ["aapatre ", "fakeid ", "cullzie "] [tool.poetry.dependencies] python = "^3.8" diff --git a/udemy_enroller.py b/run_enroller.py similarity index 100% rename from udemy_enroller.py rename to run_enroller.py diff --git a/udemy_enroller/__init__.py b/udemy_enroller/__init__.py index 7a53447..654e634 100644 --- a/udemy_enroller/__init__.py +++ b/udemy_enroller/__init__.py @@ -1,5 +1,5 @@ from .driver_manager import ALL_VALID_BROWSER_STRINGS, DriverManager -from .logging import load_logging_config +from .logger import load_logging_config from .scrapers.manager import ScraperManager from .settings import Settings from .udemy_rest import UdemyActions, UdemyStatus diff --git a/udemy_enroller/cli.py b/udemy_enroller/cli.py index 26d95ad..08cc0bc 100644 --- a/udemy_enroller/cli.py +++ b/udemy_enroller/cli.py @@ -4,7 +4,7 @@ from typing import Tuple, Union from udemy_enroller import ALL_VALID_BROWSER_STRINGS, DriverManager, Settings -from udemy_enroller.logging import get_logger +from udemy_enroller.logger import get_logger from udemy_enroller.runner import redeem_courses, redeem_courses_ui logger = get_logger() @@ -170,9 +170,7 @@ def parse_args() -> Namespace: help="Enable debug logging", ) - args = parser.parse_args() - - return args + return parser.parse_args() def main(): diff --git a/udemy_enroller/driver_manager.py b/udemy_enroller/driver_manager.py index d0d46d3..78d57f1 100644 --- a/udemy_enroller/driver_manager.py +++ b/udemy_enroller/driver_manager.py @@ -6,7 +6,7 @@ from webdriver_manager.microsoft import EdgeChromiumDriverManager, IEDriverManager from webdriver_manager.opera import OperaDriverManager -from udemy_enroller.logging import get_logger +from udemy_enroller.logger import get_logger logger = get_logger() diff --git a/udemy_enroller/http.py b/udemy_enroller/http_utils.py similarity index 87% rename from udemy_enroller/http.py rename to udemy_enroller/http_utils.py index 235755d..4fe979d 100644 --- a/udemy_enroller/http.py +++ b/udemy_enroller/http_utils.py @@ -1,11 +1,11 @@ import aiohttp -from udemy_enroller.logging import get_logger +from udemy_enroller.logger import get_logger logger = get_logger() -async def get(url, headers=None): +async def http_get(url, headers=None): """ Send REST get request to the url passed in diff --git a/udemy_enroller/logging.py b/udemy_enroller/logger.py similarity index 100% rename from udemy_enroller/logging.py rename to udemy_enroller/logger.py diff --git a/udemy_enroller/runner.py b/udemy_enroller/runner.py index 607cfcc..28d17b2 100644 --- a/udemy_enroller/runner.py +++ b/udemy_enroller/runner.py @@ -17,7 +17,7 @@ UdemyStatus, exceptions, ) -from udemy_enroller.logging import get_logger +from udemy_enroller.logger import get_logger logger = get_logger() diff --git a/udemy_enroller/scrapers/coursevania.py b/udemy_enroller/scrapers/coursevania.py index 8061605..c958a0b 100644 --- a/udemy_enroller/scrapers/coursevania.py +++ b/udemy_enroller/scrapers/coursevania.py @@ -1,15 +1,15 @@ import asyncio import json -import logging from typing import List from urllib.parse import urlencode from bs4 import BeautifulSoup -from udemy_enroller.http import get +from udemy_enroller.http_utils import http_get +from udemy_enroller.logger import get_logger from udemy_enroller.scrapers.base_scraper import BaseScraper -logger = logging.getLogger("udemy_enroller") +logger = get_logger() class CoursevaniaScraper(BaseScraper): @@ -66,7 +66,7 @@ async def load_nonce(self) -> None: :return: None """ if self._nonce is None: - response = await get(f"{self.DOMAIN}/courses") + response = await http_get(f"{self.DOMAIN}/courses") if response is not None: soup = BeautifulSoup(response, "html.parser") for script_element in soup.find_all("script"): @@ -102,7 +102,7 @@ async def get_course_links(self) -> List: "TE": "Trailers", } query_string = urlencode(query_params) - response = await get( + response = await http_get( f"{self.DOMAIN}/wp-admin/admin-ajax.php?{query_string}", headers=headers ) if response is not None: @@ -124,7 +124,7 @@ async def get_udemy_course_link(url: str) -> str: :param str url: The url to scrape data from :return: Coupon link of the udemy course """ - text = await get(url) + text = await http_get(url) if text is not None: soup = BeautifulSoup(text.decode("utf-8"), "html.parser") udemy_link = ( diff --git a/udemy_enroller/scrapers/discudemy.py b/udemy_enroller/scrapers/discudemy.py index fdb791f..70a7132 100644 --- a/udemy_enroller/scrapers/discudemy.py +++ b/udemy_enroller/scrapers/discudemy.py @@ -1,13 +1,13 @@ import asyncio -import logging from typing import List from bs4 import BeautifulSoup -from udemy_enroller.http import get +from udemy_enroller.http_utils import http_get +from udemy_enroller.logger import get_logger from udemy_enroller.scrapers.base_scraper import BaseScraper -logger = logging.getLogger("udemy_enroller") +logger = get_logger() class DiscUdemyScraper(BaseScraper): @@ -46,7 +46,7 @@ async def get_links(self) -> List: """ discudemy_links = [] self.current_page += 1 - coupons_data = await get(f"{self.DOMAIN}/all/{self.current_page}") + coupons_data = await http_get(f"{self.DOMAIN}/all/{self.current_page}") soup = BeautifulSoup(coupons_data.decode("utf-8"), "html.parser") for course_card in soup.find_all("a", class_="card-header"): url_end = course_card["href"].split("/")[-1] @@ -70,7 +70,7 @@ async def get_udemy_course_link(cls, url: str) -> str: :return: Coupon link of the udemy course """ - data = await get(url) + data = await http_get(url) soup = BeautifulSoup(data.decode("utf-8"), "html.parser") for link in soup.find_all("a", href=True): udemy_link = cls.validate_coupon_url(link["href"]) diff --git a/udemy_enroller/scrapers/freebiesglobal.py b/udemy_enroller/scrapers/freebiesglobal.py index a911696..ce364b4 100644 --- a/udemy_enroller/scrapers/freebiesglobal.py +++ b/udemy_enroller/scrapers/freebiesglobal.py @@ -1,13 +1,13 @@ import asyncio -import logging from typing import List from bs4 import BeautifulSoup -from udemy_enroller.http import get +from udemy_enroller.http_utils import http_get +from udemy_enroller.logger import get_logger from udemy_enroller.scrapers.base_scraper import BaseScraper -logger = logging.getLogger("udemy_enroller") +logger = get_logger() class FreebiesglobalScraper(BaseScraper): @@ -46,7 +46,7 @@ async def get_links(self) -> List: """ freebiesglobal_links = [] self.current_page += 1 - coupons_data = await get( + coupons_data = await http_get( f"{self.DOMAIN}/dealstore/udemy/page/{self.current_page}" ) soup = BeautifulSoup(coupons_data.decode("utf-8"), "html.parser") @@ -75,7 +75,7 @@ async def get_udemy_course_link(cls, url: str) -> str: :return: Coupon link of the udemy course """ - data = await get(url) + data = await http_get(url) soup = BeautifulSoup(data.decode("utf-8"), "html.parser") for link in soup.find_all("a", class_="re_track_btn"): udemy_link = cls.validate_coupon_url(link["href"]) diff --git a/udemy_enroller/scrapers/tutorialbar.py b/udemy_enroller/scrapers/tutorialbar.py index b293d44..bee8a8e 100644 --- a/udemy_enroller/scrapers/tutorialbar.py +++ b/udemy_enroller/scrapers/tutorialbar.py @@ -1,13 +1,13 @@ import asyncio -import logging from typing import List from bs4 import BeautifulSoup -from udemy_enroller.http import get +from udemy_enroller.http_utils import http_get +from udemy_enroller.logger import get_logger from udemy_enroller.scrapers.base_scraper import BaseScraper -logger = logging.getLogger("udemy_enroller") +logger = get_logger() class TutorialBarScraper(BaseScraper): @@ -82,7 +82,7 @@ async def get_course_links(self, url: str) -> List: :param str url: The url to scrape data from :return: list of pages on tutorialbar.com that contain Udemy coupons """ - text = await get(url) + text = await http_get(url) if text is not None: soup = BeautifulSoup(text.decode("utf-8"), "html.parser") @@ -106,7 +106,7 @@ async def get_udemy_course_link(url: str) -> str: :return: Coupon link of the udemy course """ - text = await get(url) + text = await http_get(url) if text is not None: soup = BeautifulSoup(text.decode("utf-8"), "html.parser") udemy_link = ( diff --git a/udemy_enroller/settings.py b/udemy_enroller/settings.py index 747d023..c7b230c 100644 --- a/udemy_enroller/settings.py +++ b/udemy_enroller/settings.py @@ -5,7 +5,7 @@ from ruamel.yaml import YAML, dump -from udemy_enroller.logging import get_logger +from udemy_enroller.logger import get_logger from udemy_enroller.utils import get_app_dir logger = get_logger() diff --git a/udemy_enroller/udemy_rest.py b/udemy_enroller/udemy_rest.py index 980b2b4..af22b14 100644 --- a/udemy_enroller/udemy_rest.py +++ b/udemy_enroller/udemy_rest.py @@ -10,7 +10,7 @@ from bs4 import BeautifulSoup from cloudscraper import create_scraper -from udemy_enroller.logging import get_logger +from udemy_enroller.logger import get_logger from udemy_enroller.settings import Settings from udemy_enroller.utils import get_app_dir diff --git a/udemy_enroller/udemy_ui.py b/udemy_enroller/udemy_ui.py index a7c34f9..6113857 100644 --- a/udemy_enroller/udemy_ui.py +++ b/udemy_enroller/udemy_ui.py @@ -13,7 +13,7 @@ from selenium.webdriver.support.ui import WebDriverWait from udemy_enroller.exceptions import LoginException, RobotException -from udemy_enroller.logging import get_logger +from udemy_enroller.logger import get_logger from udemy_enroller.settings import Settings logger = get_logger() @@ -298,8 +298,12 @@ def _check_categories(self, course_identifier): breadcrumbs: WebElement = self.driver.find_element_by_class_name( breadcrumbs_path ) - breadcrumbs = breadcrumbs.find_elements_by_class_name(breadcrumbs_text_path) - breadcrumb_text = [bc.text for bc in breadcrumbs] # Get only the text + breadcrumb_elements = breadcrumbs.find_elements_by_class_name( + breadcrumbs_text_path + ) + breadcrumb_text = [ + bc.text for bc in breadcrumb_elements + ] # Get only the text for category in self.settings.categories: if category in breadcrumb_text: From 36e5801bb6e3cacb67099774b19f73a8c4535b1b Mon Sep 17 00:00:00 2001 From: cullzie Date: Mon, 3 Oct 2022 12:06:55 +0100 Subject: [PATCH 03/27] Fix flake8 violations --- .github/workflows/python-package.yml | 4 +- pyproject.toml | 3 ++ pytest.ini | 5 --- setup.cfg | 12 +++++ setup.py | 1 + udemy_enroller/__init__.py | 11 ++--- udemy_enroller/cli.py | 14 +++--- udemy_enroller/driver_manager.py | 9 ++-- udemy_enroller/exceptions.py | 11 +++-- udemy_enroller/http.py | 3 +- udemy_enroller/logging.py | 11 +++-- udemy_enroller/runner.py | 14 +++--- udemy_enroller/scrapers/__init__.py | 1 + udemy_enroller/scrapers/base_scraper.py | 44 +++++++++++++------ udemy_enroller/scrapers/coursevania.py | 18 ++++---- udemy_enroller/scrapers/discudemy.py | 18 ++++---- udemy_enroller/scrapers/freebiesglobal.py | 18 ++++---- udemy_enroller/scrapers/manager.py | 14 +++--- udemy_enroller/scrapers/tutorialbar.py | 19 ++++---- udemy_enroller/settings.py | 40 +++++++++-------- udemy_enroller/udemy_rest.py | 53 +++++++++++++---------- udemy_enroller/udemy_ui.py | 26 ++++++----- udemy_enroller/utils.py | 3 +- 23 files changed, 198 insertions(+), 154 deletions(-) create mode 100644 setup.cfg diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 9432e68..2a8db3e 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -41,9 +41,9 @@ jobs: - 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 + flake8 setup.py udemy_enroller --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 + flake8 setup.py udemy_enroller --count --exit-zero --max-complexity=10 --max-line-length=120 --statistics - name: Run unittests run: | poetry run pytest diff --git a/pyproject.toml b/pyproject.toml index fee30f8..88640ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,9 @@ pytest = "^7.1.2" pytest-cov = "^3.0.0" pytest-asyncio = "^0.18.3" bumpver = "^2022.1116" +flake8 = "^5.0.4" +flake8-bugbear = "^22.9.23" +flake8-docstrings = "^1.6.0" [tool.bumpver] current_version = "4.1.2" diff --git a/pytest.ini b/pytest.ini index ba21a73..e69de29 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,5 +0,0 @@ -[pytest] -addopts = - --cov=. --cov-report xml --cov-report term -testpaths = - tests diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..54543c1 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,12 @@ +[pytest] +addopts = + --cov=. --cov-report xml --cov-report term +testpaths = + tests + +[flake8] +max-line-length = 120 +max-complexity = 10 +per-file-ignores = + udemy_enroller/udemy_ui.py: C901 + udemy_enroller/runner.py: C901 diff --git a/setup.py b/setup.py index 8d9f03e..3125f4f 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,4 @@ +"""Setup.""" import pathlib from setuptools import find_packages, setup diff --git a/udemy_enroller/__init__.py b/udemy_enroller/__init__.py index 7a53447..5d5fe51 100644 --- a/udemy_enroller/__init__.py +++ b/udemy_enroller/__init__.py @@ -1,8 +1,9 @@ -from .driver_manager import ALL_VALID_BROWSER_STRINGS, DriverManager +""".""" +from .driver_manager import ALL_VALID_BROWSER_STRINGS, DriverManager # noqa: F401 from .logging import load_logging_config -from .scrapers.manager import ScraperManager -from .settings import Settings -from .udemy_rest import UdemyActions, UdemyStatus -from .udemy_ui import UdemyActionsUI +from .scrapers.manager import ScraperManager # noqa: F401 +from .settings import Settings # noqa: F401 +from .udemy_rest import UdemyActions, UdemyStatus # noqa: F401 +from .udemy_ui import UdemyActionsUI # noqa: F401 load_logging_config() diff --git a/udemy_enroller/cli.py b/udemy_enroller/cli.py index 26d95ad..cd53c32 100644 --- a/udemy_enroller/cli.py +++ b/udemy_enroller/cli.py @@ -1,3 +1,4 @@ +"""CLI entrypoint for this script.""" import argparse import logging from argparse import Namespace @@ -12,14 +13,14 @@ def enable_debug_logging() -> None: """ - Enable debug logging for the scripts + Enable debug logging for the scripts. :return: None """ logger.setLevel(logging.DEBUG) for handler in logger.handlers: handler.setLevel(logging.DEBUG) - logger.info(f"Enabled debug logging") + logger.info("Enabled debug logging") def determine_if_scraper_enabled( @@ -29,7 +30,7 @@ def determine_if_scraper_enabled( coursevania_enabled: bool, ) -> Tuple[bool, bool, bool, bool]: """ - Determine what scrapers should be enabled and disabled + Determine what scrapers should be enabled and disabled. :return: tuple containing boolean of what scrapers should run """ @@ -66,7 +67,7 @@ def run( delete_cookie: bool, ): """ - Run the udemy enroller script + Run the udemy enroller script. :param str browser: Name of the browser we want to create a driver for :param bool freebiesglobal_enabled: @@ -103,7 +104,7 @@ def run( def parse_args() -> Namespace: """ - Parse args from the CLI or use the args passed in + Parse args from the CLI or use the args passed in. :return: Args to be used in the script """ @@ -147,7 +148,7 @@ def parse_args() -> Namespace: "--max-pages", type=int, default=5, - help=f"Max pages to scrape from sites (if pagination exists) (Default is 5)", + help="Max pages to scrape from sites (if pagination exists) (Default is 5)", ) parser.add_argument( @@ -176,6 +177,7 @@ def parse_args() -> Namespace: def main(): + """Entrypoint for scripts.""" args = parse_args() if args: if args.debug: diff --git a/udemy_enroller/driver_manager.py b/udemy_enroller/driver_manager.py index d0d46d3..a361724 100644 --- a/udemy_enroller/driver_manager.py +++ b/udemy_enroller/driver_manager.py @@ -1,3 +1,4 @@ +"""Webdriver manager.""" from selenium import webdriver from selenium.webdriver.chrome.options import Options as ChromeOptions from webdriver_manager.chrome import ChromeDriverManager @@ -21,7 +22,10 @@ class DriverManager: + """Webdriver manager.""" + def __init__(self, browser: str, is_ci_build: bool = False): + """Initialize.""" self.driver = None self.options = None self.browser = browser @@ -30,11 +34,10 @@ def __init__(self, browser: str, is_ci_build: bool = False): def _init_driver(self): """ - Initialize the correct web driver based on the users requested browser + Initialize the correct web driver based on the users requested browser. :return: None """ - if self.browser.lower() in VALID_CHROME_STRINGS: if self.is_ci_build: self.options = self._build_ci_options_chrome() @@ -76,7 +79,7 @@ def _init_driver(self): @staticmethod def _build_ci_options_chrome(): """ - Build chrome options required to run in CI + Build chrome options required to run in CI. :return: """ diff --git a/udemy_enroller/exceptions.py b/udemy_enroller/exceptions.py index ba632a2..7fc377e 100644 --- a/udemy_enroller/exceptions.py +++ b/udemy_enroller/exceptions.py @@ -1,14 +1,13 @@ +"""Custom exception module.""" + + class RobotException(Exception): - """ - You have been identified as a robot on Udemy site - """ + """You have been identified as a robot on Udemy site.""" pass class LoginException(Exception): - """ - You have failed to login to the Udemy site - """ + """You have failed to login to the Udemy site.""" pass diff --git a/udemy_enroller/http.py b/udemy_enroller/http.py index 235755d..0a82276 100644 --- a/udemy_enroller/http.py +++ b/udemy_enroller/http.py @@ -1,3 +1,4 @@ +"""HTTP helpers.""" import aiohttp from udemy_enroller.logging import get_logger @@ -7,7 +8,7 @@ async def get(url, headers=None): """ - Send REST get request to the url passed in + Send REST get request to the url passed in. :param url: The Url to get call get request on :param headers: The headers to pass with the get request diff --git a/udemy_enroller/logging.py b/udemy_enroller/logging.py index f0e5eac..3d12b30 100644 --- a/udemy_enroller/logging.py +++ b/udemy_enroller/logging.py @@ -1,3 +1,4 @@ +"""Logger utilities.""" import logging import logging.config import os @@ -6,22 +7,20 @@ class CustomFileHandler(logging.FileHandler): - """ - Allows us to log to the app directory - """ + """Allows us to log to the app directory.""" def __init__(self, file_name="app.log", mode="a"): + """Initialize.""" log_file_path = os.path.join(get_app_dir(), file_name) super(CustomFileHandler, self).__init__(log_file_path, mode) def load_logging_config() -> None: """ - Load logging configuration + Load logging configuration. :return: None """ - my_logger = logging.getLogger("udemy_enroller") my_logger.setLevel(logging.INFO) @@ -41,7 +40,7 @@ def load_logging_config() -> None: def get_logger() -> logging.Logger: """ - Convenience method to load the app logger + Get the app logger. :return: An instance of the app logger """ diff --git a/udemy_enroller/runner.py b/udemy_enroller/runner.py index 607cfcc..8c8b5cb 100644 --- a/udemy_enroller/runner.py +++ b/udemy_enroller/runner.py @@ -1,3 +1,4 @@ +"""Runner.""" import asyncio import random import time @@ -24,7 +25,7 @@ def _redeem_courses(settings: Settings, scrapers: ScraperManager) -> None: """ - Method to scrape courses from the supported sites and enroll in them on udemy + Scrape courses from the supported sites and enroll in them on udemy. :param Settings settings: Core settings used for Udemy :param ScraperManager scrapers: @@ -59,7 +60,7 @@ def _redeem_courses(settings: Settings, scrapers: ScraperManager) -> None: if settings.is_ci_build: logger.info("We have attempted to subscribe to 1 udemy course") logger.info("Ending test") - return + return # noqa: B012 else: udemy_actions.stats.table() logger.info("All scrapers complete") @@ -75,7 +76,7 @@ def redeem_courses( max_pages: Union[int, None], ) -> None: """ - Wrapper of _redeem_courses which catches unhandled exceptions + Wrap _redeem_courses to catch unhandled exceptions. :param Settings settings: Core settings used for Udemy :param bool freebiesglobal_enabled: Boolean signifying if freebiesglobal scraper should run @@ -104,7 +105,7 @@ def _redeem_courses_ui( scrapers: ScraperManager, ) -> None: """ - Method to scrape courses from the supported sites and enroll in them on udemy. + Scrape courses from the supported sites and enroll in them on udemy. :param WebDriver driver: WebDriver to use to complete enrolment :param Settings settings: Core settings used for Udemy @@ -150,7 +151,7 @@ def _redeem_courses_ui( if settings.is_ci_build: logger.info("We have attempted to subscribe to 1 udemy course") logger.info("Ending test") - return + return # noqa: B012 else: udemy_actions.stats.table() logger.info("All scrapers complete") @@ -167,7 +168,7 @@ def redeem_courses_ui( max_pages: Union[int, None], ) -> None: """ - Wrapper of _redeem_courses so we always close browser on completion + Wrap _redeem_courses so we always close browser on completion. :param WebDriver driver: WebDriver to use to complete enrolment :param Settings settings: Core settings used for Udemy @@ -178,7 +179,6 @@ def redeem_courses_ui( :param int max_pages: Max pages to scrape from sites (if pagination exists) :return: """ - try: scrapers = ScraperManager( freebiesglobal_enabled, diff --git a/udemy_enroller/scrapers/__init__.py b/udemy_enroller/scrapers/__init__.py index e69de29..c888225 100644 --- a/udemy_enroller/scrapers/__init__.py +++ b/udemy_enroller/scrapers/__init__.py @@ -0,0 +1 @@ +"""Scrapers module.""" diff --git a/udemy_enroller/scrapers/base_scraper.py b/udemy_enroller/scrapers/base_scraper.py index 57ce7f8..f18b900 100644 --- a/udemy_enroller/scrapers/base_scraper.py +++ b/udemy_enroller/scrapers/base_scraper.py @@ -1,6 +1,8 @@ +"""Base Scraper.""" import datetime import logging import re +import typing from abc import ABC, abstractmethod from enum import Enum from typing import Optional @@ -9,13 +11,18 @@ class ScraperStates(Enum): + """Scraper states.""" + DISABLED = "DISABLED" RUNNING = "RUNNING" COMPLETE = "COMPLETE" class BaseScraper(ABC): + """Base scraper logic.""" + def __init__(self): + """Initialize.""" self._state = None self.scraper_name = None self.max_pages = None @@ -24,40 +31,50 @@ def __init__(self): @abstractmethod async def run(self): + """Run method that must be implemented in subclasses.""" return @abstractmethod - async def get_links(self): + async def get_links(self) -> typing.List[str]: + """Get links method that must be implemented in subclasses.""" return @property - def state(self): + def state(self) -> str: + """State property.""" return self._state @state.setter - def state(self, value): + def state(self, value) -> None: + """Set the state of the scraper.""" if any([ss for ss in ScraperStates if ss.value == value]): self._state = value - def set_state_disabled(self): + def set_state_disabled(self) -> None: + """Set state to disable.""" self.state = ScraperStates.DISABLED.value logger.info(f"{self.scraper_name} scraper disabled") - def set_state_running(self): + def set_state_running(self) -> None: + """Set state to running.""" self.state = ScraperStates.RUNNING.value logger.info(f"{self.scraper_name} scraper is running") - def set_state_complete(self): + def set_state_complete(self) -> None: + """Set state to complete.""" self.state = ScraperStates.COMPLETE.value logger.info(f"{self.scraper_name} scraper complete") - def is_disabled(self): + def is_disabled(self) -> bool: + """Determine whether a scraper is disabled.""" return self.state == ScraperStates.DISABLED.value - def is_complete(self): + def is_complete(self) -> bool: + """Determine whether a scraper has completed.""" return self.state == ScraperStates.COMPLETE.value - def should_run(self): + def should_run(self) -> bool: + """Determine whether a scraper should run.""" should_run = not self.is_disabled() and not self.is_complete() if should_run: self.set_state_running() @@ -65,6 +82,8 @@ def should_run(self): @staticmethod def time_run(func): + """Log execution time of the function that is wrapped.""" + async def wrapper(self): start_time = datetime.datetime.utcnow() try: @@ -83,11 +102,10 @@ async def wrapper(self): def max_pages_reached(self) -> bool: """ - Returns boolean of whether or not we should continue checking tutorialbar.com + Return a boolean of whether we should continue checking site. :return: """ - should_run = True if self.max_pages is not None: @@ -110,9 +128,9 @@ def max_pages_reached(self) -> bool: @staticmethod def validate_coupon_url(url) -> Optional[str]: """ - Validate the udemy coupon url passed in - If it matches the pattern it is returned else it returns None + Validate the udemy coupon url passed in. + If it matches the pattern it is returned else it returns None :param url: The url to check the udemy coupon pattern for :return: The validated url or None """ diff --git a/udemy_enroller/scrapers/coursevania.py b/udemy_enroller/scrapers/coursevania.py index 8061605..719fdb0 100644 --- a/udemy_enroller/scrapers/coursevania.py +++ b/udemy_enroller/scrapers/coursevania.py @@ -1,3 +1,4 @@ +"""Coursevania Scraper.""" import asyncio import json import logging @@ -13,13 +14,12 @@ class CoursevaniaScraper(BaseScraper): - """ - Contains any logic related to scraping of data from coursevania.com - """ + """Contains any logic related to scraping of data from coursevania.com.""" DOMAIN = "https://coursevania.com" def __init__(self, enabled, max_pages=None): + """Initialize.""" super().__init__() self.scraper_name = "coursevania" if not enabled: @@ -31,7 +31,7 @@ def __init__(self, enabled, max_pages=None): @BaseScraper.time_run async def run(self) -> List: """ - Called to gather the udemy links + Gather the udemy links. :return: List of udemy course links """ @@ -44,7 +44,7 @@ async def run(self) -> List: async def get_links(self): """ - Scrape udemy links from coursevania.com + Scrape udemy links from coursevania.com. :return: List of udemy course urls """ @@ -61,7 +61,7 @@ async def get_links(self): async def load_nonce(self) -> None: """ - Load the nonce value needed to load the correct page data + Load the nonce value needed to load the correct page data. :return: None """ @@ -78,7 +78,7 @@ async def load_nonce(self) -> None: async def get_course_links(self) -> List: """ - Gets the url of pages which contain the udemy link we want to get + Get the url of pages which contain the udemy link we want to get. :return: list of pages on coursevania.com that contain Udemy coupons """ @@ -119,7 +119,7 @@ async def get_course_links(self) -> List: @staticmethod async def get_udemy_course_link(url: str) -> str: """ - Gets the udemy course link + Get the udemy course link. :param str url: The url to scrape data from :return: Coupon link of the udemy course @@ -134,7 +134,7 @@ async def get_udemy_course_link(url: str) -> str: async def gather_udemy_course_links(self, courses: List[str]): """ - Async fetching of the udemy course links from coursevania.com + Async fetching of the udemy course links from coursevania.com. :param list courses: A list of coursevania.com course links we want to fetch the udemy links for :return: list of udemy links diff --git a/udemy_enroller/scrapers/discudemy.py b/udemy_enroller/scrapers/discudemy.py index fdb791f..fa28aec 100644 --- a/udemy_enroller/scrapers/discudemy.py +++ b/udemy_enroller/scrapers/discudemy.py @@ -1,3 +1,4 @@ +"""Discudemy scraper.""" import asyncio import logging from typing import List @@ -11,13 +12,12 @@ class DiscUdemyScraper(BaseScraper): - """ - Contains any logic related to scraping of data from discudemy.com - """ + """Contains any logic related to scraping of data from discudemy.com.""" DOMAIN = "https://discudemy.com" def __init__(self, enabled, max_pages=None): + """Initialize.""" super().__init__() self.scraper_name = "discudemy" if not enabled: @@ -27,7 +27,7 @@ def __init__(self, enabled, max_pages=None): @BaseScraper.time_run async def run(self) -> List: """ - Called to gather the udemy links + Gathers the udemy links. :return: List of udemy course links """ @@ -40,7 +40,7 @@ async def run(self) -> List: async def get_links(self) -> List: """ - Scrape udemy links from discudemy.com + Scrape udemy links from discudemy.com. :return: List of udemy course urls """ @@ -64,12 +64,11 @@ async def get_links(self) -> List: @classmethod async def get_udemy_course_link(cls, url: str) -> str: """ - Gets the udemy course link + Get the udemy course link. :param str url: The url to scrape data from :return: Coupon link of the udemy course """ - data = await get(url) soup = BeautifulSoup(data.decode("utf-8"), "html.parser") for link in soup.find_all("a", href=True): @@ -79,7 +78,7 @@ async def get_udemy_course_link(cls, url: str) -> str: async def gather_udemy_course_links(self, courses: List[str]): """ - Async fetching of the udemy course links from discudemy.com + Async fetching of the udemy course links from discudemy.com. :param list courses: A list of discudemy.com course links we want to fetch the udemy links for :return: list of udemy links @@ -93,12 +92,11 @@ async def gather_udemy_course_links(self, courses: List[str]): @staticmethod def _get_last_page(soup: BeautifulSoup) -> int: """ - Extract the last page number to scrape + Extract the last page number to scrape. :param soup: :return: The last page number to scrape """ - return max( [ int(i.text) diff --git a/udemy_enroller/scrapers/freebiesglobal.py b/udemy_enroller/scrapers/freebiesglobal.py index a911696..facd304 100644 --- a/udemy_enroller/scrapers/freebiesglobal.py +++ b/udemy_enroller/scrapers/freebiesglobal.py @@ -1,3 +1,4 @@ +"""Freebiesglobal Scraper.""" import asyncio import logging from typing import List @@ -11,13 +12,12 @@ class FreebiesglobalScraper(BaseScraper): - """ - Contains any logic related to scraping of data from Freebiesglobal.com - """ + """Contains any logic related to scraping of data from Freebiesglobal.com.""" DOMAIN = "https://freebiesglobal.com" def __init__(self, enabled, max_pages=None): + """Initialize.""" super().__init__() self.scraper_name = "freebiesglobal" if not enabled: @@ -27,7 +27,7 @@ def __init__(self, enabled, max_pages=None): @BaseScraper.time_run async def run(self) -> List: """ - Called to gather the udemy links + Gathers the udemy links. :return: List of udemy course links """ @@ -40,7 +40,7 @@ async def run(self) -> List: async def get_links(self) -> List: """ - Scrape udemy links from freebiesglobal.com + Scrape udemy links from freebiesglobal.com. :return: List of udemy course urls """ @@ -69,12 +69,11 @@ async def get_links(self) -> List: @classmethod async def get_udemy_course_link(cls, url: str) -> str: """ - Gets the udemy course link + Get the udemy course link. :param str url: The url to scrape data from :return: Coupon link of the udemy course """ - data = await get(url) soup = BeautifulSoup(data.decode("utf-8"), "html.parser") for link in soup.find_all("a", class_="re_track_btn"): @@ -85,7 +84,7 @@ async def get_udemy_course_link(cls, url: str) -> str: async def gather_udemy_course_links(self, courses: List[str]): """ - Async fetching of the udemy course links from freebiesglobal.com + Async fetching of the udemy course links from freebiesglobal.com. :param list courses: A list of freebiesglobal.com course links we want to fetch the udemy links for :return: list of udemy links @@ -99,12 +98,11 @@ async def gather_udemy_course_links(self, courses: List[str]): @staticmethod def _get_last_page(soup: BeautifulSoup) -> int: """ - Extract the last page number to scrape + Extract the last page number to scrape. :param soup: :return: The last page number to scrape """ - return max( [ int(i.text) diff --git a/udemy_enroller/scrapers/manager.py b/udemy_enroller/scrapers/manager.py index d03e467..4d5aeba 100644 --- a/udemy_enroller/scrapers/manager.py +++ b/udemy_enroller/scrapers/manager.py @@ -1,6 +1,7 @@ +"""Manager for scapers.""" import asyncio +import typing from functools import reduce -from typing import List from udemy_enroller.scrapers.coursevania import CoursevaniaScraper from udemy_enroller.scrapers.discudemy import DiscUdemyScraper @@ -9,6 +10,8 @@ class ScraperManager: + """Manages the scrapers.""" + def __init__( self, freebiesglobal_enabled, @@ -17,6 +20,7 @@ def __init__( coursevania_enabled, max_pages, ): + """Initialize.""" self.freebiesglobal_scraper = FreebiesglobalScraper( freebiesglobal_enabled, max_pages=max_pages ) @@ -36,9 +40,9 @@ def __init__( self.coursevania_scraper, ) - async def run(self) -> List: + async def run(self) -> typing.List[str]: """ - Runs any enabled scrapers and returns a list of links + Run any enabled scrapers and returns a list of links. :return: list """ @@ -51,9 +55,9 @@ async def run(self) -> List: ) return urls - def _enabled_scrapers(self) -> List: + def _enabled_scrapers(self) -> typing.List: """ - Returns a list of scrapers that should run + Return a list of scrapers that should run. :return: """ diff --git a/udemy_enroller/scrapers/tutorialbar.py b/udemy_enroller/scrapers/tutorialbar.py index b293d44..1f99911 100644 --- a/udemy_enroller/scrapers/tutorialbar.py +++ b/udemy_enroller/scrapers/tutorialbar.py @@ -1,3 +1,4 @@ +"""Tutorialbar scraper.""" import asyncio import logging from typing import List @@ -11,14 +12,13 @@ class TutorialBarScraper(BaseScraper): - """ - Contains any logic related to scraping of data from tutorialbar.com - """ + """Contains any logic related to scraping of data from tutorialbar.com.""" DOMAIN = "https://www.tutorialbar.com" AD_DOMAINS = ("https://amzn", "https://bit.ly") def __init__(self, enabled, max_pages=None): + """Initialize.""" super().__init__() self.scraper_name = "tutorialbar" if not enabled: @@ -29,7 +29,7 @@ def __init__(self, enabled, max_pages=None): @BaseScraper.time_run async def run(self) -> List: """ - Runs the steps to scrape links from tutorialbar.com + Run the steps to scrape links from tutorialbar.com. :return: list of udemy coupon links """ @@ -39,7 +39,7 @@ async def run(self) -> List: async def get_links(self): """ - Scrape udemy links from tutorialbar.com + Scrape udemy links from tutorialbar.com. :return: List of udemy course urls """ @@ -61,7 +61,7 @@ async def get_links(self): def _filter_ad_domains(self, udemy_links) -> List: """ - Filter out any known ad domains from the links scraped + Filter out any known ad domains from the links scraped. :param list udemy_links: List of urls to filter ad domains from :return: A list of filtered urls @@ -77,7 +77,7 @@ def _filter_ad_domains(self, udemy_links) -> List: async def get_course_links(self, url: str) -> List: """ - Gets the url of pages which contain the udemy link we want to get + Get the url of pages which contain the udemy link we want to get. :param str url: The url to scrape data from :return: list of pages on tutorialbar.com that contain Udemy coupons @@ -100,12 +100,11 @@ async def get_course_links(self, url: str) -> List: @staticmethod async def get_udemy_course_link(url: str) -> str: """ - Gets the udemy course link + Get the udemy course link. :param str url: The url to scrape data from :return: Coupon link of the udemy course """ - text = await get(url) if text is not None: soup = BeautifulSoup(text.decode("utf-8"), "html.parser") @@ -116,7 +115,7 @@ async def get_udemy_course_link(url: str) -> str: async def gather_udemy_course_links(self, courses: List[str]): """ - Async fetching of the udemy course links from tutorialbar.com + Async fetching of the udemy course links from tutorialbar.com. :param list courses: A list of tutorialbar.com course links we want to fetch the udemy links for :return: list of udemy links diff --git a/udemy_enroller/settings.py b/udemy_enroller/settings.py index 747d023..6457451 100644 --- a/udemy_enroller/settings.py +++ b/udemy_enroller/settings.py @@ -1,3 +1,4 @@ +"""Settings.""" import getpass import os.path from distutils.util import strtobool @@ -12,13 +13,12 @@ class Settings: - """ - Contains all logic related to the scripts settings - """ + """Contains all logic related to the scripts settings.""" def __init__( self, delete_settings=False, delete_cookie=False, settings_path="settings.yaml" ): + """Initialize.""" self.email = None self.password = None self.zip_code = None @@ -38,7 +38,7 @@ def __init__( def _init_settings(self) -> None: """ - Initialize the settings to be used in the script + Initialize the settings to be used in the script. :return: """ @@ -52,7 +52,7 @@ def _init_settings(self) -> None: def _load_ci_settings(self): """ - Load environment variables for CI run + Load environment variables for CI run. :return: """ @@ -62,7 +62,7 @@ def _load_ci_settings(self): def _load_user_settings(self) -> Dict: """ - Loads the settings from the yaml file if it exists + Load the settings from the yaml file if it exists. :return: dictionary containing the script settings """ @@ -84,7 +84,7 @@ def _load_user_settings(self) -> Dict: def _generate_settings(self) -> None: """ - Generate the settings for the script + Generate the settings for the script. :return: """ @@ -96,7 +96,7 @@ def _generate_settings(self) -> None: def _get_email(self, prompt_save=True) -> Tuple[str, bool]: """ - Get input from user on the email to use for udemy + Get input from user on the email to use for udemy. :return: The users udemy email and if it should be saved """ @@ -113,7 +113,7 @@ def _get_email(self, prompt_save=True) -> Tuple[str, bool]: def _get_password(self, prompt_save=True) -> Tuple[str, bool]: """ - Get input from user on the password to use for udemy + Get input from user on the password to use for udemy. :return: The users udemy password and if it should be saved """ @@ -133,7 +133,7 @@ def _get_password(self, prompt_save=True) -> Tuple[str, bool]: @staticmethod def _get_zip_code() -> str: """ - Get input from user on the zip code to use for udemy + Get input from user on the zip code to use for udemy. :return: The users udemy zip code """ @@ -143,7 +143,7 @@ def _get_zip_code() -> str: @staticmethod def _get_languages() -> List[str]: """ - Get input from user on the languages they want to get courses in + Get input from user on the languages they want to get courses in. :return: list of languages the user wants to redeem udemy courses in """ @@ -154,9 +154,11 @@ def _get_languages() -> List[str]: @staticmethod def _get_categories() -> List[str]: - """Gets the categories the user wants. + """ + Get the categories the user wants. - :return: list of categories the user wants.""" + :return: list of categories the user wants. + """ categories = input( "Please enter in a list of comma separated values of" " the course categories you like, for example:\n" @@ -170,7 +172,7 @@ def _get_categories() -> List[str]: def _save_settings(self) -> None: """ - Confirm if the user wants to save settings to file + Confirm if the user wants to save settings to file. :return: """ @@ -190,7 +192,7 @@ def _save_settings(self) -> None: # Log some details for the user if not self._should_store_email: - logger.info(f"Your email has not been saved to settings.") + logger.info("Your email has not been saved to settings.") if not self._should_store_password: logger.info("Your password has not been saved to settings.") if not self._should_store_email or not self._should_store_password: @@ -200,7 +202,7 @@ def _save_settings(self) -> None: def delete_settings(self) -> None: """ - Delete the settings file + Delete the settings file. :return: None """ @@ -216,7 +218,7 @@ def delete_settings(self) -> None: def delete_cookie(self) -> None: """ - Delete the cookie file + Delete the cookie file. :return: None """ @@ -228,7 +230,7 @@ def delete_cookie(self) -> None: def prompt_email(self) -> None: """ - Prompt for Udemy email only. Does not prompt for saving + Prompt for Udemy email only. Does not prompt for saving. :return: None """ @@ -236,7 +238,7 @@ def prompt_email(self) -> None: def prompt_password(self) -> None: """ - Prompt for Udemy password only. Does not prompt for saving + Prompt for Udemy password only. Does not prompt for saving. :return: None """ diff --git a/udemy_enroller/udemy_rest.py b/udemy_enroller/udemy_rest.py index 980b2b4..8670858 100644 --- a/udemy_enroller/udemy_rest.py +++ b/udemy_enroller/udemy_rest.py @@ -1,3 +1,4 @@ +"""Udemy REST.""" import json import os import re @@ -18,9 +19,7 @@ def format_requests(func): - """ - Convenience method for handling requests response - """ + """Handle requests response.""" def formatting(*args, **kwargs): result = func(*args, **kwargs) @@ -32,6 +31,8 @@ def formatting(*args, **kwargs): @dataclass(unsafe_hash=True) class RunStatistics: + """Gather statistics on courses enrolled in.""" + prices: List[float] = field(default_factory=list) expired: int = 0 @@ -49,9 +50,11 @@ class RunStatistics: currency_symbol = "$" def savings(self): + """Calculate the savings made from enrolling to these courses.""" return sum(self.prices) or 0 def table(self): + """Log table of statistics to output.""" logger.info("================== Run Statistics ==================") logger.info(f"Enrolled: {self.enrolled}") logger.info(f"Unwanted Category: {self.unwanted_category}") @@ -66,9 +69,7 @@ def table(self): class UdemyStatus(Enum): - """ - Possible statuses of udemy course - """ + """Possible statuses of udemy course.""" ALREADY_ENROLLED = "ALREADY_ENROLLED" ENROLLED = "ENROLLED" @@ -78,6 +79,8 @@ class UdemyStatus(Enum): class UdemyActions: + """Udemy Actions.""" + LOGIN_URL = "https://www.udemy.com/join/login-popup/?locale=en_US" MY_COURSES = ( "https://www.udemy.com/api-2.0/users/me/subscribed-courses/?ordering=-last_accessed&fields[" @@ -106,6 +109,7 @@ class UdemyActions: } def __init__(self, settings: Settings, cookie_file_name: str = ".cookie"): + """Initialize.""" self.settings = settings self.user_has_preferences = self.settings.categories or self.settings.languages self.session = requests.Session() @@ -120,7 +124,8 @@ def __init__(self, settings: Settings, cookie_file_name: str = ".cookie"): def login(self, retry=False) -> None: """ - Login to Udemy using REST api + Login to Udemy using REST api. + Saves login cookies for future use :return: None @@ -207,7 +212,7 @@ def login(self, retry=False) -> None: def load_my_courses(self) -> List: """ - Loads users currently enrolled courses from Udemy + Load users currently enrolled courses from Udemy. :return: List of logged in users courses """ @@ -229,7 +234,7 @@ def load_my_courses(self) -> List: @format_requests def load_user_details(self): """ - Load the current users details + Load the current users details. :return: Dict containing the users details """ @@ -237,7 +242,7 @@ def load_user_details(self): def is_enrolled(self, course_id: int) -> bool: """ - Check if the user is currently enrolled in the course based on course_id passed in + Check if the user is currently enrolled in the course based on course_id passed in. :param int course_id: Check if the course_id is in the users current courses :return: @@ -246,7 +251,7 @@ def is_enrolled(self, course_id: int) -> bool: def _add_enrolled_course(self, course_id): """ - Add enrolled course to the list of enrolled course ids + Add enrolled course to the list of enrolled course ids. :param int course_id: The course_id to add to the list :return: @@ -258,7 +263,7 @@ def is_coupon_valid( self, course_id: int, coupon_code: str, course_identifier: str ) -> bool: """ - Check if the coupon is valid for a course + Check if the coupon is valid for a course. :param int course_id: Id of the course to check the coupon against :param str coupon_code: Coupon to apply to the course @@ -294,7 +299,7 @@ def is_preferred_language( self, course_details: Dict, course_identifier: str ) -> bool: """ - Check if the course is in one of the languages preferred by the user + Check if the course is in one of the languages preferred by the user. :param dict course_details: Dictionary containing course details from Udemy :param str course_identifier: Name of the course used for logging @@ -314,7 +319,7 @@ def is_preferred_category( self, course_details: Dict, course_identifier: str ) -> bool: """ - Check if the course is in one of the categories preferred by the user + Check if the course is in one of the categories preferred by the user. :param dict course_details: Dictionary containing course details from Udemy :param str course_identifier: Name of the course used for logging @@ -336,7 +341,7 @@ def is_preferred_category( @format_requests def my_courses(self, page: int, page_size: int) -> Dict: """ - Load the current logged in users courses + Load the current logged in users courses. :param int page: page number to load :param int page_size: number of courses to load per page @@ -347,7 +352,7 @@ def my_courses(self, page: int, page_size: int) -> Dict: @format_requests def coupon_details(self, course_id: int, coupon_code: str) -> Dict: """ - Check that the coupon is valid for the current course + Check that the coupon is valid for the current course. :param int course_id: Id of the course to check the coupon against :param str coupon_code: The coupon_code to check against the course @@ -358,7 +363,7 @@ def coupon_details(self, course_id: int, coupon_code: str) -> Dict: @format_requests def course_details(self, course_id: int) -> Dict: """ - Retrieves details relating to the course passed in + Retrieve details relating to the course passed in. :param int course_id: Id of the course to get the details of :return: dictionary containing the course details @@ -367,7 +372,7 @@ def course_details(self, course_id: int) -> Dict: def enroll(self, course_link: str) -> str: """ - Enroll the current user in the course provided + Enroll the current user in the course provided. :param str course_link: Link to the course with valid coupon attached :return: str representing the status of the enrolment @@ -409,7 +414,7 @@ def enroll(self, course_link: str) -> str: def _get_course_id(self, url: str) -> int: """ - Get the course id from the url provided + Get the course id from the url provided. :param str url: Udemy url to fetch the course from :return: int representing the course id @@ -428,7 +433,7 @@ def _checkout( retry: bool = False, ) -> str: """ - Checkout process for the course and coupon provided + Checkout process for the course and coupon provided. :param int course_id: The course id of the course to enroll in :param str coupon_code: The coupon code to apply on checkout @@ -465,7 +470,7 @@ def _checkout( def _build_checkout_payload(self, course_id: int, coupon_code: str) -> Dict: """ - Build the payload for checkout + Build the payload for checkout. :param int course_id: The course id to checkout :param str coupon_code: The coupon code to use at checkout @@ -489,7 +494,7 @@ def _build_checkout_payload(self, course_id: int, coupon_code: str) -> Dict: def _cache_cookies(self, cookies: Dict) -> None: """ - Caches cookies for future logins + Cache cookies for future logins. :param cookies: :return: @@ -500,7 +505,7 @@ def _cache_cookies(self, cookies: Dict) -> None: def _load_cookies(self) -> Dict: """ - Loads existing cookie file + Load existing cookie file. :return: """ @@ -516,7 +521,7 @@ def _load_cookies(self) -> Dict: def _delete_cookies(self) -> None: """ - Remove existing cookie file + Remove existing cookie file. :return: """ diff --git a/udemy_enroller/udemy_ui.py b/udemy_enroller/udemy_ui.py index a7c34f9..3453e21 100644 --- a/udemy_enroller/udemy_ui.py +++ b/udemy_enroller/udemy_ui.py @@ -1,3 +1,4 @@ +"""Udemy UI.""" import time from dataclasses import dataclass, field from datetime import datetime @@ -21,6 +22,8 @@ @dataclass(unsafe_hash=True) class RunStatistics: + """Gather statistics on courses enrolled in.""" + prices: List[Decimal] = field(default_factory=list) expired: int = 0 @@ -33,10 +36,12 @@ class RunStatistics: currency_symbol = None - def savings(self): + def savings(self) -> int: + """Calculate the savings made from enrolling to these courses.""" return sum(self.prices) or 0 def table(self): + """Log table of statistics to output.""" # Only show the table if we have something to show if self.prices: if self.currency_symbol is None: @@ -59,9 +64,7 @@ def table(self): class UdemyStatus(Enum): - """ - Possible statuses of udemy course - """ + """Possible statuses of udemy course.""" ALREADY_ENROLLED = "ALREADY_ENROLLED" ENROLLED = "ENROLLED" @@ -71,13 +74,12 @@ class UdemyStatus(Enum): class UdemyActionsUI: - """ - Contains any logic related to interacting with udemy website - """ + """Contains any logic related to interacting with udemy website.""" DOMAIN = "https://www.udemy.com" def __init__(self, driver: WebDriver, settings: Settings): + """Initialize.""" self.driver = driver self.settings = settings self.logged_in = False @@ -86,7 +88,7 @@ def __init__(self, driver: WebDriver, settings: Settings): def login(self, is_retry=False) -> None: """ - Login to your udemy account + Login to your udemy account. :param bool is_retry: Is this is a login retry and we still have captcha raise RobotException @@ -141,7 +143,7 @@ def login(self, is_retry=False) -> None: def enroll(self, url: str) -> str: """ - Redeems the course url passed in + Redeems the course url passed in. :param str url: URL of the course to redeem :return: A string detailing course status @@ -197,8 +199,8 @@ def enroll(self, url: str) -> str: self.settings.zip_code ) - # After you put the zip code in, the page refreshes itself and disables the enroll button for a split - # second. + # After you put the zip code in, the page refreshes itself and disables + # the enroll button for a split second. enroll_button_is_clickable = EC.element_to_be_clickable( (By.XPATH, enroll_button_xpath) ) @@ -351,7 +353,7 @@ def _check_price(self, course_name): def _check_if_robot(self) -> bool: """ - Simply checks if the captcha element is present on login if email/password elements are not + Simply checks if the captcha element is present on login if email/password elements are not. :return: Bool """ diff --git a/udemy_enroller/utils.py b/udemy_enroller/utils.py index 4f183da..20930cd 100644 --- a/udemy_enroller/utils.py +++ b/udemy_enroller/utils.py @@ -1,9 +1,10 @@ +"""Utility functions.""" import os def get_app_dir() -> str: """ - Gets the app directory where all data related to the script is stored + Get the app directory where all data related to the script is stored. :return: """ From 251b49961db58a477c8b10ccc2918b366d3b3a81 Mon Sep 17 00:00:00 2001 From: cullzie Date: Mon, 3 Oct 2022 14:15:55 +0100 Subject: [PATCH 04/27] Update codeql version --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 403fb9a..8d354cf 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -39,7 +39,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -50,7 +50,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v2 # ℹī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -64,4 +64,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 From b6893feba5d87a61843c12d0587f0e9651e5c393 Mon Sep 17 00:00:00 2001 From: Nowshin <82333564+Nyctophilia58@users.noreply.github.com> Date: Fri, 14 Oct 2022 10:32:49 +0600 Subject: [PATCH 05/27] Update README.md --- README.md | 102 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 58 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 6f7ae03..3fc35b5 100644 --- a/README.md +++ b/README.md @@ -28,13 +28,13 @@ Also, don't forget to **Fork & Star the repository if you like it!** **_We are also on [GitLab](https://gitlab.com/the-automators/Automatic-Udemy-Course-Enroller-GET-PAID-UDEMY-COURSES-for-FREE)_** -**_Video Proof:_** +### **Video Proof:** [![Udemy Auto-Course-Enroller](https://img.youtube.com/vi/tdLsVoraMxw/0.jpg)](https://www.youtube.com/watch?v=tdLsVoraMxw "GET Udemy Courses for FREE with Python | 2 Minute Tuesday") --- -** **_Disclaimer & WARNINGS:_** +### **Disclaimer & WARNINGS:** 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. **THIS PROJECT IS NOT AFFILIATED WITH UDEMY.** @@ -45,17 +45,17 @@ Also, don't forget to **Fork & Star the repository if you like it!** --- -** Requirements: +## Requirements: -*** How to Install the Requirements? +### How to Install the Requirements? -**Required Python version:** [Python 3.8+](https://www.python.org/downloads/) +__** Required Python version:__ [Python 3.8+](https://www.python.org/downloads/) -**(Windows users only) Required Microsoft Visual C++ 14.0+ version:** [Microsoft Visual C++ 14.0+](https://visualstudio.microsoft.com/visual-cpp-build-tools/) +__** (Windows users only) Required Microsoft Visual C++ 14.0+ version:__ [Microsoft Visual C++ 14.0+](https://visualstudio.microsoft.com/visual-cpp-build-tools/) ![alt text](https://docs.microsoft.com/answers/storage/attachments/34873-10262.png) -**You must have pip or poetry installed. Please look up how to install them 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 @@ -63,7 +63,7 @@ get all the requirements installed in one go. Similar instructions applies for p --- -** Instructions +## Instructions Props to Davidd Sargent for making a super simple video tutorial. If you prefer written instructions then continue reading further, else click on the image below for a quick video tutorial: @@ -71,13 +71,15 @@ Props to Davidd Sargent for making a super simple video tutorial. If you prefer 1 . Install from PyPI `pip install udemy-enroller` -- Run the script and the cli will guide you through the settings required +- Run the script and the cli will guide you through the settings required. - If you decide to save the settings they will be stored in your home directory:
**Windows**: C:/Users/CurrentUserName/.udemy_enroller
**Linux**: - /home/username/.udemy_enroller - **The values in settings.yaml should be in the same language as the site you are browsing on** + /home/username/.udemy_enroller + **The values in settings.yaml should be in the same language as the site you are browsing on.** + +
2 . The script can be passed arguments: @@ -90,23 +92,29 @@ Props to Davidd Sargent for making a super simple video tutorial. If you prefer - `--max-pages=`: Max number of pages to scrape from sites before exiting the script (default is 5) - `--delete-settings`: Delete existing settings file - `--delete-cookie`: Delete the cookie file if it exists -- `--debug`: Enable debug logging +- `--debug`: Enable debug logging + +
3 . Run the script in terminal with your target runner: - `udemy_enroller` - `udemy_enroller --browser=chrome` -- `udemy_enroller --browser=chromium` +- `udemy_enroller --browser=chromium` + +
4 . The bot starts scraping the course links from the first **All Courses** page on [Tutorial Bar](https://www.tutorialbar.com/all-courses/page/1), [DiscUdemy](https://www.discudemy.com/all), [Coursevania](https://coursevania.com) and [FreebiesGlobal](https://freebiesglobal.com) and starts enrolling you to Udemy courses. After it has enrolled you to courses from the first page, it then moves to the next site page and the cycle continues. -- Stop the script by pressing ctrl+c in terminal to stop the enrollment process. +- Stop the script by pressing `ctrl+c` in terminal to stop the enrollment process. -5 . _[New]_ At the end of process a detailed result is shown: +
+ +5 . *[New]* At the end of process a detailed result is shown: ``` ================== Run Statistics ================== @@ -126,85 +134,91 @@ Savings: â‚Ŧ2674.44 ## FAQs -*** 1. Can I get a specific course for free with this script? +__1. Can I get a specific course for free with this script?__ -Unfortunately no, but let me assure you that you may be lucky enough to get a +  Unfortunately no, but let me assure you that you may be lucky enough to get a particular course for free when the instructor posts its coupon code in order to promote it. Also, over time you would build a library of courses by running the script often and have all the required courses in your collection. In fact, I made this course after completing a -[Python automation course](https://www.udemy.com/course/automate/) and selenium, +[Python automation course](https://www.udemy.com/course/automate/) and [Selenium](https://www.selenium.dev/), which of course I got for free! :) +

+ +__2. How does the bot work?__ -*** 2. How does the bot work? - -The bot retrieves coupon links from Tutorial Bar, DiscUdemy and Coursevania's lists to cut the prices and +  The bot retrieves coupon links from [Tutorial Bar](https://www.tutorialbar.com/all-courses/page/1), [DiscUdemy](https://www.discudemy.com/all) and [Coursevania](https://coursevania.com)'s lists to cut the prices and then uses REST requests to authenticate and enroll to the courses. Think of it this way: Epic Games & other clients like Steam provide you a handful of games each week, for free; Only in this case, we need a coupon code -to make those courses free. +to make those courses free.

+ -*** 3. How frequently should you run the script? +__3. How frequently should you run the script?__ -Daily, at least once! 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. So, a mere **0.001%** of courses are **actually paid** in my collection! Thankfully, you can get more than what I gathered in 4 years, in a matter of -weeks! 🙌đŸģ +weeks! 🙌đŸģ

-*** 4. Why did I create this? -It used to be my daily habit to redeem courses and it was an extremely tedious +__4. Why did I create this?__ + +  It used to be my daily habit to redeem courses and it was an extremely tedious task that took around 15 minutes, for 10 courses. And then I suddenly got the idea to automate it, after I found the automation course mentioned above. I bet, -it will save your precious time too! :) +it will save your precious time too! :)

+ -*** 5. The code compiles successfully, but it's taking too long to work! IS there any way to fix that? +__5. The code compiles successfully, but it's taking too long to work! IS there any way to fix that?__ -Since we are heavily dependent on a third-party site to retrieve coupons links, +  Since we are heavily dependent on a third-party site to retrieve coupons links, there may be issues when the site is down. Needless to mention the connectivity issues too. If everything is working fine, you can see the courses being -retrieved in the Python console/shell, which may take a while. +retrieved in the Python console/shell, which may take a while.

+ + +__6. Which is the best way to run the script?__ + +  It is recommended to run the script using your terminal and system python.

+ -*** 6. Which is the best way to run the script? +__7. Which branch to commit against?__ -It is recommended to run the script using your terminal and system python. +  Pull request should be made on "_develop_" branch.

-*** 7. Which branch to commit against? -Pull request should be made on "develop" branch. +__8. What's the roadmap?__ -*** 8. What's the roadmap? +  Take a look at our [Roadmap here](https://github.com/aapatre/Automatic-Udemy-Course-Enroller-GET-PAID-UDEMY-COURSES-for-FREE/projects/1) and help us on what you want or talk to us about your proposed changes.

-Take a look at our -[Roadmap here](https://github.com/aapatre/Automatic-Udemy-Course-Enroller-GET-PAID-UDEMY-COURSES-for-FREE/projects/1) -and help us on what you want or talk to us about your proposed changes. --- -** Support & Maintenance Notice +## Support & Maintenance Notice By using this repo/script, you agree that the authors and contributors are under no obligation to provide support for the script and can discontinue it's development, as and when necessary, without prior notice. --- -** Supporters +## ** Supporters -*** Jetbrains +### *** 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 [![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. -*** GitLab +### *** GitLab [![GitLab](https://i.imgur.com/aUWtSn4.png)](https://gitlab.com) From b1fb74a4d09e57bb60d22f89c0abc59e25dd1b90 Mon Sep 17 00:00:00 2001 From: Nyctophilia58 Date: Fri, 14 Oct 2022 13:21:26 +0600 Subject: [PATCH 06/27] Updated the readme file --- .idea/Udemy_repo.iml | 15 ++++++ README.md | 122 +++++++++++++++++++++++++------------------ 2 files changed, 85 insertions(+), 52 deletions(-) create mode 100644 .idea/Udemy_repo.iml diff --git a/.idea/Udemy_repo.iml b/.idea/Udemy_repo.iml new file mode 100644 index 0000000..a895c33 --- /dev/null +++ b/.idea/Udemy_repo.iml @@ -0,0 +1,15 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 6f7ae03..e315651 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ * Udemy Coupon Grabber & Course Enroller: Grab FREE Coupons! +
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 @@ -14,6 +15,8 @@ web-scraping and automation, this script will find the necessary Udemy Coupons **NOTE: THIS PROJECT WILL NOT WORK WITH NON ENGLISH UDEMY.** +
+ The code scrapes course links and coupons from: - [tutorialbar.com](https://tutorialbar.com) @@ -22,40 +25,41 @@ The code scrapes course links and coupons from: - [freebiesglobal.com](https://freebiesglobal.com) -> _New_ -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!** +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!** -**_We are also on [GitLab](https://gitlab.com/the-automators/Automatic-Udemy-Course-Enroller-GET-PAID-UDEMY-COURSES-for-FREE)_** +**We are also on _[GitLab](https://gitlab.com/the-automators/Automatic-Udemy-Course-Enroller-GET-PAID-UDEMY-COURSES-for-FREE)_** +

-**_Video Proof:_** +## Video Proof: [![Udemy Auto-Course-Enroller](https://img.youtube.com/vi/tdLsVoraMxw/0.jpg)](https://www.youtube.com/watch?v=tdLsVoraMxw "GET Udemy Courses for FREE with Python | 2 Minute Tuesday") --- -** **_Disclaimer & WARNINGS:_** +## Disclaimer & WARNINGS: -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. **THIS PROJECT IS NOT AFFILIATED WITH UDEMY.** -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 +  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. **THIS PROJECT IS NOT AFFILIATED WITH UDEMY.** +
+  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! --- -** Requirements: +## Requirements: -*** How to Install the Requirements? +### How to Install the Requirements? -**Required Python version:** [Python 3.8+](https://www.python.org/downloads/) +  __Required Python version:__ [Python 3.8+](https://www.python.org/downloads/) -**(Windows users only) Required Microsoft Visual C++ 14.0+ version:** [Microsoft Visual C++ 14.0+](https://visualstudio.microsoft.com/visual-cpp-build-tools/) +  __(Windows users only) Required Microsoft Visual C++ 14.0+ version:__ [Microsoft Visual C++ 14.0+](https://visualstudio.microsoft.com/visual-cpp-build-tools/) ![alt text](https://docs.microsoft.com/answers/storage/attachments/34873-10262.png) -**You must have pip or poetry installed. Please look up how to install them 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 @@ -63,7 +67,7 @@ get all the requirements installed in one go. Similar instructions applies for p --- -** Instructions +## Instructions Props to Davidd Sargent for making a super simple video tutorial. If you prefer written instructions then continue reading further, else click on the image below for a quick video tutorial: @@ -71,13 +75,15 @@ Props to Davidd Sargent for making a super simple video tutorial. If you prefer 1 . Install from PyPI `pip install udemy-enroller` -- Run the script and the cli will guide you through the settings required +- Run the script and the cli will guide you through the settings required. - If you decide to save the settings they will be stored in your home directory:
**Windows**: C:/Users/CurrentUserName/.udemy_enroller
**Linux**: - /home/username/.udemy_enroller - **The values in settings.yaml should be in the same language as the site you are browsing on** + /home/username/.udemy_enroller + **The values in settings.yaml should be in the same language as the site you are browsing on.** + +
2 . The script can be passed arguments: @@ -90,23 +96,29 @@ Props to Davidd Sargent for making a super simple video tutorial. If you prefer - `--max-pages=`: Max number of pages to scrape from sites before exiting the script (default is 5) - `--delete-settings`: Delete existing settings file - `--delete-cookie`: Delete the cookie file if it exists -- `--debug`: Enable debug logging +- `--debug`: Enable debug logging + +
3 . Run the script in terminal with your target runner: - `udemy_enroller` - `udemy_enroller --browser=chrome` -- `udemy_enroller --browser=chromium` +- `udemy_enroller --browser=chromium` + +
4 . The bot starts scraping the course links from the first **All Courses** page on [Tutorial Bar](https://www.tutorialbar.com/all-courses/page/1), [DiscUdemy](https://www.discudemy.com/all), [Coursevania](https://coursevania.com) and [FreebiesGlobal](https://freebiesglobal.com) and starts enrolling you to Udemy courses. After it has enrolled you to courses from the first page, it then moves to the next site page and the cycle continues. -- Stop the script by pressing ctrl+c in terminal to stop the enrollment process. +- Stop the script by pressing `ctrl+c` in terminal to stop the enrollment process. + +
-5 . _[New]_ At the end of process a detailed result is shown: +5 . *[New]* At the end of process a detailed result is shown: ``` ================== Run Statistics ================== @@ -126,85 +138,91 @@ Savings: â‚Ŧ2674.44 ## FAQs -*** 1. Can I get a specific course for free with this script? +__1. Can I get a specific course for free with this script?__ -Unfortunately no, but let me assure you that you may be lucky enough to get a +  Unfortunately no, but let me assure you that you may be lucky enough to get a particular course for free when the instructor posts its coupon code in order to promote it. Also, over time you would build a library of courses by running the script often and have all the required courses in your collection. In fact, I made this course after completing a -[Python automation course](https://www.udemy.com/course/automate/) and selenium, +[Python automation course](https://www.udemy.com/course/automate/) and [Selenium](https://www.selenium.dev/), which of course I got for free! :) +

+ +__2. How does the bot work?__ -*** 2. How does the bot work? - -The bot retrieves coupon links from Tutorial Bar, DiscUdemy and Coursevania's lists to cut the prices and +  The bot retrieves coupon links from [Tutorial Bar](https://www.tutorialbar.com/all-courses/page/1), [DiscUdemy](https://www.discudemy.com/all) and [Coursevania](https://coursevania.com)'s lists to cut the prices and then uses REST requests to authenticate and enroll to the courses. Think of it this way: Epic Games & other clients like Steam provide you a handful of games each week, for free; Only in this case, we need a coupon code -to make those courses free. +to make those courses free.

-*** 3. How frequently should you run the script? -Daily, at least once! I've painstakingly amassed over 4000 +__3. How frequently should you run the script?__ + +  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. So, a mere **0.001%** of courses are **actually paid** in my collection! Thankfully, you can get more than what I gathered in 4 years, in a matter of -weeks! 🙌đŸģ +weeks! 🙌đŸģ

+ -*** 4. Why did I create this? +__4. Why did I create this?__ -It used to be my daily habit to redeem courses and it was an extremely tedious +  It used to be my daily habit to redeem courses and it was an extremely tedious task that took around 15 minutes, for 10 courses. And then I suddenly got the idea to automate it, after I found the automation course mentioned above. I bet, -it will save your precious time too! :) +it will save your precious time too! :)

-*** 5. The code compiles successfully, but it's taking too long to work! IS there any way to fix that? -Since we are heavily dependent on a third-party site to retrieve coupons links, +__5. The code compiles successfully, but it's taking too long to work! IS there any way to fix that?__ + +  Since we are heavily dependent on a third-party site to retrieve coupons links, there may be issues when the site is down. Needless to mention the connectivity issues too. If everything is working fine, you can see the courses being -retrieved in the Python console/shell, which may take a while. +retrieved in the Python console/shell, which may take a while.

+ + +__6. Which is the best way to run the script?__ + +  It is recommended to run the script using your terminal and system python.

+ -*** 6. Which is the best way to run the script? +__7. Which branch to commit against?__ -It is recommended to run the script using your terminal and system python. +  Pull request should be made on "_develop_" branch.

-*** 7. Which branch to commit against? -Pull request should be made on "develop" branch. +__8. What's the roadmap?__ -*** 8. What's the roadmap? +  Take a look at our [Roadmap here](https://github.com/aapatre/Automatic-Udemy-Course-Enroller-GET-PAID-UDEMY-COURSES-for-FREE/projects/1) and help us on what you want or talk to us about your proposed changes.

-Take a look at our -[Roadmap here](https://github.com/aapatre/Automatic-Udemy-Course-Enroller-GET-PAID-UDEMY-COURSES-for-FREE/projects/1) -and help us on what you want or talk to us about your proposed changes. --- -** Support & Maintenance Notice +## Support & Maintenance Notice By using this repo/script, you agree that the authors and contributors are under no obligation to provide support for the script and can discontinue it's development, as and when necessary, without prior notice. --- -** Supporters +## Supporters -*** Jetbrains +### 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 [![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. -*** GitLab +### GitLab [![GitLab](https://i.imgur.com/aUWtSn4.png)](https://gitlab.com) From 233b8477bf23bf2544ab2667c1394a8e78e98c61 Mon Sep 17 00:00:00 2001 From: Nowshin <82333564+Nyctophilia58@users.noreply.github.com> Date: Fri, 14 Oct 2022 13:40:52 +0600 Subject: [PATCH 07/27] Delete README.md --- README.md | 288 ------------------------------------------------------ 1 file changed, 288 deletions(-) delete mode 100644 README.md diff --git a/README.md b/README.md deleted file mode 100644 index ff26164..0000000 --- a/README.md +++ /dev/null @@ -1,288 +0,0 @@ -[![forthebadge](https://forthebadge.com/images/badges/made-with-python.svg)](https://forthebadge.com) -[![forthebadge](https://forthebadge.com/images/badges/it-works-why.svg)](https://forthebadge.com) - -* ALPHA IS A PRE DEVELOPMENT BRANCH, DO NOT EXPECT USER FACING ISSUES TO BE ADDRESSED IN THIS BRANCH! - - -* Udemy Coupon Grabber & Course Enroller: Grab FREE Coupons! -
- -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 to PAID UDEMY COURSES, ABSOLUTELY FREE! - -**NOTE: THIS PROJECT IS NOT AFFILIATED WITH UDEMY.** - -**NOTE: THIS PROJECT WILL NOT WORK WITH NON ENGLISH UDEMY.** - -
- -The code scrapes course links and coupons from: - - - [tutorialbar.com](https://tutorialbar.com) - - [discudemy.com](https://discudemy.com) - - [coursevania.com](https://coursevania.com) - - [freebiesglobal.com](https://freebiesglobal.com) -> _New_ - - -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!** - -**We are also on _[GitLab](https://gitlab.com/the-automators/Automatic-Udemy-Course-Enroller-GET-PAID-UDEMY-COURSES-for-FREE)_** -

- -<<<<<<< HEAD -## Video Proof: -======= -### **Video Proof:** ->>>>>>> develop - -[![Udemy Auto-Course-Enroller](https://img.youtube.com/vi/tdLsVoraMxw/0.jpg)](https://www.youtube.com/watch?v=tdLsVoraMxw "GET Udemy Courses for FREE with Python | 2 Minute Tuesday") - ---- - -<<<<<<< HEAD -## Disclaimer & WARNINGS: -======= -### **Disclaimer & WARNINGS:** ->>>>>>> develop - -  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. **THIS PROJECT IS NOT AFFILIATED WITH UDEMY.** -
-  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! - ---- - -## Requirements: - -### How to Install the Requirements? - -<<<<<<< HEAD -  __Required Python version:__ [Python 3.8+](https://www.python.org/downloads/) - -  __(Windows users only) Required Microsoft Visual C++ 14.0+ version:__ [Microsoft Visual C++ 14.0+](https://visualstudio.microsoft.com/visual-cpp-build-tools/) - -![alt text](https://docs.microsoft.com/answers/storage/attachments/34873-10262.png) - -  __You must have pip or poetry installed. Please look up how to install them in your OS.__ -======= -__** Required Python version:__ [Python 3.8+](https://www.python.org/downloads/) - -__** (Windows users only) Required Microsoft Visual C++ 14.0+ version:__ [Microsoft Visual C++ 14.0+](https://visualstudio.microsoft.com/visual-cpp-build-tools/) - -![alt text](https://docs.microsoft.com/answers/storage/attachments/34873-10262.png) - -__** You must have pip or poetry installed. Please look up how to install them in your OS.__ ->>>>>>> develop - -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. Similar instructions applies for poetry. - ---- - -## Instructions - -Props to Davidd Sargent for making a super simple video tutorial. If you prefer written instructions then continue reading further, else click on the image below for a quick video tutorial: - -[![GET Udemy Courses for FREE with Python | 2 Minute Tuesday](https://i.ytimg.com/vi/6HLbqM-598k/hq720.jpg)](https://www.youtube.com/watch?v=6HLbqM-598k "pip installation of Automatic Udemy Course Enroller") - -1 . Install from PyPI `pip install udemy-enroller` - -- Run the script and the cli will guide you through the settings required. -- If you decide to save the settings they will be stored in your home directory:
- **Windows**: - C:/Users/CurrentUserName/.udemy_enroller
- **Linux**: - /home/username/.udemy_enroller - **The values in settings.yaml should be in the same language as the site you are browsing on.** - -
- -2 . The script can be passed arguments: - -- `--help`: View full list of arguments available -- `--browser=`: Run with a specific browser -- `--discudemy`: Run the discudemy scraper only -- `--coursevania`: Run the coursevania scraper only -- `--tutorialbar`: Run the tutorialbar scraper only -- `--freebiesglobal`: Run the freebiesglobal scraper only -- `--max-pages=`: Max number of pages to scrape from sites before exiting the script (default is 5) -- `--delete-settings`: Delete existing settings file -- `--delete-cookie`: Delete the cookie file if it exists -- `--debug`: Enable debug logging - -
- - -3 . Run the script in terminal with your target runner: - -- `udemy_enroller` -- `udemy_enroller --browser=chrome` -- `udemy_enroller --browser=chromium` - -
- -4 . The bot starts scraping the course links from the first **All Courses** page -on [Tutorial Bar](https://www.tutorialbar.com/all-courses/page/1), [DiscUdemy](https://www.discudemy.com/all), [Coursevania](https://coursevania.com) and [FreebiesGlobal](https://freebiesglobal.com) and starts -enrolling you to Udemy courses. After it has enrolled you to courses from the -first page, it then moves to the next site page and the cycle continues. - -- Stop the script by pressing `ctrl+c` in terminal to stop the enrollment process. -<<<<<<< HEAD - -
- -======= - -
- ->>>>>>> develop -5 . *[New]* At the end of process a detailed result is shown: - -``` -================== Run Statistics ================== - -Enrolled: 56 -Unwanted Category: 0 -Unwanted Language: 1 -Already Claimed: 93 -Expired: 7 -Total Enrolments: 1705 -Savings: â‚Ŧ2674.44 -================== Run Statistics ================== -``` - - ---- - -## FAQs - -__1. Can I get a specific course for free with this script?__ - -  Unfortunately no, but let me assure you that you may be lucky enough to get a -particular course for free when the instructor posts its coupon code in order -to promote it. Also, over time you would build a library of courses by running -the script often and have all the required courses in your collection. In fact, -I made this course after completing a -[Python automation course](https://www.udemy.com/course/automate/) and [Selenium](https://www.selenium.dev/), -which of course I got for free! :) -

- -__2. How does the bot work?__ - -  The bot retrieves coupon links from [Tutorial Bar](https://www.tutorialbar.com/all-courses/page/1), [DiscUdemy](https://www.discudemy.com/all) and [Coursevania](https://coursevania.com)'s lists to cut the prices and -then uses REST requests to authenticate and enroll to the -courses. Think of it this way: Epic Games & other clients like Steam provide you -a handful of games each week, for free; Only in this case, we need a coupon code -to make those courses free.

-<<<<<<< HEAD - - -__3. How frequently should you run the script?__ - -======= - - -__3. How frequently should you run the script?__ - ->>>>>>> develop -  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. - -So, a mere **0.001%** of courses are **actually paid** in my collection! -Thankfully, you can get more than what I gathered in 4 years, in a matter of -weeks! 🙌đŸģ

-<<<<<<< HEAD - - -__4. Why did I create this?__ - -======= - - -__4. Why did I create this?__ - ->>>>>>> develop -  It used to be my daily habit to redeem courses and it was an extremely tedious -task that took around 15 minutes, for 10 courses. And then I suddenly got the -idea to automate it, after I found the automation course mentioned above. I bet, -it will save your precious time too! :)

-<<<<<<< HEAD - - -__5. The code compiles successfully, but it's taking too long to work! IS there any way to fix that?__ - -======= - - -__5. The code compiles successfully, but it's taking too long to work! IS there any way to fix that?__ - ->>>>>>> develop -  Since we are heavily dependent on a third-party site to retrieve coupons links, -there may be issues when the site is down. Needless to mention the connectivity -issues too. If everything is working fine, you can see the courses being -retrieved in the Python console/shell, which may take a while.

- - -__6. Which is the best way to run the script?__ - -  It is recommended to run the script using your terminal and system python.

- - -__7. Which branch to commit against?__ - -  Pull request should be made on "_develop_" branch.

- - -__8. What's the roadmap?__ - -  Take a look at our [Roadmap here](https://github.com/aapatre/Automatic-Udemy-Course-Enroller-GET-PAID-UDEMY-COURSES-for-FREE/projects/1) and help us on what you want or talk to us about your proposed changes.

- - ---- - -## Support & Maintenance Notice - -By using this repo/script, you agree that the authors and contributors are under no obligation to provide support for the script and can discontinue it's development, as and when necessary, without prior notice. - ---- - -<<<<<<< HEAD -## Supporters - -### Jetbrains -======= -## ** Supporters - -### *** Jetbrains ->>>>>>> develop - -[![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. - -<<<<<<< HEAD -### GitBook -======= -### *** GitBook ->>>>>>> develop - -[![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. - -<<<<<<< HEAD -### GitLab -======= -### *** GitLab ->>>>>>> develop - -[![GitLab](https://i.imgur.com/aUWtSn4.png)](https://gitlab.com) - -Thanks to [GitLab](https://gitlab.com) for supporting us. GitLab is one of the main code hosting and CI/CD providers out there. They support the open source community through their GitLab for [Open Source program](https://about.gitlab.com/solutions/open-source/). Please check them out. From cb477f01d7b12ed4715c235a0ecc96289fccee50 Mon Sep 17 00:00:00 2001 From: Nowshin <82333564+Nyctophilia58@users.noreply.github.com> Date: Fri, 14 Oct 2022 13:49:49 +0600 Subject: [PATCH 08/27] Create README.md --- README.md | 229 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..e315651 --- /dev/null +++ b/README.md @@ -0,0 +1,229 @@ +[![forthebadge](https://forthebadge.com/images/badges/made-with-python.svg)](https://forthebadge.com) +[![forthebadge](https://forthebadge.com/images/badges/it-works-why.svg)](https://forthebadge.com) + +* ALPHA IS A PRE DEVELOPMENT BRANCH, DO NOT EXPECT USER FACING ISSUES TO BE ADDRESSED IN THIS BRANCH! + + +* Udemy Coupon Grabber & Course Enroller: Grab FREE Coupons! +
+ +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 to PAID UDEMY COURSES, ABSOLUTELY FREE! + +**NOTE: THIS PROJECT IS NOT AFFILIATED WITH UDEMY.** + +**NOTE: THIS PROJECT WILL NOT WORK WITH NON ENGLISH UDEMY.** + +
+ +The code scrapes course links and coupons from: + + - [tutorialbar.com](https://tutorialbar.com) + - [discudemy.com](https://discudemy.com) + - [coursevania.com](https://coursevania.com) + - [freebiesglobal.com](https://freebiesglobal.com) -> _New_ + + +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!** + +**We are also on _[GitLab](https://gitlab.com/the-automators/Automatic-Udemy-Course-Enroller-GET-PAID-UDEMY-COURSES-for-FREE)_** +

+ +## Video Proof: + +[![Udemy Auto-Course-Enroller](https://img.youtube.com/vi/tdLsVoraMxw/0.jpg)](https://www.youtube.com/watch?v=tdLsVoraMxw "GET Udemy Courses for FREE with Python | 2 Minute Tuesday") + +--- + +## Disclaimer & WARNINGS: + +  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. **THIS PROJECT IS NOT AFFILIATED WITH UDEMY.** +
+  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! + +--- + +## Requirements: + +### How to Install the Requirements? + +  __Required Python version:__ [Python 3.8+](https://www.python.org/downloads/) + +  __(Windows users only) Required Microsoft Visual C++ 14.0+ version:__ [Microsoft Visual C++ 14.0+](https://visualstudio.microsoft.com/visual-cpp-build-tools/) + +![alt text](https://docs.microsoft.com/answers/storage/attachments/34873-10262.png) + +  __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. Similar instructions applies for poetry. + +--- + +## Instructions + +Props to Davidd Sargent for making a super simple video tutorial. If you prefer written instructions then continue reading further, else click on the image below for a quick video tutorial: + +[![GET Udemy Courses for FREE with Python | 2 Minute Tuesday](https://i.ytimg.com/vi/6HLbqM-598k/hq720.jpg)](https://www.youtube.com/watch?v=6HLbqM-598k "pip installation of Automatic Udemy Course Enroller") + +1 . Install from PyPI `pip install udemy-enroller` + +- Run the script and the cli will guide you through the settings required. +- If you decide to save the settings they will be stored in your home directory:
+ **Windows**: + C:/Users/CurrentUserName/.udemy_enroller
+ **Linux**: + /home/username/.udemy_enroller + **The values in settings.yaml should be in the same language as the site you are browsing on.** + +
+ +2 . The script can be passed arguments: + +- `--help`: View full list of arguments available +- `--browser=`: Run with a specific browser +- `--discudemy`: Run the discudemy scraper only +- `--coursevania`: Run the coursevania scraper only +- `--tutorialbar`: Run the tutorialbar scraper only +- `--freebiesglobal`: Run the freebiesglobal scraper only +- `--max-pages=`: Max number of pages to scrape from sites before exiting the script (default is 5) +- `--delete-settings`: Delete existing settings file +- `--delete-cookie`: Delete the cookie file if it exists +- `--debug`: Enable debug logging + +
+ + +3 . Run the script in terminal with your target runner: + +- `udemy_enroller` +- `udemy_enroller --browser=chrome` +- `udemy_enroller --browser=chromium` + +
+ +4 . The bot starts scraping the course links from the first **All Courses** page +on [Tutorial Bar](https://www.tutorialbar.com/all-courses/page/1), [DiscUdemy](https://www.discudemy.com/all), [Coursevania](https://coursevania.com) and [FreebiesGlobal](https://freebiesglobal.com) and starts +enrolling you to Udemy courses. After it has enrolled you to courses from the +first page, it then moves to the next site page and the cycle continues. + +- Stop the script by pressing `ctrl+c` in terminal to stop the enrollment process. + +
+ +5 . *[New]* At the end of process a detailed result is shown: + +``` +================== Run Statistics ================== + +Enrolled: 56 +Unwanted Category: 0 +Unwanted Language: 1 +Already Claimed: 93 +Expired: 7 +Total Enrolments: 1705 +Savings: â‚Ŧ2674.44 +================== Run Statistics ================== +``` + + +--- + +## FAQs + +__1. Can I get a specific course for free with this script?__ + +  Unfortunately no, but let me assure you that you may be lucky enough to get a +particular course for free when the instructor posts its coupon code in order +to promote it. Also, over time you would build a library of courses by running +the script often and have all the required courses in your collection. In fact, +I made this course after completing a +[Python automation course](https://www.udemy.com/course/automate/) and [Selenium](https://www.selenium.dev/), +which of course I got for free! :) +

+ +__2. How does the bot work?__ + +  The bot retrieves coupon links from [Tutorial Bar](https://www.tutorialbar.com/all-courses/page/1), [DiscUdemy](https://www.discudemy.com/all) and [Coursevania](https://coursevania.com)'s lists to cut the prices and +then uses REST requests to authenticate and enroll to the +courses. Think of it this way: Epic Games & other clients like Steam provide you +a handful of games each week, for free; Only in this case, we need a coupon code +to make those courses free.

+ + +__3. How frequently should you run the script?__ + +  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. + +So, a mere **0.001%** of courses are **actually paid** in my collection! +Thankfully, you can get more than what I gathered in 4 years, in a matter of +weeks! 🙌đŸģ

+ + +__4. Why did I create this?__ + +  It used to be my daily habit to redeem courses and it was an extremely tedious +task that took around 15 minutes, for 10 courses. And then I suddenly got the +idea to automate it, after I found the automation course mentioned above. I bet, +it will save your precious time too! :)

+ + +__5. The code compiles successfully, but it's taking too long to work! IS there any way to fix that?__ + +  Since we are heavily dependent on a third-party site to retrieve coupons links, +there may be issues when the site is down. Needless to mention the connectivity +issues too. If everything is working fine, you can see the courses being +retrieved in the Python console/shell, which may take a while.

+ + +__6. Which is the best way to run the script?__ + +  It is recommended to run the script using your terminal and system python.

+ + +__7. Which branch to commit against?__ + +  Pull request should be made on "_develop_" branch.

+ + +__8. What's the roadmap?__ + +  Take a look at our [Roadmap here](https://github.com/aapatre/Automatic-Udemy-Course-Enroller-GET-PAID-UDEMY-COURSES-for-FREE/projects/1) and help us on what you want or talk to us about your proposed changes.

+ + +--- + +## Support & Maintenance Notice + +By using this repo/script, you agree that the authors and contributors are under no obligation to provide support for the script and can discontinue it's development, as and when necessary, without prior notice. + +--- + +## Supporters + +### 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. + +### GitLab + +[![GitLab](https://i.imgur.com/aUWtSn4.png)](https://gitlab.com) + +Thanks to [GitLab](https://gitlab.com) for supporting us. GitLab is one of the main code hosting and CI/CD providers out there. They support the open source community through their GitLab for [Open Source program](https://about.gitlab.com/solutions/open-source/). Please check them out. From c1260cdfab45111a5f7629c13bdf21b00bec4b3b Mon Sep 17 00:00:00 2001 From: Somogyi Krisztian Date: Fri, 14 Oct 2022 13:47:28 +0200 Subject: [PATCH 09/27] build: add .dockerignore and Dockerfile --- .dockerignore | 3 +++ Dockerfile | 10 ++++++++++ 2 files changed, 13 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a007d89 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +.git +tests +sample_settings.yaml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..436763b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.10-alpine + +WORKDIR /src + +COPY . . + +RUN apk add --no-cache build-base +RUN pip install --no-cache-dir -r requirements.txt + +CMD [ "python", "udemy_enroller.py", "--max-pages=900000"] From e74e4fa7d6c28cea59d0b71fa603e975a1f12116 Mon Sep 17 00:00:00 2001 From: cullzie Date: Mon, 17 Oct 2022 15:56:40 +0100 Subject: [PATCH 10/27] Remove empty file --- pytest.ini | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 pytest.ini diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index e69de29..0000000 From 9d6b29b20303b2559411feae114218d07f50f687 Mon Sep 17 00:00:00 2001 From: cullzie Date: Mon, 17 Oct 2022 16:00:51 +0100 Subject: [PATCH 11/27] Use pytest.ini for pytest config --- pytest.ini | 5 +++++ setup.cfg | 6 ------ 2 files changed, 5 insertions(+), 6 deletions(-) create mode 100644 pytest.ini diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..ba21a73 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,5 @@ +[pytest] +addopts = + --cov=. --cov-report xml --cov-report term +testpaths = + tests diff --git a/setup.cfg b/setup.cfg index 54543c1..432b896 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,9 +1,3 @@ -[pytest] -addopts = - --cov=. --cov-report xml --cov-report term -testpaths = - tests - [flake8] max-line-length = 120 max-complexity = 10 From 6e2b48a763118ff0773aa07e1899eff27bef1418 Mon Sep 17 00:00:00 2001 From: Somogyi Krisztian Date: Tue, 18 Oct 2022 17:00:35 +0200 Subject: [PATCH 12/27] feat: change cmd to entrypoint to allow passing arguments to the script --- Dockerfile | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 436763b..2b8fda4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,15 @@ FROM python:3.10-alpine +RUN apk add --no-cache build-base + +RUN addgroup -S enroller && adduser -S enroller -G enroller +USER enroller +RUN mkdir -p ~/.udemy_enroller + WORKDIR /src COPY . . -RUN apk add --no-cache build-base RUN pip install --no-cache-dir -r requirements.txt -CMD [ "python", "udemy_enroller.py", "--max-pages=900000"] +ENTRYPOINT [ "python", "udemy_enroller.py" ] From a076c395fcf9de926100c07da01e69d9f2207ad8 Mon Sep 17 00:00:00 2001 From: Somogyi Krisztian Date: Tue, 18 Oct 2022 17:12:03 +0200 Subject: [PATCH 13/27] docs: update readme with the usage/build instructions --- README.md | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6f7ae03..a4ab8ff 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,32 @@ Total Enrolments: 1705 Savings: â‚Ŧ2674.44 ================== Run Statistics ================== ``` - + +### Docker + +Alternatively you can run the script in docker. + +To build the image run: + +``` +docker build -t udemy_enroller . +``` + +After the build is finished you can run your container with one of the commands below (you can pass arguments as you would in the cli): + +``` +docker run -it udemy_enroller +``` + +After you entered your login credentials and settings detach from the interactive mode by pressing the `Ctrl-P` followed by `Ctrl-Q`. + +You can also create a `settings.yaml` file from the `sample_settings.yaml` and mount to the container with the command: + +``` +docker run -v $(pwd)/settings.yaml:/home/enroller/.udemy_enroller/settings.yaml udemy_enroller +``` + + --- From e4c13e69e72a6a4303fee15dd2c521bf1246b1bb Mon Sep 17 00:00:00 2001 From: Somogyi Krisztian Date: Tue, 18 Oct 2022 17:16:38 +0200 Subject: [PATCH 14/27] refactor: add settings.yaml to .dockerignore for security reasons --- .dockerignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.dockerignore b/.dockerignore index a007d89..2afce53 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,4 @@ .git tests sample_settings.yaml +settings.yaml From 2668a36cb10bf1c19de8763845eb172d6207f162 Mon Sep 17 00:00:00 2001 From: Nowshin <82333564+Nyctophilia58@users.noreply.github.com> Date: Tue, 18 Oct 2022 22:25:38 +0600 Subject: [PATCH 15/27] Delete Udemy_repo.iml --- .idea/Udemy_repo.iml | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 .idea/Udemy_repo.iml diff --git a/.idea/Udemy_repo.iml b/.idea/Udemy_repo.iml deleted file mode 100644 index a895c33..0000000 --- a/.idea/Udemy_repo.iml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file From 76e8419724c730b63b3afc548e84f9267dcfb6d2 Mon Sep 17 00:00:00 2001 From: cullzie Date: Mon, 31 Oct 2022 11:26:33 +0000 Subject: [PATCH 16/27] Add idownloadcoupon scraper --- README.md | 2 + udemy_enroller/cli.py | 32 +++++-- udemy_enroller/runner.py | 6 ++ udemy_enroller/scrapers/base_scraper.py | 6 +- udemy_enroller/scrapers/idownloadcoupon.py | 106 +++++++++++++++++++++ udemy_enroller/scrapers/manager.py | 7 ++ 6 files changed, 149 insertions(+), 10 deletions(-) create mode 100644 udemy_enroller/scrapers/idownloadcoupon.py diff --git a/README.md b/README.md index de6f84b..9f44918 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ The code scrapes course links and coupons from: - [discudemy.com](https://discudemy.com) - [coursevania.com](https://coursevania.com) - [freebiesglobal.com](https://freebiesglobal.com) + - [idownloadcoupon.com](https://idownloadcoupon.com) -> _New_ 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!** @@ -90,6 +91,7 @@ Props to Davidd Sargent for making a super simple video tutorial. If you prefer - `--coursevania`: Run the coursevania scraper only - `--tutorialbar`: Run the tutorialbar scraper only - `--freebiesglobal`: Run the freebiesglobal scraper only +- `--idownloadcoupon`: Run the idownloadcoupon scraper only - `--max-pages=`: Max number of pages to scrape from sites before exiting the script (default is 5) - `--delete-settings`: Delete existing settings file - `--delete-cookie`: Delete the cookie file if it exists diff --git a/udemy_enroller/cli.py b/udemy_enroller/cli.py index f0328cc..c2a8147 100644 --- a/udemy_enroller/cli.py +++ b/udemy_enroller/cli.py @@ -62,31 +62,35 @@ def log_os_version(): def determine_if_scraper_enabled( + idownloadcoupon_enabled: bool, freebiesglobal_enabled: bool, tutorialbar_enabled: bool, discudemy_enabled: bool, coursevania_enabled: bool, -) -> Tuple[bool, bool, bool, bool]: +) -> Tuple[bool, bool, bool, bool, bool]: """ Determine what scrapers should be enabled and disabled. :return: tuple containing boolean of what scrapers should run """ if ( - not freebiesglobal_enabled + not idownloadcoupon_enabled + and not freebiesglobal_enabled and not tutorialbar_enabled and not discudemy_enabled and not coursevania_enabled ): # Set all to True ( + idownloadcoupon_enabled, freebiesglobal_enabled, tutorialbar_enabled, discudemy_enabled, coursevania_enabled, - ) = (True, True, True, True) + ) = (True, True, True, True, True) return ( + idownloadcoupon_enabled, freebiesglobal_enabled, tutorialbar_enabled, discudemy_enabled, @@ -96,6 +100,7 @@ def determine_if_scraper_enabled( def run( browser: str, + idownloadcoupon_enabled: bool, freebiesglobal_enabled: bool, tutorialbar_enabled: bool, discudemy_enabled: bool, @@ -108,6 +113,7 @@ def run( Run the udemy enroller script. :param str browser: Name of the browser we want to create a driver for + :param bool idownloadcoupon_enabled: :param bool freebiesglobal_enabled: :param bool tutorialbar_enabled: :param bool discudemy_enabled: @@ -123,6 +129,7 @@ def run( redeem_courses_ui( dm.driver, settings, + idownloadcoupon_enabled, freebiesglobal_enabled, tutorialbar_enabled, discudemy_enabled, @@ -132,6 +139,7 @@ def run( else: redeem_courses( settings, + idownloadcoupon_enabled, freebiesglobal_enabled, tutorialbar_enabled, discudemy_enabled, @@ -155,6 +163,12 @@ def parse_args() -> Namespace: choices=ALL_VALID_BROWSER_STRINGS, help="Browser to use for Udemy Enroller", ) + parser.add_argument( + "--idownloadcoupon", + action="store_true", + default=False, + help="Run idownloadcoupon scraper", + ) parser.add_argument( "--freebiesglobal", action="store_true", @@ -167,28 +181,24 @@ def parse_args() -> Namespace: default=False, help="Run tutorialbar scraper", ) - parser.add_argument( "--discudemy", action="store_true", default=False, help="Run discudemy scraper", ) - parser.add_argument( "--coursevania", action="store_true", default=False, help="Run coursevania scraper", ) - parser.add_argument( "--max-pages", type=int, default=5, help="Max pages to scrape from sites (if pagination exists) (Default is 5)", ) - parser.add_argument( "--delete-settings", action="store_true", @@ -222,15 +232,21 @@ def main(): log_python_version() log_os_version() ( + idownloadcoupon_enabled, freebiesglobal_enabled, tutorialbar_enabled, discudemy_enabled, coursevania_enabled, ) = determine_if_scraper_enabled( - args.freebiesglobal, args.tutorialbar, args.discudemy, args.coursevania + args.idownloadcoupon, + args.freebiesglobal, + args.tutorialbar, + args.discudemy, + args.coursevania, ) run( args.browser, + idownloadcoupon_enabled, freebiesglobal_enabled, tutorialbar_enabled, discudemy_enabled, diff --git a/udemy_enroller/runner.py b/udemy_enroller/runner.py index d962460..748d5f6 100644 --- a/udemy_enroller/runner.py +++ b/udemy_enroller/runner.py @@ -69,6 +69,7 @@ def _redeem_courses(settings: Settings, scrapers: ScraperManager) -> None: def redeem_courses( settings: Settings, + idownloadcoupon_enabled: bool, freebiesglobal_enabled: bool, tutorialbar_enabled: bool, discudemy_enabled: bool, @@ -79,6 +80,7 @@ def redeem_courses( Wrap _redeem_courses to catch unhandled exceptions. :param Settings settings: Core settings used for Udemy + :param bool idownloadcoupon_enabled: Boolean signifying if idownloadcoupon scraper should run :param bool freebiesglobal_enabled: Boolean signifying if freebiesglobal scraper should run :param bool tutorialbar_enabled: Boolean signifying if tutorialbar scraper should run :param bool discudemy_enabled: Boolean signifying if discudemy scraper should run @@ -88,6 +90,7 @@ def redeem_courses( """ try: scrapers = ScraperManager( + idownloadcoupon_enabled, freebiesglobal_enabled, tutorialbar_enabled, discudemy_enabled, @@ -161,6 +164,7 @@ def _redeem_courses_ui( def redeem_courses_ui( driver, settings: Settings, + idownloadcoupon_enabled: bool, freebiesglobal_enabled: bool, tutorialbar_enabled: bool, discudemy_enabled: bool, @@ -172,6 +176,7 @@ def redeem_courses_ui( :param WebDriver driver: WebDriver to use to complete enrolment :param Settings settings: Core settings used for Udemy + :param bool idownloadcoupon_enabled: Boolean signifying if idownloadcoupon scraper should run :param bool freebiesglobal_enabled: Boolean signifying if freebiesglobal scraper should run :param bool tutorialbar_enabled: Boolean signifying if tutorialbar scraper should run :param bool discudemy_enabled: Boolean signifying if discudemy scraper should run @@ -181,6 +186,7 @@ def redeem_courses_ui( """ try: scrapers = ScraperManager( + idownloadcoupon_enabled, freebiesglobal_enabled, tutorialbar_enabled, discudemy_enabled, diff --git a/udemy_enroller/scrapers/base_scraper.py b/udemy_enroller/scrapers/base_scraper.py index f18b900..ed5ccd7 100644 --- a/udemy_enroller/scrapers/base_scraper.py +++ b/udemy_enroller/scrapers/base_scraper.py @@ -89,7 +89,9 @@ async def wrapper(self): try: response = await func(self) except Exception as e: - logger.error(f"Error while running {self.scraper_name} scraper: {e}") + logger.exception( + f"Error while running {self.scraper_name} scraper: {e}" + ) self.is_complete() return [] end_time = datetime.datetime.utcnow() @@ -126,7 +128,7 @@ def max_pages_reached(self) -> bool: return should_run @staticmethod - def validate_coupon_url(url) -> Optional[str]: + def validate_coupon_url(url: str) -> Optional[str]: """ Validate the udemy coupon url passed in. diff --git a/udemy_enroller/scrapers/idownloadcoupon.py b/udemy_enroller/scrapers/idownloadcoupon.py new file mode 100644 index 0000000..606e796 --- /dev/null +++ b/udemy_enroller/scrapers/idownloadcoupon.py @@ -0,0 +1,106 @@ +"""IDownloadCoupon scraper.""" +import asyncio +import urllib.parse +from typing import List + +from bs4 import BeautifulSoup + +from udemy_enroller.http_utils import http_get +from udemy_enroller.logger import get_logger +from udemy_enroller.scrapers.base_scraper import BaseScraper + +logger = get_logger() + + +class IDownloadCouponScraper(BaseScraper): + """Contains any logic related to scraping of site data.""" + + DOMAIN = "https://www.idownloadcoupon.com" + + def __init__(self, enabled, max_pages=None): + """Initialize.""" + super().__init__() + self.scraper_name = "idownloadcoupon" + if not enabled: + self.set_state_disabled() + self.last_page = None + self.max_pages = max_pages + + @BaseScraper.time_run + async def run(self) -> List: + """ + Run the steps to scrape links. + + :return: list of udemy coupon links + """ + links = await self.get_links() + self.max_pages_reached() + return links + + async def get_links(self): + """ + Scrape udemy links. + + :return: List of udemy course urls + """ + self.current_page += 1 + course_links = await self.get_course_links( + f"{self.DOMAIN}/page/{self.current_page}/" + ) + + logger.info( + f"Page: {self.current_page} of {self.last_page} scraped from {self.scraper_name}" + ) + udemy_links = await self.gather_udemy_course_links(course_links) + + for counter, course in enumerate(udemy_links): + logger.debug(f"Received Link {counter + 1} : {course}") + + return udemy_links + + async def get_course_links(self, url: str) -> List: + """ + Get the url of pages which contain the udemy link we want to get. + + :param str url: The url to scrape data from + :return: list of pages that contain Udemy coupons + """ + text = await http_get(url) + if text is not None: + soup = BeautifulSoup(text.decode("utf-8"), "html.parser") + + links = soup.find_all("li", class_="product") + course_links = [link.find_all("a")[1].get("href") for link in links] + + self.last_page = int( + soup.find("ul", class_="page-numbers") + .find_all("a", class_="page-numbers")[-2] + .text + ) + + return course_links + + @classmethod + async def get_udemy_course_link(cls, url: str) -> str: + """ + Get the udemy course link. + + :param str url: The url to scrape data from + :return: Coupon link of the udemy course + """ + urls = url.split("murl=") + if urls: + return cls.validate_coupon_url(urllib.parse.unquote(urls[1])) + + async def gather_udemy_course_links(self, courses: List[str]): + """ + Async fetching of the udemy course links. + + :param list courses: A list of course links we want to fetch the udemy links for + :return: list of udemy links + """ + return [ + link + for link in await asyncio.gather(*map(self.get_udemy_course_link, courses)) + if link is not None + ] diff --git a/udemy_enroller/scrapers/manager.py b/udemy_enroller/scrapers/manager.py index 4d5aeba..19f7a7e 100644 --- a/udemy_enroller/scrapers/manager.py +++ b/udemy_enroller/scrapers/manager.py @@ -6,6 +6,7 @@ from udemy_enroller.scrapers.coursevania import CoursevaniaScraper from udemy_enroller.scrapers.discudemy import DiscUdemyScraper from udemy_enroller.scrapers.freebiesglobal import FreebiesglobalScraper +from udemy_enroller.scrapers.idownloadcoupon import IDownloadCouponScraper from udemy_enroller.scrapers.tutorialbar import TutorialBarScraper @@ -14,6 +15,7 @@ class ScraperManager: def __init__( self, + idownloadcoupon_enabled, freebiesglobal_enabled, tutorialbar_enabled, discudemy_enabled, @@ -21,6 +23,10 @@ def __init__( max_pages, ): """Initialize.""" + self.idownloadcoupons_scraper = IDownloadCouponScraper( + idownloadcoupon_enabled, max_pages=max_pages + ) + self.freebiesglobal_scraper = FreebiesglobalScraper( freebiesglobal_enabled, max_pages=max_pages ) @@ -34,6 +40,7 @@ def __init__( coursevania_enabled, max_pages=max_pages ) self._scrapers = ( + self.idownloadcoupons_scraper, self.freebiesglobal_scraper, self.tutorialbar_scraper, self.discudemy_scraper, From 46499bf9d11c923f51d2fee83b58ad3cf1961e4d Mon Sep 17 00:00:00 2001 From: cullzie Date: Mon, 31 Oct 2022 11:42:37 +0000 Subject: [PATCH 17/27] Add isort profile to pyproject.toml --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index fe4c65b..e2d1ef7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,10 @@ bumpver = "^2022.1116" flake8 = "^5.0.4" flake8-bugbear = "^22.9.23" flake8-docstrings = "^1.6.0" +flake8-isort = "^5.0.0" + +[tool.isort] +profile = "black" [tool.bumpver] current_version = "4.1.2" From c64207a35893aa991de586fbfc9ec4c8e6b0e8c9 Mon Sep 17 00:00:00 2001 From: Voulz <12561988+Voulz@users.noreply.github.com> Date: Fri, 25 Nov 2022 21:38:39 +1100 Subject: [PATCH 18/27] Fix a bug where we try to access an invalid page from Udemy which raises an exception For some reason, the for loop is up to total_pages +2 and not total_pages +1. Instead of going this route, I saw that the response contains a "next" key which is None when there is no next page to load, so I replaced the for loop with a while loop which fixed my issue. --- udemy_enroller/udemy_rest.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/udemy_enroller/udemy_rest.py b/udemy_enroller/udemy_rest.py index ba5a991..08c5abd 100644 --- a/udemy_enroller/udemy_rest.py +++ b/udemy_enroller/udemy_rest.py @@ -220,10 +220,12 @@ def load_my_courses(self) -> List: all_courses = list() page_size = 100 - my_courses = self.my_courses(1, page_size) + page = 1 + my_courses = self.my_courses(page, page_size) all_courses.extend(my_courses["results"]) - total_pages = my_courses["count"] // page_size - for page in range(2, total_pages + 2): + + while ("next" in my_courses and my_courses["next"] != None): + page += 1 my_courses = self.my_courses(page, page_size) if "results" in my_courses: all_courses.extend(my_courses["results"]) From 009488f4624a6f255d449c54ab09133ff85ce14b Mon Sep 17 00:00:00 2001 From: Voulz <12561988+Voulz@users.noreply.github.com> Date: Sat, 26 Nov 2022 18:09:48 +1100 Subject: [PATCH 19/27] Fixing some code styles from PR Review --- udemy_enroller/udemy_rest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/udemy_enroller/udemy_rest.py b/udemy_enroller/udemy_rest.py index 08c5abd..ad52162 100644 --- a/udemy_enroller/udemy_rest.py +++ b/udemy_enroller/udemy_rest.py @@ -224,7 +224,7 @@ def load_my_courses(self) -> List: my_courses = self.my_courses(page, page_size) all_courses.extend(my_courses["results"]) - while ("next" in my_courses and my_courses["next"] != None): + while "next" in my_courses and my_courses["next"] is not None: page += 1 my_courses = self.my_courses(page, page_size) if "results" in my_courses: From 2f5eafa6913108a9a1983c4202da928a3c59b125 Mon Sep 17 00:00:00 2001 From: Asunnya Date: Thu, 29 Dec 2022 11:46:15 -0300 Subject: [PATCH 20/27] fix no cookie avaiable, Now getting csrftoken from response cookies --- udemy_enroller/udemy_rest.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/udemy_enroller/udemy_rest.py b/udemy_enroller/udemy_rest.py index 980b2b4..93d588d 100644 --- a/udemy_enroller/udemy_rest.py +++ b/udemy_enroller/udemy_rest.py @@ -129,8 +129,7 @@ def login(self, retry=False) -> None: if cookie_details is None: response = self.udemy_scraper.get(self.LOGIN_URL) soup = BeautifulSoup(response.content, "html.parser") - csrf_element = soup.find("input", {"name": "csrfmiddlewaretoken"}) or {} - csrf_token = csrf_element.get("value") + csrf_token = response.cookies["csrftoken"] if csrf_token is None: raise Exception("Unable to get csrf_token") From 5747573fd6393fef16217b4865ffbfa1cfe28a7b Mon Sep 17 00:00:00 2001 From: cullzie Date: Fri, 6 Jan 2023 16:05:06 +0000 Subject: [PATCH 21/27] Fix idownloadcoupon and disable coursevania (offlined) --- udemy_enroller/scrapers/coursevania.py | 4 ++-- udemy_enroller/scrapers/idownloadcoupon.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/udemy_enroller/scrapers/coursevania.py b/udemy_enroller/scrapers/coursevania.py index 44adca5..8507537 100644 --- a/udemy_enroller/scrapers/coursevania.py +++ b/udemy_enroller/scrapers/coursevania.py @@ -22,8 +22,8 @@ def __init__(self, enabled, max_pages=None): """Initialize.""" super().__init__() self.scraper_name = "coursevania" - if not enabled: - self.set_state_disabled() + # TODO: Always set disabled as it is not longer online. + self.set_state_disabled() self.last_page = None self.max_pages = max_pages self._nonce = None diff --git a/udemy_enroller/scrapers/idownloadcoupon.py b/udemy_enroller/scrapers/idownloadcoupon.py index 606e796..84becd1 100644 --- a/udemy_enroller/scrapers/idownloadcoupon.py +++ b/udemy_enroller/scrapers/idownloadcoupon.py @@ -75,7 +75,7 @@ async def get_course_links(self, url: str) -> List: self.last_page = int( soup.find("ul", class_="page-numbers") .find_all("a", class_="page-numbers")[-2] - .text + .text.replace(",", "") ) return course_links From 20b48afba9a0557dd9e1d937e47850cc62a66825 Mon Sep 17 00:00:00 2001 From: cullzie Date: Fri, 6 Jan 2023 16:17:12 +0000 Subject: [PATCH 22/27] Fix Dockerfile entrypoint --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 2b8fda4..114d5a9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,4 +12,4 @@ COPY . . RUN pip install --no-cache-dir -r requirements.txt -ENTRYPOINT [ "python", "udemy_enroller.py" ] +ENTRYPOINT [ "python", "run_enroller.py" ] From 7a04fc9e5d359db0fb4e2b764c39ddca145a9f17 Mon Sep 17 00:00:00 2001 From: Isadora Date: Fri, 6 Jan 2023 15:52:13 -0300 Subject: [PATCH 23/27] Update udemy_enroller/udemy_rest.py Co-authored-by: cullzie <20401034+cullzie@users.noreply.github.com> --- udemy_enroller/udemy_rest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/udemy_enroller/udemy_rest.py b/udemy_enroller/udemy_rest.py index 93d588d..1c1a14a 100644 --- a/udemy_enroller/udemy_rest.py +++ b/udemy_enroller/udemy_rest.py @@ -129,7 +129,7 @@ def login(self, retry=False) -> None: if cookie_details is None: response = self.udemy_scraper.get(self.LOGIN_URL) soup = BeautifulSoup(response.content, "html.parser") - csrf_token = response.cookies["csrftoken"] + csrf_token = response.cookies.get("csrftoken") if csrf_token is None: raise Exception("Unable to get csrf_token") From 3320c339d65d9040f438fe49010a92f1100dd56b Mon Sep 17 00:00:00 2001 From: cullzie <20401034+cullzie@users.noreply.github.com> Date: Thu, 12 Jan 2023 20:21:19 +0000 Subject: [PATCH 24/27] Prepare changelog and re-enable coursevania --- CHANGELOG.md | 8 ++++++++ udemy_enroller/scrapers/coursevania.py | 4 ++-- udemy_enroller/udemy_rest.py | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f17dd20..3730e56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ 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). +## [4.1.3] - 2023-01-12 + +### Added +- Fixing issues with enrollment and scraping +- Tidying up of the code structure + ## [4.1.2] - 2022-06-03 ### Added @@ -116,6 +122,8 @@ can continue as normal project running locally. Suitable for users who are not looking forward to contribute. +[4.1.3]: + https://github.com/aapatre/Automatic-Udemy-Course-Enroller-GET-PAID-UDEMY-COURSES-for-FREE/releases/tag/v4.1.3 [4.1.2]: https://github.com/aapatre/Automatic-Udemy-Course-Enroller-GET-PAID-UDEMY-COURSES-for-FREE/releases/tag/v4.1.2 [4.1.1]: diff --git a/udemy_enroller/scrapers/coursevania.py b/udemy_enroller/scrapers/coursevania.py index 8507537..44adca5 100644 --- a/udemy_enroller/scrapers/coursevania.py +++ b/udemy_enroller/scrapers/coursevania.py @@ -22,8 +22,8 @@ def __init__(self, enabled, max_pages=None): """Initialize.""" super().__init__() self.scraper_name = "coursevania" - # TODO: Always set disabled as it is not longer online. - self.set_state_disabled() + if not enabled: + self.set_state_disabled() self.last_page = None self.max_pages = max_pages self._nonce = None diff --git a/udemy_enroller/udemy_rest.py b/udemy_enroller/udemy_rest.py index af410ec..554ce72 100644 --- a/udemy_enroller/udemy_rest.py +++ b/udemy_enroller/udemy_rest.py @@ -222,7 +222,7 @@ def load_my_courses(self) -> List: page = 1 my_courses = self.my_courses(page, page_size) all_courses.extend(my_courses["results"]) - + while "next" in my_courses and my_courses["next"] is not None: page += 1 my_courses = self.my_courses(page, page_size) From 38a6565acfd5eb6a30fd906232c6e7f3083aa33e Mon Sep 17 00:00:00 2001 From: cullzie <20401034+cullzie@users.noreply.github.com> Date: Thu, 12 Jan 2023 20:21:27 +0000 Subject: [PATCH 25/27] Bump version 4.1.2 -> 4.1.3 --- pyproject.toml | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e2d1ef7..2bc5dbf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "udemy-enroller" -version = "4.1.2" +version = "4.1.3" description = "" authors = ["aapatre ", "fakeid ", "cullzie "] @@ -31,7 +31,7 @@ flake8-isort = "^5.0.0" profile = "black" [tool.bumpver] -current_version = "4.1.2" +current_version = "4.1.3" version_pattern = "MAJOR.MINOR.PATCH" commit_message = "Bump version {old_version} -> {new_version}" commit = true diff --git a/setup.py b/setup.py index 3125f4f..abe3cb5 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( name="udemy-enroller", - version="4.1.2", + version="4.1.3", long_description=long_description, long_description_content_type="text/markdown", author="aapatre", From afbaf332697d494af0ce5d7bae0a944fb31df633 Mon Sep 17 00:00:00 2001 From: cullzie <20401034+cullzie@users.noreply.github.com> Date: Tue, 7 Mar 2023 20:05:44 +0000 Subject: [PATCH 26/27] Fix dates in CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3730e56..1eaad55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ 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). -## [4.1.3] - 2023-01-12 +## [4.1.3] - 2023-03-07 ### Added - Fixing issues with enrollment and scraping From b47e3f855f1bc5b43bce1b2fe627cfe15da5b25b Mon Sep 17 00:00:00 2001 From: cullzie <20401034+cullzie@users.noreply.github.com> Date: Tue, 7 Mar 2023 20:41:25 +0000 Subject: [PATCH 27/27] Update to use poetry for builds --- pyproject.toml | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2bc5dbf..26466de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,16 @@ name = "udemy-enroller" version = "4.1.3" description = "" +homepage = "https://github.com/aapatre/Automatic-Udemy-Course-Enroller-GET-PAID-UDEMY-COURSES-for-FREE" authors = ["aapatre ", "fakeid ", "cullzie "] +readme = "README.md" +keywords = ["udemy", "education", "enroll"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Education", + "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", + "Programming Language :: Python :: 3.8", +] [tool.poetry.dependencies] python = "^3.8" @@ -48,5 +57,8 @@ push = false ] [build-system] -requires = ["setuptools", "wheel"] -build-backend = "setuptools.build_meta" +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry.scripts] +udemy_enroller = "udemy_enroller.cli:main"