feat: better cli

This commit is contained in:
eneller
2025-04-14 11:50:30 +02:00
parent 82d87ba771
commit ce9a476adf
5 changed files with 64 additions and 13 deletions

2
.gitignore vendored
View File

@@ -8,3 +8,5 @@ wheels/
# Virtual environments # Virtual environments
.venv .venv
#----------------Custom
.env

View File

@@ -17,13 +17,24 @@ This will provide the `uulm` command.
``` ```
Usage: uulm [OPTIONS] COMMAND [ARGS]... 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: Options:
-u, --username TEXT -u, --username TEXT
-p, --password TEXT -p, --password TEXT
--headful Show the browser window --headful Show the browser window
-d, --debug Set the log level to DEBUG
--help Show this message and exit. --help Show this message and exit.
Commands: Commands:
campusonline Interact with the module tree in Campusonline campusonline Interact with the module tree in Campusonline.
coronang Automatically register for courses on CoronaNG 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...
``` ```

View File

@@ -9,6 +9,7 @@ dependencies = [
"click>=8.1.8", "click>=8.1.8",
"pandas>=2.2.3", "pandas>=2.2.3",
"playwright>=1.51.0", "playwright>=1.51.0",
"python-dotenv>=1.1.0",
"questionary>=2.1.0", "questionary>=2.1.0",
] ]
[project.scripts] [project.scripts]

View File

@@ -2,6 +2,8 @@ import asyncclick as click
import questionary import questionary
from playwright.async_api import async_playwright, Playwright from playwright.async_api import async_playwright, Playwright
import pandas as pd import pandas as pd
from dotenv import load_dotenv
from enum import Enum from enum import Enum
import asyncio import asyncio
@@ -10,9 +12,9 @@ import logging
from datetime import datetime from datetime import datetime
load_dotenv() # take environment variables
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
CORONANG_VERSION='v1.8.00'
Selection = Enum('Selection', ['TREE_WALK', 'TREE_LEAF', 'ITEM_SELECTED']) Selection = Enum('Selection', ['TREE_WALK', 'TREE_LEAF', 'ITEM_SELECTED'])
async def selection_or_walk(options): async def selection_or_walk(options):
@@ -33,18 +35,24 @@ async def run_playwright(headless: bool):
await browser.close() await browser.close()
@click.group() @click.group()
@click.option('--username','-u') @click.option('--username','-u', envvar='UULM_USERNAME', prompt='Enter your kiz username:')
@click.option('--password','-p') @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('--headful', is_flag=True, help='Show the browser window')
@click.option('--debug', '-d', is_flag=True, help='Set the log level to DEBUG') @click.option('--debug', '-d', is_flag=True, help='Set the log level to DEBUG')
@click.pass_context @click.pass_context
async def cli(ctx, username, password, headful, debug): 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') logging.basicConfig(level=logging.WARNING,format='%(asctime)s - %(levelname)s - %(message)s')
if(debug): logger.setLevel(logging.DEBUG) if(debug): logger.setLevel(logging.DEBUG)
ctx.ensure_object(dict) 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() @cli.command()
@@ -65,17 +73,18 @@ async def campusonline(ctx):
selection = await selection_or_walk(options) selection = await selection_or_walk(options)
print(selection) print(selection)
sleep(2) sleep(2)
raise NotImplementedError
@cli.command() @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.option('--before', '-b', type=int, default=10, help='How many seconds before the target time to start')
@click.pass_context @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". 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. 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) logger.info('Parsed input times as %s', target_times)
async for page, browser, context in run_playwright(ctx.obj['HEADLESS']): async for page, browser, context in run_playwright(ctx.obj['HEADLESS']):
await page.goto("https://campusonline.uni-ulm.de/CoronaNG/user/mycorona.html") 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") server_time = datetime.strptime(server_str.split().pop(), "%H:%M:%S")
break break
exit() raise NotImplementedError
await page.locator("input[name=\"uid\"]").click() await page.locator("input[name=\"uid\"]").click()
await page.locator("input[name=\"uid\"]").fill(ctx.obj['USERNAME']) await page.locator("input[name=\"uid\"]").fill(ctx.obj['USERNAME'])
await page.locator("input[name=\"password\"]").click() 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.get_by_role("cell", name="An Markierten teilnehmen Ausf").get_by_role("button").click()
await page.reload() 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() @cli.command()
@click.argument('filename', type=click.Path(exists=True)) @click.argument('filename', type=click.Path(exists=True))
@click.option('--target_lp', '-t', type=int, default=74, help='Target number of n credits needed') @click.option('--target_lp', '-t', type=int, default=74, help='Target number of n credits needed')

11
uv.lock generated
View File

@@ -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 }, { 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]] [[package]]
name = "pytz" name = "pytz"
version = "2025.2" version = "2025.2"
@@ -285,6 +294,7 @@ dependencies = [
{ name = "click" }, { name = "click" },
{ name = "pandas" }, { name = "pandas" },
{ name = "playwright" }, { name = "playwright" },
{ name = "python-dotenv" },
{ name = "questionary" }, { name = "questionary" },
] ]
@@ -294,6 +304,7 @@ requires-dist = [
{ name = "click", specifier = ">=8.1.8" }, { name = "click", specifier = ">=8.1.8" },
{ name = "pandas", specifier = ">=2.2.3" }, { name = "pandas", specifier = ">=2.2.3" },
{ name = "playwright", specifier = ">=1.51.0" }, { name = "playwright", specifier = ">=1.51.0" },
{ name = "python-dotenv", specifier = ">=1.1.0" },
{ name = "questionary", specifier = ">=2.1.0" }, { name = "questionary", specifier = ">=2.1.0" },
] ]