Compare commits
16 Commits
Author | SHA1 | Date |
---|---|---|
Adam Blažek | e64c2e9c59 | |
Adam Blažek | ecdce60091 | |
Adam Blažek | 02addb3db9 | |
Adam Blažek | 2005610c7c | |
Adam Blažek | e38fe7d1c3 | |
Adam Blažek | 0911886487 | |
Adam Blažek | 13919442ec | |
Adam Blažek | d4e470b442 | |
Adam Blažek | 8e32e6e76b | |
Adam Blažek | 2c9e35b7ef | |
Adam Blažek | 571ebdb1eb | |
Adam Blažek | a2e71b5cb9 | |
Adam Blažek | caf580779a | |
Adam Blažek | 128a0e4862 | |
Adam Blažek | 6157817025 | |
Adam Blažek | e826c8f90a |
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
162
src/bk.nim
162
src/bk.nim
|
@ -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."
|
||||
|
|
Loading…
Reference in New Issue