From ce9a476adfafdfc28c45b444a9502f6625f813c2 Mon Sep 17 00:00:00 2001 From: eneller Date: Mon, 14 Apr 2025 11:50:30 +0200 Subject: [PATCH] feat: better cli --- .gitignore | 2 ++ README.md | 15 +++++++++++-- pyproject.toml | 1 + src/uulm_utils/main.py | 48 ++++++++++++++++++++++++++++++++---------- uv.lock | 11 ++++++++++ 5 files changed, 64 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 505a3b1..9b615a1 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ wheels/ # Virtual environments .venv +#----------------Custom +.env diff --git a/README.md b/README.md index 6d1e681..8edfe90 100644 --- a/README.md +++ b/README.md @@ -17,13 +17,24 @@ This will provide the `uulm` command. ``` Usage: uulm [OPTIONS] COMMAND [ARGS]... + Passing username and password is supported through multiple ways as entering + your password visibly into your shell history is discouraged for security + reasons. + + - using environment variables `UULM_USERNAME`, `UULM_PASSWORD` + - using a `.env` file in the current working directory with the same variables + - interactive mode, if none of the above was specified + Options: -u, --username TEXT -p, --password TEXT --headful Show the browser window + -d, --debug Set the log level to DEBUG --help Show this message and exit. Commands: - campusonline Interact with the module tree in Campusonline - coronang Automatically register for courses on CoronaNG + campusonline Interact with the module tree in Campusonline. + coronang Automatically register for courses on CoronaNG by... + grades Calculate your weighted grade using the best n credits. + sport Automatically register for courses on the AktivKonzepte... ``` diff --git a/pyproject.toml b/pyproject.toml index 324e36a..082fddb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,7 @@ dependencies = [ "click>=8.1.8", "pandas>=2.2.3", "playwright>=1.51.0", + "python-dotenv>=1.1.0", "questionary>=2.1.0", ] [project.scripts] diff --git a/src/uulm_utils/main.py b/src/uulm_utils/main.py index ca9effc..5f38ea8 100644 --- a/src/uulm_utils/main.py +++ b/src/uulm_utils/main.py @@ -2,6 +2,8 @@ import asyncclick as click import questionary from playwright.async_api import async_playwright, Playwright import pandas as pd +from dotenv import load_dotenv + from enum import Enum import asyncio @@ -10,9 +12,9 @@ import logging from datetime import datetime +load_dotenv() # take environment variables logger = logging.getLogger(__name__) -CORONANG_VERSION='v1.8.00' Selection = Enum('Selection', ['TREE_WALK', 'TREE_LEAF', 'ITEM_SELECTED']) async def selection_or_walk(options): @@ -33,19 +35,25 @@ async def run_playwright(headless: bool): await browser.close() @click.group() -@click.option('--username','-u') -@click.option('--password','-p') +@click.option('--username','-u', envvar='UULM_USERNAME', prompt='Enter your kiz username:') +@click.option('--password','-p', envvar='UULM_PASSWORD', prompt='Enter your kiz password:', hide_input=True) @click.option('--headful', is_flag=True, help='Show the browser window') @click.option('--debug', '-d', is_flag=True, help='Set the log level to DEBUG') @click.pass_context async def cli(ctx, username, password, headful, debug): + ''' + Passing username and password is supported through multiple ways + as entering your password visibly into your shell history is discouraged for security reasons. + + \b + - using environment variables `UULM_USERNAME`, `UULM_PASSWORD` + - using a `.env` file in the current working directory with the same variables + - interactive mode, if none of the above was specified + ''' logging.basicConfig(level=logging.WARNING,format='%(asctime)s - %(levelname)s - %(message)s') if(debug): logger.setLevel(logging.DEBUG) ctx.ensure_object(dict) - if ctx.invoked_subcommand != 'grades': - ctx.obj['USERNAME'] = username or await questionary.text('Enter your kiz username:').ask_async() - ctx.obj['PASSWORD'] = password or await questionary.password('Enter your kiz password:').ask_async() - ctx.obj['HEADLESS'] = not headful + ctx.obj['HEADLESS'] = not headful @cli.command() @click.pass_context @@ -65,17 +73,18 @@ async def campusonline(ctx): selection = await selection_or_walk(options) print(selection) sleep(2) + raise NotImplementedError @cli.command() -@click.argument('times', nargs=-1, required=True) +@click.argument('target_times', nargs=-1, type=click.DateTime( ['%H:%M:%S']), required=True) @click.option('--before', '-b', type=int, default=10, help='How many seconds before the target time to start') @click.pass_context -async def coronang(ctx, times, before): +async def coronang(ctx, target_times, before): ''' Automatically register for courses on CoronaNG by specifying one or more timestamps of the format "HH:MM:SS". Please beware that CoronaNG only allows one active session at all times. ''' - target_times = sorted([datetime.strptime(t, "%H:%M:%S") for t in times]) + CORONANG_VERSION='v1.8.00' logger.info('Parsed input times as %s', target_times) async for page, browser, context in run_playwright(ctx.obj['HEADLESS']): await page.goto("https://campusonline.uni-ulm.de/CoronaNG/user/mycorona.html") @@ -90,7 +99,7 @@ async def coronang(ctx, times, before): server_time = datetime.strptime(server_str.split().pop(), "%H:%M:%S") break - exit() + raise NotImplementedError await page.locator("input[name=\"uid\"]").click() await page.locator("input[name=\"uid\"]").fill(ctx.obj['USERNAME']) await page.locator("input[name=\"password\"]").click() @@ -102,6 +111,23 @@ async def coronang(ctx, times, before): await page.get_by_role("cell", name="An Markierten teilnehmen Ausf").get_by_role("button").click() await page.reload() +@cli.command() +@click.argument('target_times', nargs=-1, type=click.DateTime( ["%H:%M:%S"]), required=True) +@click.option('--target_course', '-t', multiple=True, required=True, help='Unique course name to register for. Can be passed multiple times') +@click.option('--before', '-b', type=int, default=10, help='How many seconds before the target time to start') +@click.pass_context +async def sport(ctx, target_times, target_course, before): + ''' + Automatically register for courses on the AktivKonzepte Hochschulsport Platform + by specifying one or more timestamps of the format "HH:MM:SS". + ''' + print(target_course) + # TODO Check Version in HTML Head + logger.info('Parsed input times as %s', target_times) + async for page, browser, context in run_playwright(ctx.obj['HEADLESS']): + pass + raise NotImplementedError + @cli.command() @click.argument('filename', type=click.Path(exists=True)) @click.option('--target_lp', '-t', type=int, default=74, help='Target number of n credits needed') diff --git a/uv.lock b/uv.lock index 4ccfd5c..6a2a45d 100644 --- a/uv.lock +++ b/uv.lock @@ -219,6 +219,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, ] +[[package]] +name = "python-dotenv" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 }, +] + [[package]] name = "pytz" version = "2025.2" @@ -285,6 +294,7 @@ dependencies = [ { name = "click" }, { name = "pandas" }, { name = "playwright" }, + { name = "python-dotenv" }, { name = "questionary" }, ] @@ -294,6 +304,7 @@ requires-dist = [ { name = "click", specifier = ">=8.1.8" }, { name = "pandas", specifier = ">=2.2.3" }, { name = "playwright", specifier = ">=1.51.0" }, + { name = "python-dotenv", specifier = ">=1.1.0" }, { name = "questionary", specifier = ">=2.1.0" }, ]