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
.venv
#----------------Custom
.env

View File

@@ -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...
```

View File

@@ -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]

View File

@@ -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')

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 },
]
[[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" },
]