Migrate sdk main module to coroutines

This commit is contained in:
Mikołaj Pich 2020-06-14 23:33:39 +02:00
parent 23ece778fe
commit e626b49d95
18 changed files with 261 additions and 349 deletions

View file

@ -29,6 +29,11 @@ allprojects {
maven { url "https://jitpack.io" }
}
dependencies {
implementation "org.slf4j:slf4j-api:$slf4j"
testImplementation "org.slf4j:slf4j-simple:$slf4j"
}
version = PUBLISH_VERSION
group = "io.github.wulkanowy"
@ -84,9 +89,7 @@ subprojects {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-rx2:1.3.7'
implementation "io.reactivex.rxjava2:rxjava:2.2.19"
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp3"
compileOnly "org.threeten:threetenbp:$threetenbp:no-tzdb"

View file

@ -12,7 +12,4 @@ dependencies {
implementation "pl.droidsonroids.retrofit2:converter-jspoon:$jspoon"
implementation "com.squareup.okhttp3:okhttp-urlconnection:$okhttp3"
implementation "org.slf4j:slf4j-api:$slf4j"
testImplementation "org.slf4j:slf4j-simple:$slf4j"
}

View file

@ -1,18 +1,13 @@
package io.github.wulkanowy.sdk
import io.github.wulkanowy.sdk.exception.FeatureNotAvailableException
import io.github.wulkanowy.sdk.exception.ScrapperExceptionTransformer
import io.github.wulkanowy.sdk.mapper.*
import io.github.wulkanowy.sdk.mobile.Mobile
import io.github.wulkanowy.sdk.pojo.*
import io.github.wulkanowy.sdk.scrapper.Scrapper
import io.reactivex.Completable
import io.reactivex.Maybe
import io.reactivex.Observable
import io.reactivex.Single
import kotlinx.coroutines.rx2.rxSingle
import okhttp3.Interceptor
import okhttp3.logging.HttpLoggingInterceptor
import org.slf4j.LoggerFactory
import org.threeten.bp.LocalDate
import org.threeten.bp.LocalDateTime
@ -152,6 +147,11 @@ class Sdk {
scrapper.emptyCookieJarInterceptor = value
}
companion object {
@JvmStatic
private val logger = LoggerFactory.getLogger(this::class.java)
}
private val interceptors: MutableList<Pair<Interceptor, Boolean>> = mutableListOf()
fun setSimpleHttpLogger(logger: (String) -> Unit) {
@ -174,39 +174,37 @@ class Sdk {
}
}
fun getPasswordResetCaptchaCode(registerBaseUrl: String, symbol: String) = rxSingle { scrapper.getPasswordResetCaptcha(registerBaseUrl, symbol) }
suspend fun getPasswordResetCaptchaCode(registerBaseUrl: String, symbol: String) = scrapper.getPasswordResetCaptcha(registerBaseUrl, symbol)
fun sendPasswordResetRequest(registerBaseUrl: String, symbol: String, email: String, captchaCode: String): Single<String> {
return rxSingle { scrapper.sendPasswordResetRequest(registerBaseUrl, symbol, email, captchaCode) }
suspend fun sendPasswordResetRequest(registerBaseUrl: String, symbol: String, email: String, captchaCode: String): String {
return scrapper.sendPasswordResetRequest(registerBaseUrl, symbol, email, captchaCode)
}
fun getStudentsFromMobileApi(token: String, pin: String, symbol: String, firebaseToken: String, apiKey: String = ""): Single<List<Student>> {
return rxSingle { mobile.getStudents(mobile.getCertificate(token, pin, symbol, buildTag, androidVersion, firebaseToken), apiKey).mapStudents(symbol) }
suspend fun getStudentsFromMobileApi(token: String, pin: String, symbol: String, firebaseToken: String, apiKey: String = ""): List<Student> {
return mobile.getStudents(mobile.getCertificate(token, pin, symbol, buildTag, androidVersion, firebaseToken), apiKey).mapStudents(symbol)
}
fun getStudentsFromScrapper(email: String, password: String, scrapperBaseUrl: String, symbol: String = "Default"): Single<List<Student>> {
suspend fun getStudentsFromScrapper(email: String, password: String, scrapperBaseUrl: String, symbol: String = "Default"): List<Student> {
return scrapper.let {
it.baseUrl = scrapperBaseUrl
it.email = email
it.password = password
it.symbol = symbol
rxSingle { it.getStudents().mapStudents() }.compose(ScrapperExceptionTransformer())
it.getStudents().mapStudents()
}
}
fun getStudentsHybrid(
suspend fun getStudentsHybrid(
email: String,
password: String,
scrapperBaseUrl: String,
firebaseToken: String,
startSymbol: String = "Default",
apiKey: String = ""
): Single<List<Student>> {
): List<Student> {
return getStudentsFromScrapper(email, password, scrapperBaseUrl, startSymbol)
.compose(ScrapperExceptionTransformer())
.map { students -> students.distinctBy { it.symbol } }
.flatMapObservable { Observable.fromIterable(it) }
.flatMapSingle { scrapperStudent ->
.distinctBy { it.symbol }
.map { scrapperStudent ->
scrapper.let {
it.symbol = scrapperStudent.symbol
it.schoolSymbol = scrapperStudent.schoolSymbol
@ -215,201 +213,172 @@ class Sdk {
it.classId = scrapperStudent.classId
it.loginType = Scrapper.LoginType.valueOf(scrapperStudent.loginType.name)
}
rxSingle { scrapper.getToken() }.compose(ScrapperExceptionTransformer())
.flatMap { getStudentsFromMobileApi(it.token, it.pin, it.symbol, firebaseToken, apiKey) }
.map { apiStudents ->
apiStudents.map { student ->
student.copy(
loginMode = Mode.HYBRID,
loginType = scrapperStudent.loginType,
scrapperBaseUrl = scrapperStudent.scrapperBaseUrl
)
}
}
}.toList().map { it.flatten() }
val token = scrapper.getToken()
getStudentsFromMobileApi(token.token, token.pin, token.symbol, firebaseToken, apiKey).map { student ->
student.copy(
loginMode = Mode.HYBRID,
loginType = scrapperStudent.loginType,
scrapperBaseUrl = scrapperStudent.scrapperBaseUrl
)
}
}.toList().flatten()
}
fun getSemesters(now: LocalDate = LocalDate.now()): Single<List<Semester>> {
return rxSingle {
when (mode) {
Mode.HYBRID, Mode.SCRAPPER -> scrapper.getSemesters().mapSemesters()
Mode.API -> mobile.getStudents().mapSemesters(studentId, now)
}
}.compose(ScrapperExceptionTransformer())
}
fun getAttendance(startDate: LocalDate, endDate: LocalDate, semesterId: Int): Single<List<Attendance>> {
return rxSingle {
when (mode) {
Mode.SCRAPPER -> scrapper.getAttendance(startDate, endDate).mapAttendance()
Mode.HYBRID, Mode.API -> mobile.getAttendance(startDate, endDate, semesterId).mapAttendance(mobile.getDictionaries())
}
}.compose(ScrapperExceptionTransformer())
}
fun getAttendanceSummary(subjectId: Int? = -1): Single<List<AttendanceSummary>> {
suspend fun getSemesters(now: LocalDate = LocalDate.now()): List<Semester> {
return when (mode) {
Mode.HYBRID, Mode.SCRAPPER -> rxSingle { scrapper.getAttendanceSummary(subjectId).mapAttendanceSummary() }.compose(ScrapperExceptionTransformer())
Mode.HYBRID, Mode.SCRAPPER -> scrapper.getSemesters().mapSemesters()
Mode.API -> mobile.getStudents().mapSemesters(studentId, now)
}
}
suspend fun getAttendance(startDate: LocalDate, endDate: LocalDate, semesterId: Int): List<Attendance> {
return when (mode) {
Mode.SCRAPPER -> scrapper.getAttendance(startDate, endDate).mapAttendance()
Mode.HYBRID, Mode.API -> mobile.getAttendance(startDate, endDate, semesterId).mapAttendance(mobile.getDictionaries())
}
}
suspend fun getAttendanceSummary(subjectId: Int? = -1): List<AttendanceSummary> {
return when (mode) {
Mode.HYBRID, Mode.SCRAPPER -> scrapper.getAttendanceSummary(subjectId).mapAttendanceSummary()
Mode.API -> throw FeatureNotAvailableException("Attendance summary is not available in API mode")
}
}
fun excuseForAbsence(absents: List<Absent>, content: String? = null): Single<Boolean> {
suspend fun excuseForAbsence(absents: List<Absent>, content: String? = null): Boolean {
return when (mode) {
Mode.HYBRID, Mode.SCRAPPER -> rxSingle { scrapper.excuseForAbsence(absents.mapToScrapperAbsent(), content) }.compose(ScrapperExceptionTransformer())
Mode.HYBRID, Mode.SCRAPPER -> scrapper.excuseForAbsence(absents.mapToScrapperAbsent(), content)
Mode.API -> throw FeatureNotAvailableException("Absence excusing is not available in API mode")
}
}
fun getSubjects(): Single<List<Subject>> {
return rxSingle {
when (mode) {
Mode.HYBRID, Mode.SCRAPPER -> scrapper.getSubjects().mapSubjects()
Mode.API -> mobile.getDictionaries().subjects.mapSubjects()
}
}.compose(ScrapperExceptionTransformer())
}
fun getExams(start: LocalDate, end: LocalDate, semesterId: Int): Single<List<Exam>> {
return rxSingle {
when (mode) {
Mode.SCRAPPER -> scrapper.getExams(start, end).mapExams()
Mode.HYBRID, Mode.API -> mobile.getExams(start, end, semesterId).mapExams(mobile.getDictionaries())
}
}.compose(ScrapperExceptionTransformer())
}
fun getGrades(semesterId: Int): Single<Pair<List<Grade>, List<GradeSummary>>> {
return rxSingle {
when (mode) {
Mode.SCRAPPER -> scrapper.getGrades(semesterId).mapGrades()
Mode.HYBRID, Mode.API -> mobile.getGrades(semesterId).mapGrades(mobile.getDictionaries())
}
}.compose(ScrapperExceptionTransformer())
}
fun getGradesDetails(semesterId: Int): Single<List<Grade>> {
return rxSingle {
when (mode) {
Mode.SCRAPPER -> scrapper.getGradesDetails(semesterId).mapGradesDetails()
Mode.HYBRID, Mode.API -> mobile.getGradesDetails(semesterId).mapGradesDetails(mobile.getDictionaries())
}
}.compose(ScrapperExceptionTransformer())
}
fun getGradesSummary(semesterId: Int): Single<List<GradeSummary>> {
return rxSingle {
when (mode) {
Mode.SCRAPPER -> scrapper.getGradesSummary(semesterId).mapGradesSummary()
Mode.HYBRID, Mode.API -> mobile.getGradesSummary(semesterId).mapGradesSummary(mobile.getDictionaries())
}
}.compose(ScrapperExceptionTransformer())
}
fun getGradesAnnualStatistics(semesterId: Int): Single<List<GradeStatistics>> {
suspend fun getSubjects(): List<Subject> {
return when (mode) {
Mode.HYBRID, Mode.SCRAPPER -> rxSingle { scrapper.getGradesAnnualStatistics(semesterId).mapGradeStatistics() }.compose(ScrapperExceptionTransformer())
Mode.HYBRID, Mode.SCRAPPER -> scrapper.getSubjects().mapSubjects()
Mode.API -> mobile.getDictionaries().subjects.mapSubjects()
}
}
suspend fun getExams(start: LocalDate, end: LocalDate, semesterId: Int): List<Exam> {
return when (mode) {
Mode.SCRAPPER -> scrapper.getExams(start, end).mapExams()
Mode.HYBRID, Mode.API -> mobile.getExams(start, end, semesterId).mapExams(mobile.getDictionaries())
}
}
suspend fun getGrades(semesterId: Int): Pair<List<Grade>, List<GradeSummary>> {
return when (mode) {
Mode.SCRAPPER -> scrapper.getGrades(semesterId).mapGrades()
Mode.HYBRID, Mode.API -> mobile.getGrades(semesterId).mapGrades(mobile.getDictionaries())
}
}
suspend fun getGradesDetails(semesterId: Int): List<Grade> {
return when (mode) {
Mode.SCRAPPER -> scrapper.getGradesDetails(semesterId).mapGradesDetails()
Mode.HYBRID, Mode.API -> mobile.getGradesDetails(semesterId).mapGradesDetails(mobile.getDictionaries())
}
}
suspend fun getGradesSummary(semesterId: Int): List<GradeSummary> {
return when (mode) {
Mode.SCRAPPER -> scrapper.getGradesSummary(semesterId).mapGradesSummary()
Mode.HYBRID, Mode.API -> mobile.getGradesSummary(semesterId).mapGradesSummary(mobile.getDictionaries())
}
}
suspend fun getGradesAnnualStatistics(semesterId: Int): List<GradeStatistics> {
return when (mode) {
Mode.HYBRID, Mode.SCRAPPER -> scrapper.getGradesAnnualStatistics(semesterId).mapGradeStatistics()
Mode.API -> throw FeatureNotAvailableException("Class grades annual statistics is not available in API mode")
}
}
fun getGradesPartialStatistics(semesterId: Int): Single<List<GradeStatistics>> {
suspend fun getGradesPartialStatistics(semesterId: Int): List<GradeStatistics> {
return when (mode) {
Mode.HYBRID, Mode.SCRAPPER -> rxSingle { scrapper.getGradesPartialStatistics(semesterId).mapGradeStatistics() }.compose(ScrapperExceptionTransformer())
Mode.HYBRID, Mode.SCRAPPER -> scrapper.getGradesPartialStatistics(semesterId).mapGradeStatistics()
Mode.API -> throw FeatureNotAvailableException("Class grades partial statistics is not available in API mode")
}
}
fun getGradesPointsStatistics(semesterId: Int): Single<List<GradePointsStatistics>> {
suspend fun getGradesPointsStatistics(semesterId: Int): List<GradePointsStatistics> {
return when (mode) {
Mode.HYBRID, Mode.SCRAPPER -> rxSingle { scrapper.getGradesPointsStatistics(semesterId).mapGradePointsStatistics() }.compose(ScrapperExceptionTransformer())
Mode.HYBRID, Mode.SCRAPPER -> scrapper.getGradesPointsStatistics(semesterId).mapGradePointsStatistics()
Mode.API -> throw FeatureNotAvailableException("Class grades points statistics is not available in API mode")
}
}
fun getHomework(start: LocalDate, end: LocalDate, semesterId: Int = 0): Single<List<Homework>> {
return rxSingle {
when (mode) {
Mode.SCRAPPER -> scrapper.getHomework(start, end).mapHomework()
Mode.HYBRID, Mode.API -> mobile.getHomework(start, end, semesterId).mapHomework(mobile.getDictionaries())
}
}.compose(ScrapperExceptionTransformer())
}
fun getNotes(semesterId: Int): Single<List<Note>> {
return rxSingle {
when (mode) {
Mode.SCRAPPER -> scrapper.getNotes().mapNotes()
Mode.HYBRID, Mode.API -> mobile.getNotes(semesterId).mapNotes(mobile.getDictionaries())
}
}.compose(ScrapperExceptionTransformer())
}
fun getRegisteredDevices(): Single<List<Device>> {
suspend fun getHomework(start: LocalDate, end: LocalDate, semesterId: Int = 0): List<Homework> {
return when (mode) {
Mode.HYBRID, Mode.SCRAPPER -> rxSingle { scrapper.getRegisteredDevices().mapDevices() }.compose(ScrapperExceptionTransformer())
Mode.SCRAPPER -> scrapper.getHomework(start, end).mapHomework()
Mode.HYBRID, Mode.API -> mobile.getHomework(start, end, semesterId).mapHomework(mobile.getDictionaries())
}
}
suspend fun getNotes(semesterId: Int): List<Note> {
return when (mode) {
Mode.SCRAPPER -> scrapper.getNotes().mapNotes()
Mode.HYBRID, Mode.API -> mobile.getNotes(semesterId).mapNotes(mobile.getDictionaries())
}
}
suspend fun getRegisteredDevices(): List<Device> {
return when (mode) {
Mode.HYBRID, Mode.SCRAPPER -> scrapper.getRegisteredDevices().mapDevices()
Mode.API -> throw FeatureNotAvailableException("Devices management is not available in API mode")
}
}
fun getToken(): Single<Token> {
suspend fun getToken(): Token {
return when (mode) {
Mode.HYBRID, Mode.SCRAPPER -> rxSingle { scrapper.getToken().mapToken() }.compose(ScrapperExceptionTransformer())
Mode.HYBRID, Mode.SCRAPPER -> scrapper.getToken().mapToken()
Mode.API -> throw FeatureNotAvailableException("Devices management is not available in API mode")
}
}
fun unregisterDevice(id: Int): Single<Boolean> {
suspend fun unregisterDevice(id: Int): Boolean {
return when (mode) {
Mode.HYBRID, Mode.SCRAPPER -> rxSingle { scrapper.unregisterDevice(id) }.compose(ScrapperExceptionTransformer())
Mode.HYBRID, Mode.SCRAPPER -> scrapper.unregisterDevice(id)
Mode.API -> throw FeatureNotAvailableException("Devices management is not available in API mode")
}
}
fun getTeachers(semesterId: Int): Single<List<Teacher>> {
return rxSingle {
when (mode) {
Mode.SCRAPPER -> scrapper.getTeachers().mapTeachers()
Mode.HYBRID, Mode.API -> mobile.getTeachers(studentId, semesterId).mapTeachers(mobile.getDictionaries())
}
}.compose(ScrapperExceptionTransformer())
suspend fun getTeachers(semesterId: Int): List<Teacher> {
return when (mode) {
Mode.SCRAPPER -> scrapper.getTeachers().mapTeachers()
Mode.HYBRID, Mode.API -> mobile.getTeachers(studentId, semesterId).mapTeachers(mobile.getDictionaries())
}
}
fun getSchool(): Single<School> {
suspend fun getSchool(): School {
return when (mode) {
Mode.HYBRID, Mode.SCRAPPER -> rxSingle { scrapper.getSchool().mapSchool() }.compose(ScrapperExceptionTransformer())
Mode.HYBRID, Mode.SCRAPPER -> scrapper.getSchool().mapSchool()
Mode.API -> throw FeatureNotAvailableException("School info is not available in API mode")
}
}
fun getStudentInfo(): Single<StudentInfo> {
return rxSingle {
when (mode) {
Mode.HYBRID, Mode.SCRAPPER -> scrapper.getStudentInfo().mapStudent()
Mode.API -> throw FeatureNotAvailableException("Student info is not available in API mode")
}
}.compose(ScrapperExceptionTransformer())
suspend fun getStudentInfo(): StudentInfo {
return when (mode) {
Mode.HYBRID, Mode.SCRAPPER -> scrapper.getStudentInfo().mapStudent()
Mode.API -> throw FeatureNotAvailableException("Student info is not available in API mode")
}
}
fun getReportingUnits(): Single<List<ReportingUnit>> {
return rxSingle {
when (mode) {
Mode.HYBRID, Mode.SCRAPPER -> scrapper.getReportingUnits().mapReportingUnits()
Mode.API -> mobile.getStudents().mapReportingUnits(studentId)
}
}.compose(ScrapperExceptionTransformer())
suspend fun getReportingUnits(): List<ReportingUnit> {
return when (mode) {
Mode.HYBRID, Mode.SCRAPPER -> scrapper.getReportingUnits().mapReportingUnits()
Mode.API -> mobile.getStudents().mapReportingUnits(studentId)
}
}
fun getRecipients(unitId: Int, role: Int = 2): Single<List<Recipient>> {
return rxSingle {
when (mode) {
Mode.HYBRID, Mode.SCRAPPER -> scrapper.getRecipients(unitId, role).mapRecipients()
Mode.API -> mobile.getDictionaries().teachers.mapRecipients(unitId)
}
}.compose(ScrapperExceptionTransformer())
suspend fun getRecipients(unitId: Int, role: Int = 2): List<Recipient> {
return when (mode) {
Mode.HYBRID, Mode.SCRAPPER -> scrapper.getRecipients(unitId, role).mapRecipients()
Mode.API -> mobile.getDictionaries().teachers.mapRecipients(unitId)
}
}
fun getMessages(folder: Folder, start: LocalDateTime, end: LocalDateTime): Single<List<Message>> {
suspend fun getMessages(folder: Folder, start: LocalDateTime, end: LocalDateTime): List<Message> {
return when (folder) {
Folder.RECEIVED -> getReceivedMessages(start, end)
Folder.SENT -> getSentMessages(start, end)
@ -417,181 +386,166 @@ class Sdk {
}
}
fun getReceivedMessages(start: LocalDateTime, end: LocalDateTime): Single<List<Message>> {
return rxSingle {
when (mode) {
Mode.HYBRID, Mode.SCRAPPER -> scrapper.getReceivedMessages().mapMessages() // TODO
Mode.API -> mobile.getMessages(start, end).mapMessages(mobile.getDictionaries())
}
}.compose(ScrapperExceptionTransformer())
}
fun getSentMessages(start: LocalDateTime, end: LocalDateTime): Single<List<Message>> {
return rxSingle {
when (mode) {
Mode.HYBRID, Mode.SCRAPPER -> scrapper.getSentMessages().mapMessages()
Mode.API -> mobile.getMessagesSent(start, end).mapMessages(mobile.getDictionaries())
}
}.compose(ScrapperExceptionTransformer())
}
fun getDeletedMessages(start: LocalDateTime, end: LocalDateTime): Single<List<Message>> {
return rxSingle {
when (mode) {
Mode.HYBRID, Mode.SCRAPPER -> scrapper.getDeletedMessages().mapMessages()
Mode.API -> mobile.getMessagesDeleted(start, end).mapMessages(mobile.getDictionaries())
}
}.compose(ScrapperExceptionTransformer())
}
fun getMessageRecipients(messageId: Int, senderId: Int): Single<List<Recipient>> {
suspend fun getReceivedMessages(start: LocalDateTime, end: LocalDateTime): List<Message> {
return when (mode) {
Mode.HYBRID, Mode.SCRAPPER -> rxSingle { scrapper.getMessageRecipients(messageId, senderId).mapRecipients() }.compose(ScrapperExceptionTransformer())
Mode.HYBRID, Mode.SCRAPPER -> scrapper.getReceivedMessages().mapMessages() // TODO
Mode.API -> mobile.getMessages(start, end).mapMessages(mobile.getDictionaries())
}
}
suspend fun getSentMessages(start: LocalDateTime, end: LocalDateTime): List<Message> {
return when (mode) {
Mode.HYBRID, Mode.SCRAPPER -> scrapper.getSentMessages().mapMessages()
Mode.API -> mobile.getMessagesSent(start, end).mapMessages(mobile.getDictionaries())
}
}
suspend fun getDeletedMessages(start: LocalDateTime, end: LocalDateTime): List<Message> {
return when (mode) {
Mode.HYBRID, Mode.SCRAPPER -> scrapper.getDeletedMessages().mapMessages()
Mode.API -> mobile.getMessagesDeleted(start, end).mapMessages(mobile.getDictionaries())
}
}
suspend fun getMessageRecipients(messageId: Int, senderId: Int): List<Recipient> {
return when (mode) {
Mode.HYBRID, Mode.SCRAPPER -> scrapper.getMessageRecipients(messageId, senderId).mapRecipients()
Mode.API -> TODO()
}
}
fun getMessageDetails(messageId: Int, folderId: Int, read: Boolean = false, id: Int? = null): Single<MessageDetails> {
return rxSingle {
when (mode) {
Mode.HYBRID, Mode.SCRAPPER -> scrapper.getMessageDetails(messageId, folderId, read, id).mapScrapperMessage()
Mode.API -> mobile.changeMessageStatus(messageId, when (folderId) {
1 -> "Odebrane"
2 -> "Wysłane"
else -> "Usunięte"
}, "Widoczna").let { MessageDetails("", emptyList()) }
}
}.compose(ScrapperExceptionTransformer())
}
fun sendMessage(subject: String, content: String, recipients: List<Recipient>): Single<SentMessage> {
return rxSingle {
when (mode) {
Mode.HYBRID, Mode.SCRAPPER -> scrapper.sendMessage(subject, content, recipients.mapFromRecipientsToScraper()).mapSentMessage()
Mode.API -> mobile.sendMessage(subject, content, recipients.mapFromRecipientsToMobile()).mapSentMessage(loginId)
}
}.compose(ScrapperExceptionTransformer())
}
fun deleteMessages(messages: List<Pair<Int, Int>>): Single<Boolean> {
suspend fun getMessageDetails(messageId: Int, folderId: Int, read: Boolean = false, id: Int? = null): MessageDetails {
return when (mode) {
Mode.SCRAPPER -> rxSingle { scrapper.deleteMessages(messages) }.compose(ScrapperExceptionTransformer())
Mode.HYBRID, Mode.API -> Completable.mergeDelayError(messages.map { (messageId, folderId) ->
rxSingle {
mobile.changeMessageStatus(messageId, when (folderId) {
1 -> "Odebrane"
2 -> "Wysłane"
else -> "Usunięte"
}, "Usunieta")
}.ignoreElement()
}).toSingleDefault(true)
Mode.HYBRID, Mode.SCRAPPER -> scrapper.getMessageDetails(messageId, folderId, read, id).mapScrapperMessage()
Mode.API -> mobile.changeMessageStatus(messageId, when (folderId) {
1 -> "Odebrane"
2 -> "Wysłane"
else -> "Usunięte"
}, "Widoczna").let { MessageDetails("", emptyList()) }
}
}
fun getTimetable(start: LocalDate, end: LocalDate): Single<List<Timetable>> {
return rxSingle {
when (mode) {
Mode.SCRAPPER -> scrapper.getTimetable(start, end).mapTimetable()
Mode.HYBRID, Mode.API -> mobile.getTimetable(start, end, 0).mapTimetable(mobile.getDictionaries())
}
}.compose(ScrapperExceptionTransformer())
suspend fun sendMessage(subject: String, content: String, recipients: List<Recipient>): SentMessage {
return when (mode) {
Mode.HYBRID, Mode.SCRAPPER -> scrapper.sendMessage(subject, content, recipients.mapFromRecipientsToScraper()).mapSentMessage()
Mode.API -> mobile.sendMessage(subject, content, recipients.mapFromRecipientsToMobile()).mapSentMessage(loginId)
}
}
fun getCompletedLessons(start: LocalDate, end: LocalDate? = null, subjectId: Int = -1): Single<List<CompletedLesson>> {
suspend fun deleteMessages(messages: List<Pair<Int, Int>>): Boolean {
return when (mode) {
Mode.HYBRID, Mode.SCRAPPER -> rxSingle { scrapper.getCompletedLessons(start, end, subjectId).mapCompletedLessons() }.compose(ScrapperExceptionTransformer())
Mode.SCRAPPER -> scrapper.deleteMessages(messages)
Mode.HYBRID, Mode.API -> messages.map { (messageId, folderId) ->
mobile.changeMessageStatus(messageId, when (folderId) {
1 -> "Odebrane"
2 -> "Wysłane"
else -> "Usunięte"
}, "Usunieta")
}.isNotEmpty()
}
}
suspend fun getTimetable(start: LocalDate, end: LocalDate): List<Timetable> {
return when (mode) {
Mode.SCRAPPER -> scrapper.getTimetable(start, end).mapTimetable()
Mode.HYBRID, Mode.API -> mobile.getTimetable(start, end, 0).mapTimetable(mobile.getDictionaries())
}
}
suspend fun getCompletedLessons(start: LocalDate, end: LocalDate? = null, subjectId: Int = -1): List<CompletedLesson> {
return when (mode) {
Mode.HYBRID, Mode.SCRAPPER -> scrapper.getCompletedLessons(start, end, subjectId).mapCompletedLessons()
Mode.API -> throw FeatureNotAvailableException("Completed lessons are not available in API mode")
}
}
fun getLuckyNumber(unitName: String = ""): Maybe<Int> {
return getKidsLuckyNumbers().filter { it.isNotEmpty() }.flatMap { numbers ->
// if lucky number unitName match unit name from student tile
numbers.singleOrNull { number -> number.unitName == unitName }?.let {
return@flatMap Maybe.just(it)
}
suspend fun getLuckyNumber(unitName: String = ""): Int {
val numbers = getKidsLuckyNumbers()
// if lucky number unitName match unit name from student tile
numbers.singleOrNull { number -> number.unitName == unitName }?.let {
return it.number
}
// if there there is only one lucky number and its doesn't match to any student
if (numbers.size == 1) {
return@flatMap Maybe.just(numbers.single())
}
// if there there is only one lucky number and its doesn't match to any student
if (numbers.size == 1) {
return numbers.single().number
}
// if there is more than one lucky number, return first (just like this was working before 0.16.0)
if (numbers.size > 1) {
return@flatMap Maybe.just(numbers.first())
}
// if there is more than one lucky number, return first (just like this was working before 0.16.0)
if (numbers.size > 1) {
return numbers.first().number
}
// else
Maybe.empty<LuckyNumber>()
}.map { it.number }
// else
return -1
}
fun getSelfGovernments(): Single<List<GovernmentUnit>> {
suspend fun getSelfGovernments(): List<GovernmentUnit> {
return when (mode) {
Mode.HYBRID, Mode.SCRAPPER -> rxSingle { scrapper.getSelfGovernments().mapToUnits() }.compose(ScrapperExceptionTransformer())
Mode.HYBRID, Mode.SCRAPPER -> scrapper.getSelfGovernments().mapToUnits()
Mode.API -> throw FeatureNotAvailableException("Self governments is not available in API mode")
}
}
fun getStudentThreats(): Single<List<String>> {
suspend fun getStudentThreats(): List<String> {
return when (mode) {
Mode.HYBRID, Mode.SCRAPPER -> rxSingle { scrapper.getStudentThreats() }.compose(ScrapperExceptionTransformer())
Mode.HYBRID, Mode.SCRAPPER -> scrapper.getStudentThreats()
Mode.API -> throw FeatureNotAvailableException("Student threats are not available in API mode")
}
}
fun getStudentsTrips(): Single<List<String>> {
suspend fun getStudentsTrips(): List<String> {
return when (mode) {
Mode.HYBRID, Mode.SCRAPPER -> rxSingle { scrapper.getStudentsTrips() }.compose(ScrapperExceptionTransformer())
Mode.HYBRID, Mode.SCRAPPER -> scrapper.getStudentsTrips()
Mode.API -> throw FeatureNotAvailableException("Students trips is not available in API mode")
}
}
fun getLastGrades(): Single<List<String>> {
suspend fun getLastGrades(): List<String> {
return when (mode) {
Mode.HYBRID, Mode.SCRAPPER -> rxSingle { scrapper.getLastGrades() }.compose(ScrapperExceptionTransformer())
Mode.HYBRID, Mode.SCRAPPER -> scrapper.getLastGrades()
Mode.API -> throw FeatureNotAvailableException("Last grades is not available in API mode")
}
}
fun getFreeDays(): Single<List<String>> {
suspend fun getFreeDays(): List<String> {
return when (mode) {
Mode.HYBRID, Mode.SCRAPPER -> rxSingle { scrapper.getFreeDays() }.compose(ScrapperExceptionTransformer())
Mode.HYBRID, Mode.SCRAPPER -> scrapper.getFreeDays()
Mode.API -> throw FeatureNotAvailableException("Free days is not available in API mode")
}
}
fun getKidsLuckyNumbers(): Single<List<LuckyNumber>> {
suspend fun getKidsLuckyNumbers(): List<LuckyNumber> {
return when (mode) {
Mode.HYBRID, Mode.SCRAPPER -> rxSingle { scrapper.getKidsLuckyNumbers().mapLuckyNumbers() }.compose(ScrapperExceptionTransformer())
Mode.HYBRID, Mode.SCRAPPER -> scrapper.getKidsLuckyNumbers().mapLuckyNumbers()
Mode.API -> throw FeatureNotAvailableException("Kids Lucky number is not available in API mode")
}
}
fun getKidsTimetable(): Single<List<String>> {
suspend fun getKidsTimetable(): List<String> {
return when (mode) {
Mode.HYBRID, Mode.SCRAPPER -> rxSingle { scrapper.getKidsLessonPlan() }.compose(ScrapperExceptionTransformer())
Mode.HYBRID, Mode.SCRAPPER -> scrapper.getKidsLessonPlan()
Mode.API -> throw FeatureNotAvailableException("Kids timetable is not available in API mode")
}
}
fun getLastHomework(): Single<List<String>> {
suspend fun getLastHomework(): List<String> {
return when (mode) {
Mode.HYBRID, Mode.SCRAPPER -> rxSingle { scrapper.getLastHomework() }.compose(ScrapperExceptionTransformer())
Mode.HYBRID, Mode.SCRAPPER -> scrapper.getLastHomework()
Mode.API -> throw FeatureNotAvailableException("Last homework is not available in API mode")
}
}
fun getLastExams(): Single<List<String>> {
suspend fun getLastExams(): List<String> {
return when (mode) {
Mode.HYBRID, Mode.SCRAPPER -> rxSingle { scrapper.getLastTests() }.compose(ScrapperExceptionTransformer())
Mode.HYBRID, Mode.SCRAPPER -> scrapper.getLastTests()
Mode.API -> throw FeatureNotAvailableException("Last exams is not available in API mode")
}
}
fun getLastStudentLessons(): Single<List<String>> {
suspend fun getLastStudentLessons(): List<String> {
return when (mode) {
Mode.HYBRID, Mode.SCRAPPER -> rxSingle { scrapper.getLastStudentLessons() }.compose(ScrapperExceptionTransformer())
Mode.HYBRID, Mode.SCRAPPER -> scrapper.getLastStudentLessons()
Mode.API -> throw FeatureNotAvailableException("Last student lesson is not available in API mode")
}
}

View file

@ -1,5 +0,0 @@
package io.github.wulkanowy.sdk.exception
import java.io.IOException
open class ApiException internal constructor(message: String, cause: Throwable? = null) : IOException(message, cause)

View file

@ -1,3 +0,0 @@
package io.github.wulkanowy.sdk.exception
class BadCredentialsException internal constructor(message: String, cause: Throwable? = null) : VulcanException(message, cause)

View file

@ -1,3 +0,0 @@
package io.github.wulkanowy.sdk.exception
class FeatureDisabledException internal constructor(message: String, cause: Throwable? = null) : VulcanException(message, cause)

View file

@ -1,3 +1,5 @@
package io.github.wulkanowy.sdk.exception
class FeatureNotAvailableException(message: String) : ApiException(message)
import java.io.IOException
class FeatureNotAvailableException internal constructor(message: String) : IOException(message)

View file

@ -1,3 +0,0 @@
package io.github.wulkanowy.sdk.exception
class NotLoggedInException internal constructor(message: String, cause: Throwable? = null) : VulcanException(message)

View file

@ -1,3 +0,0 @@
package io.github.wulkanowy.sdk.exception
class PasswordChangeRequiredException constructor(message: String, val redirectUrl: String, cause: Throwable?) : VulcanException(message, cause)

View file

@ -1,30 +0,0 @@
package io.github.wulkanowy.sdk.exception
import io.reactivex.Single
import io.reactivex.SingleSource
import io.reactivex.SingleTransformer
import io.github.wulkanowy.sdk.scrapper.ScrapperException as ScrapperApiException
import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException as ScrapperFeatureDisabledException
import io.github.wulkanowy.sdk.scrapper.exception.ServiceUnavailableException as ScrapperServiceUnavailableException
import io.github.wulkanowy.sdk.scrapper.exception.VulcanException as ScrapperVulcanException
import io.github.wulkanowy.sdk.scrapper.login.BadCredentialsException as ScrapperBadCredentialsException
import io.github.wulkanowy.sdk.scrapper.login.NotLoggedInException as ScrapperNotLoggedInException
import io.github.wulkanowy.sdk.scrapper.login.PasswordChangeRequiredException as ScrapperPasswordChangeRequiredException
class ScrapperExceptionTransformer<T : Any?> : SingleTransformer<T, T> {
override fun apply(upstream: Single<T>): SingleSource<T> {
return upstream.onErrorResumeNext {
Single.error(when (it) {
is ScrapperFeatureDisabledException -> FeatureDisabledException(it.message.orEmpty(), it)
is ScrapperNotLoggedInException -> NotLoggedInException(it.message.orEmpty(), it)
is ScrapperServiceUnavailableException -> ServiceUnavailableException(it.message.orEmpty(), it)
is ScrapperBadCredentialsException -> BadCredentialsException(it.message.orEmpty(), it)
is ScrapperPasswordChangeRequiredException -> PasswordChangeRequiredException(it.message.orEmpty(), it.redirectUrl, it)
is ScrapperVulcanException -> VulcanException(it.message.orEmpty(), it)
is ScrapperApiException -> ApiException(it.message.orEmpty(), it)
else -> it
})
}
}
}

View file

@ -1,3 +0,0 @@
package io.github.wulkanowy.sdk.exception
class ServiceUnavailableException internal constructor(message: String, cause: Throwable? = null) : VulcanException(message, cause)

View file

@ -2,4 +2,4 @@ package io.github.wulkanowy.sdk.exception
import java.io.IOException
open class VulcanException internal constructor(message: String, cause: Throwable? = null) : IOException(message, cause)
class VulcanException internal constructor(message: String) : IOException(message)

View file

@ -1,6 +1,7 @@
package io.github.wulkanowy.sdk
import io.github.wulkanowy.sdk.pojo.Folder
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Ignore
import org.junit.Test
@ -21,7 +22,7 @@ class SdkRemoteTest {
fun getStudents_api() {
val sdk = Sdk()
sdk.setSimpleHttpLogger { println(it) }
val students = sdk.getStudentsFromMobileApi(token = "FK100000", pin = "999999", symbol = "powiatwulkanowy", apiKey = API_KEY, firebaseToken = "").blockingGet()
val students = runBlocking { sdk.getStudentsFromMobileApi(token = "FK100000", pin = "999999", symbol = "powiatwulkanowy", apiKey = API_KEY, firebaseToken = "") }
assertEquals(2, students.size)
}
@ -31,7 +32,7 @@ class SdkRemoteTest {
// mode = Sdk.Mode.SCRAPPER
}
val students = sdk.getStudentsFromScrapper(email = "jan@fakelog.cf", password = "jan123", scrapperBaseUrl = "http://fakelog.cf", symbol = "powiatwulkanowy").blockingGet()
val students = runBlocking { sdk.getStudentsFromScrapper(email = "jan@fakelog.cf", password = "jan123", scrapperBaseUrl = "http://fakelog.cf", symbol = "powiatwulkanowy") }
assertEquals(6, students.size)
}
@ -41,7 +42,7 @@ class SdkRemoteTest {
// mode = Sdk.Mode.HYBRID
}
val students = sdk.getStudentsHybrid(email = "jan@fakelog.cf", password = "jan123", apiKey = API_KEY, scrapperBaseUrl = "http://fakelog.cf", startSymbol = "powiatwulkanowy", firebaseToken = "").blockingGet()
val students = runBlocking { sdk.getStudentsHybrid(email = "jan@fakelog.cf", password = "jan123", apiKey = API_KEY, scrapperBaseUrl = "http://fakelog.cf", startSymbol = "powiatwulkanowy", firebaseToken = "") }
assertEquals(6, students.size)
}
@ -60,7 +61,7 @@ class SdkRemoteTest {
classId = 14
}
val semesters = sdk.getSemesters().blockingGet()
val semesters = runBlocking { sdk.getSemesters() }
assertEquals(2, semesters.size)
}
@ -79,7 +80,7 @@ class SdkRemoteTest {
classId = 14
}
val grades = sdk.getGradesDetails(1).blockingGet()
val grades = runBlocking { sdk.getGradesDetails(1) }
assertEquals(22, grades.size)
}
@ -103,7 +104,7 @@ class SdkRemoteTest {
password = "jan123"
}
val grades = sdk.getGradesDetails(1).blockingGet()
val grades = runBlocking { sdk.getGradesDetails(1) }
assertEquals(22, grades.size)
}
@ -122,7 +123,7 @@ class SdkRemoteTest {
classId = 14
}
val grades = sdk.getGradesSummary(1).blockingGet()
val grades = runBlocking { sdk.getGradesSummary(1) }
assertEquals(4, grades.size)
}
@ -141,7 +142,7 @@ class SdkRemoteTest {
classId = 14
}
val attendance = sdk.getAttendance(of(2018, 1, 1), of(2018, 1, 2), 1).blockingGet()
val attendance = runBlocking { sdk.getAttendance(of(2018, 1, 1), of(2018, 1, 2), 1) }
assertEquals(24, attendance.size)
}
@ -160,7 +161,7 @@ class SdkRemoteTest {
classId = 14
}
val subjects = sdk.getSubjects().blockingGet()
val subjects = runBlocking { sdk.getSubjects() }
assertEquals(14, subjects.size)
}
@ -179,7 +180,7 @@ class SdkRemoteTest {
classId = 14
}
val notes = sdk.getNotes(1).blockingGet()
val notes = runBlocking { sdk.getNotes(1) }
assertEquals(5, notes.size)
}
@ -198,7 +199,7 @@ class SdkRemoteTest {
classId = 14
}
val teachers = sdk.getTeachers(1).blockingGet()
val teachers = runBlocking { sdk.getTeachers(1) }
assertEquals(9, teachers.size)
}
@ -217,7 +218,7 @@ class SdkRemoteTest {
classId = 14
}
val homework = sdk.getHomework(of(2018, 1, 1), of(2018, 1, 2)).blockingGet()
val homework = runBlocking { sdk.getHomework(of(2018, 1, 1), of(2018, 1, 2)) }
assertEquals(4, homework.size)
}
@ -236,7 +237,7 @@ class SdkRemoteTest {
classId = 14
}
val timetable = sdk.getTimetable(of(2018, 1, 1), of(2018, 1, 2)).blockingGet()
val timetable = runBlocking { sdk.getTimetable(of(2018, 1, 1), of(2018, 1, 2)) }
assertEquals(24, timetable.size)
}
@ -255,13 +256,13 @@ class SdkRemoteTest {
classId = 14
}
val messages = sdk.getMessages(Folder.RECEIVED, LocalDateTime.of(2018, 1, 1, 0, 0, 0), LocalDateTime.of(2018, 1, 2, 0, 0, 0)).blockingGet()
val messages = runBlocking { sdk.getMessages(Folder.RECEIVED, LocalDateTime.of(2018, 1, 1, 0, 0, 0), LocalDateTime.of(2018, 1, 2, 0, 0, 0)) }
assertEquals(2, messages.size)
val messagesSent = sdk.getMessages(Folder.SENT, LocalDateTime.of(2018, 1, 1, 0, 0, 0), LocalDateTime.of(2018, 1, 2, 0, 0, 0)).blockingGet()
val messagesSent = runBlocking { sdk.getMessages(Folder.SENT, LocalDateTime.of(2018, 1, 1, 0, 0, 0), LocalDateTime.of(2018, 1, 2, 0, 0, 0)) }
assertEquals(1, messagesSent.size)
val messagesTrashed = sdk.getMessages(Folder.TRASHED, LocalDateTime.of(2018, 1, 1, 0, 0, 0), LocalDateTime.of(2018, 1, 2, 0, 0, 0)).blockingGet()
val messagesTrashed = runBlocking { sdk.getMessages(Folder.TRASHED, LocalDateTime.of(2018, 1, 1, 0, 0, 0), LocalDateTime.of(2018, 1, 2, 0, 0, 0)) }
assertEquals(1, messagesTrashed.size)
}
@ -281,7 +282,7 @@ class SdkRemoteTest {
classId = 14
}
val message = sdk.getMessageDetails(1, 2, true, 1).blockingGet()
val message = runBlocking { sdk.getMessageDetails(1, 2, true, 1) }
assertEquals("Zmiana statusu wiadomości.", message)
}
@ -301,7 +302,7 @@ class SdkRemoteTest {
classId = 14
}
val isDeleted = sdk.deleteMessages(listOf(1 to 1, 2 to 2)).blockingGet()
val isDeleted = runBlocking { sdk.deleteMessages(listOf(1 to 1, 2 to 2)) }
assertEquals(true, isDeleted)
}
}

View file

@ -3,6 +3,7 @@ package io.github.wulkanowy.sdk.mapper
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.mobile.BaseLocalTest
import io.github.wulkanowy.sdk.mobile.exams.ExamsTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
import org.threeten.bp.LocalDate.now
@ -21,7 +22,7 @@ class ExamsMapperTest : BaseLocalTest() {
server.enqueueAndStart("Sprawdziany.json", ExamsTest::class.java)
server.enqueue("Slowniki.json", BaseLocalTest::class.java)
val exams = mobile.getExams(now(), now(), 1).blockingGet()
val exams = runBlocking { mobile.getExams(now(), now(), 1) }
assertEquals(3, exams.size)
assertEquals("Sprawdzian", exams[0].type)

View file

@ -3,6 +3,7 @@ package io.github.wulkanowy.sdk.mapper
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.mobile.BaseLocalTest
import io.github.wulkanowy.sdk.mobile.grades.GradesTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
@ -20,7 +21,7 @@ class GradesMapperTest : BaseLocalTest() {
server.enqueueAndStart("Oceny.json", GradesTest::class.java)
server.enqueue("Slowniki.json", BaseLocalTest::class.java)
val grades = mobile.getGradesDetails(0).blockingGet()
val grades = runBlocking { mobile.getGradesDetails(0) }
assertEquals(2, grades.size)
with(grades[0]) {

View file

@ -3,6 +3,7 @@ package io.github.wulkanowy.sdk.mapper
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.mobile.BaseLocalTest
import io.github.wulkanowy.sdk.mobile.register.RegisterTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
import org.threeten.bp.LocalDate.of
@ -21,7 +22,7 @@ class SemesterMapperTest : BaseLocalTest() {
server.enqueueAndStart("ListaUczniow.json", RegisterTest::class.java)
mobile.studentId = 1
val semesters = mobile.getSemesters(of(2020, 1, 30)).blockingGet()
val semesters = runBlocking { mobile.getSemesters(of(2020, 1, 30)) }
assertEquals(2, semesters.size)
with(semesters[0]) {
@ -41,7 +42,7 @@ class SemesterMapperTest : BaseLocalTest() {
server.enqueueAndStart("ListaUczniow-2.json", RegisterTest::class.java)
mobile.studentId = 1
val semesters = mobile.getSemesters(of(2020, 1, 29)).blockingGet()
val semesters = runBlocking { mobile.getSemesters(of(2020, 1, 29)) }
assertEquals(2, semesters.size)
with(semesters[0]) {

View file

@ -2,6 +2,7 @@ package io.github.wulkanowy.sdk.mapper
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.mobile.BaseLocalTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
@ -18,7 +19,7 @@ class SubjectsMapperTest : BaseLocalTest() {
fun getApiSubjects() {
server.enqueueAndStart("Slowniki.json", BaseLocalTest::class.java)
val subjects = mobile.getSubjects().blockingGet()
val subjects = runBlocking { mobile.getSubjects() }
assertEquals(15, subjects.size)
with(subjects[0]) {
assertEquals(-1, id)

View file

@ -3,6 +3,7 @@ package io.github.wulkanowy.sdk.mapper
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.mobile.BaseLocalTest
import io.github.wulkanowy.sdk.mobile.timetable.TimetableTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
import org.threeten.bp.LocalDate.of
@ -22,7 +23,7 @@ class TimetableMapperTest : BaseLocalTest() {
server.enqueueAndStart("PlanLekcji.json", TimetableTest::class.java)
server.enqueue("Slowniki.json", BaseLocalTest::class.java)
val lessons = mobile.getTimetable(of(2020, 2, 3), of(2020, 2, 4)).blockingGet()
val lessons = runBlocking { mobile.getTimetable(of(2020, 2, 3), of(2020, 2, 4)) }
assertEquals(4, lessons.size)
with(lessons[1]) {
@ -49,7 +50,7 @@ class TimetableMapperTest : BaseLocalTest() {
server.enqueueAndStart("PlanLekcji.json", TimetableTest::class.java)
server.enqueue("Slowniki.json", BaseLocalTest::class.java)
val lessons = mobile.getTimetable(of(2020, 2, 3), of(2020, 2, 4)).blockingGet()
val lessons = runBlocking { mobile.getTimetable(of(2020, 2, 3), of(2020, 2, 4)) }
with(lessons[3]) {
assertEquals(4, number)