Compare commits

...

16 Commits

3 changed files with 154 additions and 54 deletions

View File

@ -1,6 +1,6 @@
# Package
version = "1.0.0"
version = "1.5.0"
author = "Adam Blažek"
description = "CLI client for Bakaláři"
license = "GPL-3.0"
@ -12,5 +12,8 @@ bin = @["bk"]
# Dependencies
requires "nim >= 1.2.4"
requires "cligen >= 1.2.0"
requires "colorize >= 0.2.0"
requires "elvis >= 0.2.0"
requires "zero_functional >= 1.2.0"

View File

@ -1,11 +1,21 @@
import httpcore, httpclient, json, strformat, tables, times, uri
import elvis
import httpcore, httpclient, json, options, strformat, tables, times, uri
import elvis, zero_functional
type
Bakalari* = ref object
website*: Uri
accessToken*: string
refreshToken*: string
Grade* = object
text*: string
weight*: int
caption*: string
addTime*: DateTime
editTime*: DateTime
GradedSubject* = object
name*: string
abbrev*: string
grades*: seq[Grade]
Homework* = object
id*: string
subject*: string
@ -36,6 +46,7 @@ type
room*: Room
subject*: Subject
teacher*: Teacher
change*: Option[string]
Day* = object
lessons*: seq[Lesson]
dayOfWeek*: int
@ -112,6 +123,23 @@ proc postEndpoint*(bakalari: Bakalari, endpoint: string): JsonNode =
bakalari.renewTokens
result = body.parseJson
iterator grades*(bakalari: Bakalari): GradedSubject =
let root = bakalari.getEndpoint("marks")
for subjectNode in root{"Subjects"}:
yield GradedSubject(
name: subjectNode{"Subject"}{"Name"}.getStr,
abbrev: subjectNode{"Subject"}{"Abbrev"}.getStr,
grades: subjectNode{"Marks"} --> (gradeNode) --> map(
Grade(
text: gradeNode{"MarkText"}.getStr,
weight: gradeNode{"Weight"}.getInt,
caption: gradeNode{"Caption"}.getStr,
addTime: gradeNode{"MarkDate"}.getStr.parse(iso8601),
editTime: gradeNode{"EditDate"}.getStr.parse(iso8601),
)
)
)
iterator homework*(bakalari: Bakalari): Homework =
let root = bakalari.getEndpoint("homeworks")
for node in root{"Homeworks"}.getElems:
@ -134,8 +162,11 @@ iterator messages*(bakalari: Bakalari): Message =
sentTime: node{"SentDate"}.getStr.parse(iso8601)
)
proc timetable*(bakalari: Bakalari, permanent: bool): Timetable =
let root = bakalari.getEndpoint(permanent ? "timetable/permanent" ! "timetable/actual")
proc timetable*(bakalari: Bakalari, permanent: bool, date: Option[DateTime] = none(DateTime)): Timetable =
var endpoint = permanent ? "timetable/permanent" ! "timetable/actual"
if date.isSome:
endpoint &= "?date=" & date.unsafeGet.format("yyyy-MM-dd")
let root = bakalari.getEndpoint(endpoint)
var hours: Table[int, Hour]
for hourNode in root{"Hours"}:
hours[hourNode{"Id"}.getInt] = Hour(
@ -169,6 +200,8 @@ proc timetable*(bakalari: Bakalari, permanent: bool): Timetable =
lesson.room = rooms.getOrDefault(lessonNode{"RoomId"}.getStr, invalidRoom)
lesson.subject = subjects.getOrDefault(lessonNode{"SubjectId"}.getStr, invalidSubject)
lesson.teacher = teachers.getOrDefault(lessonNode{"TeacherId"}.getStr, invalidTeacher)
if lessonNode{"Change"}.kind != JNull:
lesson.change = some(lessonNode{"Change"}{"Description"}.getStr)
day.lessons.add lesson
day.dayOfWeek = dayNode{"DayOfWeek"}.getInt
day.date = dayNode{"Date"}.getStr.parse(iso8601)

View File

@ -1,6 +1,7 @@
import json, os, times, unicode, uri
import cligen
from bakalari as baka import newBakalari
import htmlparser, json, net, options, os, strscans, times, unicode, uri, xmltree
from strutils import ffDecimal, formatFloat, replace
import cligen, colorize, elvis
from bakalari as baka import Grade, newBakalari
type
Config = object
@ -20,31 +21,40 @@ const
7: "Sun",
]
proc error(message: string) {.noReturn.} =
stderr.writeLine("Error: " & message)
quit QuitFailure
proc loadConfig(configFile = defaultConfigFile): Config =
try:
defaultConfigFile.readFile.parseJson.to(Config)
configFile.readFile.parseJson.to(Config)
except IOError:
stderr.writeLine "Error: Can't read the config file. Are you signed in?"
quit QuitFailure
error "Can't read the config file. Are you signed in?"
except JsonParsingError, JsonKindError:
stderr.writeLine "Error: Invalid config file. Make sure it's in JSON format."
quit QuitFailure
error "Invalid config file. Make sure it's in JSON format."
except KeyError:
stderr.writeLine "Error: The config file doesn't contain all necessary fields."
quit QuitFailure
error "The config file doesn't contain all necessary fields."
proc saveConfig(configFile = defaultConfigFile, config: Config) =
try:
configFile.writeFile((%*config).pretty)
except IOError:
stderr.writeLine "Error: Can't write the config file."
quit QuitFailure
error "Can't write the config file."
template withBakalari(configFile: string, body: untyped): untyped =
var config = configFile.loadConfig
let bakalari {.inject.} = newBakalari(config.website.parseUri, config.refreshToken)
try:
body
finally:
config.refreshToken = bakalari.refreshToken
configFile.saveConfig(config)
proc signin(
website: string,
username: string,
password: string,
configFile = defaultConfigFile,
website: string,
username: string,
password: string,
configFile = defaultConfigFile,
) =
## sign in to Bakaláři and save the credentials
let
@ -56,49 +66,95 @@ proc signin(
)
configFile.saveConfig(config)
proc average(grades: seq[Grade]): float =
var sum, weightSum: float
for grade in grades:
var value: int
if scanf(grade.text, "$i-", value):
sum += (value.float + 0.5) * grade.weight.float
weightSum += grade.weight.float
elif scanf(grade.text, "$i", value):
sum += value.float * grade.weight.float
weightSum += grade.weight.float
return weightSum ? (sum / weightSum) ! 0.0
proc grades(
configFile = defaultConfigFile,
) =
## display the list of grades and calculate averages
withBakalari(configFile):
for subject in baka.grades(bakalari):
stdout.writeLine subject.name.fgLightYellow & " " & subject.grades.average.formatFloat(ffDecimal, 2)
for grade in subject.grades:
var line = ""
line &= grade.text.align(2).fgLightMagenta
line &= " "
line &= ($grade.weight).align(2).fgLightCyan
line &= " "
line &= grade.caption
stdout.writeLine line
proc homework(
configFile = defaultConfigFile,
) =
## display the list of homework
var config = configFile.loadConfig
let bakalari = newBakalari(config.website.parseUri, config.refreshToken)
config.refreshToken = bakalari.refreshToken
configFile.saveConfig(config)
for homework in baka.homework(bakalari):
stdout.writeLine "----------------------------------------------------------------"
stdout.writeLine homework.startTime.format("yyyy-MM-dd") & " / " & homework.endTime.format("yyyy-MM-dd")
stdout.writeLine homework.subject
stdout.writeLine homework.teacher
stdout.writeLine homework.content
withBakalari(configFile):
for homework in baka.homework(bakalari):
stdout.writeLine "----------------------------------------------------------------".fgLightGray
stdout.writeLine (homework.startTime.format("yyyy-MM-dd") & " / " & homework.endTime.format("yyyy-MM-dd")).fgLightCyan
stdout.writeLine homework.subject.fgLightYellow
stdout.writeLine homework.teacher.fgLightMagenta
stdout.writeLine homework.content
proc messages(
configFile = defaultConfigFile,
) =
withBakalari(defaultConfigFile):
for message in baka.messages(bakalari):
stdout.writeLine "----------------------------------------------------------------".fgLightGray
stdout.writeLine message.sentTime.format("yyyy-MM-dd") .fgLightCyan
stdout.writeLine message.title.fgLightYellow
stdout.writeLine message.sender.fgLightMagenta
stdout.writeLine message.text.replace("<br />", "{{br}}").parseHtml.innerText.replace("{{br}}", "\n")
proc timetable(
configFile = defaultConfigFile,
date = "",
nextWeek = false,
oneDay = false,
permanent = false,
) =
## display the timetable (for the current week by default)
var config = configFile.loadConfig
let bakalari = newBakalari(config.website.parseUri, config.refreshToken)
config.refreshToken = bakalari.refreshToken
configFile.saveConfig(config)
let timetable = baka.timetable(bakalari, permanent)
for day in timetable.days:
stdout.writeLine dayNames[day.dayOfWeek] & " " & day.date.format("yyyy-MM-dd")
for lesson in day.lessons:
var line = ""
line &= lesson.hour.number
line &= ". "
line &= lesson.hour.beginTime.align(5, '0'.Rune)
line &= "-"
line &= lesson.hour.endTime.align(5, '0'.Rune)
line &= " "
line &= lesson.subject.abbrev.align(4)
line &= " "
line &= lesson.teacher.abbrev.align(4)
line &= " "
line &= lesson.room.abbrev.align(4)
stdout.writeLine line
let date = if ?date:
try:
date.parse("yyyyMMdd")
except TimeParseError:
error "Can't parse the date. Make sure it's in YYYYMMDD format."
else:
nextWeek ? (now() + 1.weeks) ! now()
withBakalari(configFile):
let timetable = baka.timetable(bakalari, permanent, some(date))
for day in timetable.days:
if oneDay:
if date.format("yyyyMMdd") != day.date.format("yyyyMMdd"):
continue
stdout.writeLine dayNames[day.dayOfWeek] & " " & day.date.format("yyyy-MM-dd")
for lesson in day.lessons:
var line = ""
line &= lesson.hour.number
line &= ". "
line &= lesson.hour.beginTime.align(5, '0'.Rune).fgLightCyan
line &= "-".fgLightCyan
line &= lesson.hour.endTime.align(5, '0'.Rune).fgLightCyan
line &= " "
line &= lesson.subject.abbrev.align(4).fgLightYellow
line &= " "
line &= lesson.teacher.abbrev.align(4).fgLightMagenta
line &= " "
line &= lesson.room.abbrev.align(4).fgLightGreen
stdout.writeLine line
if lesson.change.isSome:
stdout.writeLine ("^ " & lesson.change.unsafeGet.fgLightRed).fgLightRed
try:
dispatchMulti(
@ -108,16 +164,24 @@ try:
"password": "your password (will not be stored anywhere)",
"config-file": "where to store the credentials",
}],
[grades, help = {
"config-file": "where the credentials are stored",
}],
[homework, help = {
"config-file": "where the credentials are stored",
}],
[messages, help = {
"config-file": "where the credentials are stored",
}],
[timetable, help = {
"config-file": "where the credentials are stored",
"date": "any date inside the week you want to display, in YYYYMMDD format (defaults to today)",
"nextWeek": "display the next week rather that the current week (overriden by --date)",
"one-day": "display only the specified day",
"permanent": "display the permanent timetable",
}],
)
except OSError:
stderr.writeLine "Error: Generic OS error. Check your internet connection."
quit QuitFailure
error "Generic OS error. Check your internet connection."
except SslError:
error "Connection interrupted. Check your internet connection."