CLI client
This commit is contained in:
@@ -101,11 +101,35 @@ object CLI {
|
||||
data class LabelName(val name: String): ILabelIdentification
|
||||
data class ID(val id: Uuid) : IProjectIdentification, ILabelIdentification
|
||||
|
||||
sealed interface IData<T>
|
||||
data class DirectData<T>(val data: List<T>) : IData<T>
|
||||
data class FileData<T>(val file: InputStream, val parse: (String) -> T) : IData<T> {
|
||||
sealed interface IData<T> {
|
||||
abstract fun toList(): List<T>
|
||||
}
|
||||
data class DirectData<T>(val data: List<T>) : IData<T> {
|
||||
override fun toList(): List<T> = data
|
||||
}
|
||||
data class FileData<T>(val file: InputStream, val parse: (String) -> T?) : IData<T> {
|
||||
override fun toList(): List<T> {
|
||||
val raw = file.bufferedReader().use { it.readText() }.split(',')
|
||||
val parsed = ArrayList<T>(raw.size)
|
||||
val errors = mutableListOf<String>()
|
||||
raw.forEach {
|
||||
val use = it.trim()
|
||||
if(use.isEmpty()) return@forEach
|
||||
|
||||
parse(use)?.let { p -> parsed += p } ?: run {
|
||||
errors += use
|
||||
}
|
||||
}
|
||||
|
||||
if(errors.isNotEmpty()) {
|
||||
System.err.println("Failed to parse these entries: ${errors.map { "'$it'" }}")
|
||||
exitProcess(-1)
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun floatFile(file: InputStream) = FileData<Float>(file) { it.toFloat() }
|
||||
fun floatFile(file: InputStream) = FileData<Float>(file) { it.toFloatOrNull() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,7 +144,7 @@ object CLI {
|
||||
|
||||
val project by findOrSetObject { this }
|
||||
|
||||
override fun run() {}
|
||||
override fun run() { project /* force initialization */ }
|
||||
|
||||
@Suppress("unused")
|
||||
class ProjectList : CliktCommand(name = "list") {
|
||||
@@ -172,7 +196,7 @@ object CLI {
|
||||
option("--ns", "--nano", "--nanosec", help = "Set the time unit to nanoseconds").flag().convert { TimeUnit.NANOS },
|
||||
option("--us", "--micro", "--μs", "--microsec", help = "Set the time unit to microseconds").flag().convert { TimeUnit.MICROS },
|
||||
option("--ms", "--milli", "--millis", "--millisec", help = "Set the time unit to milliseconds").flag().convert { TimeUnit.MILLIS },
|
||||
option("--s", "--sec", "--second", help = "Set the time unit to seconds").flag().convert{ TimeUnit.SECONDS },
|
||||
option("--s", "--sec", "--second", help = "Set the time unit to seconds").flag().convert{ TimeUnit.SECONDS },
|
||||
option("--min", "--m", "--minutes", help = "Set the time unit to minutes").flag().convert { TimeUnit.MINUTES },
|
||||
option("--h", "--hour", help = "Set the time unit to hours").flag().convert { TimeUnit.HOURS }
|
||||
).single()
|
||||
|
||||
@@ -1,9 +1,35 @@
|
||||
package com.jaytux.phoebench.clients.cli
|
||||
|
||||
import com.github.ajalt.clikt.parameters.options.convert
|
||||
import com.github.ajalt.clikt.parameters.options.flag
|
||||
import com.github.ajalt.clikt.parameters.options.option
|
||||
import com.jaytux.phoebench.clients.cli.CLI.Commands.Project.ProjectName
|
||||
import com.jaytux.phoebench.common.ApiRoute
|
||||
import com.jaytux.phoebench.common.Either
|
||||
import com.jaytux.phoebench.common.EmptyRequest
|
||||
import com.jaytux.phoebench.common.EntryRequest
|
||||
import com.jaytux.phoebench.common.ErrorResponse
|
||||
import com.jaytux.phoebench.common.LabelRequest
|
||||
import com.jaytux.phoebench.common.LabelResponse
|
||||
import com.jaytux.phoebench.common.ProjectRequest
|
||||
import com.jaytux.phoebench.common.Routes
|
||||
import com.jaytux.phoebench.common.TimeUnit
|
||||
import com.jaytux.phoebench.common.bind
|
||||
import com.jaytux.phoebench.common.error
|
||||
import com.jaytux.phoebench.common.fold
|
||||
import com.jaytux.phoebench.common.map
|
||||
import com.jaytux.phoebench.common.value
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import java.nio.file.OpenOption
|
||||
import kotlin.io.path.Path
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.inputStream
|
||||
import kotlin.io.path.isReadable
|
||||
import kotlin.io.path.isRegularFile
|
||||
import kotlin.system.exitProcess
|
||||
import kotlin.time.Clock
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
object ProjectHandlers {
|
||||
fun list() {
|
||||
@@ -19,13 +45,94 @@ object ProjectHandlers {
|
||||
}
|
||||
}
|
||||
|
||||
fun ensureProjectIdentification(got: CLI.Commands.Project.IProjectIdentification?): CLI.Commands.Project.IProjectIdentification =
|
||||
got.maybePrompt("project name (as [user]/[project])") {
|
||||
val split = it.split('/')
|
||||
if (split.size != 2) {
|
||||
System.err.println("Invalid format (expected [user]/[project])")
|
||||
exitProcess(-1)
|
||||
}
|
||||
ProjectName(split[0], split[1])
|
||||
}
|
||||
|
||||
suspend fun CLI.Commands.Project.IProjectIdentification.toId(): Either<ErrorResponse, Uuid> =
|
||||
when(this) {
|
||||
is CLI.Commands.Project.ID -> id.value()
|
||||
is ProjectName -> Client.callRoute(Routes.home, EmptyRequest()).bind {
|
||||
(it.ownProjects + it.publicProjects).firstOrNull { pr ->
|
||||
pr.owner.name == user && pr.name == project
|
||||
}?.id?.value() ?: ErrorResponse("Project ${user}/${project} not found").error()
|
||||
}
|
||||
}
|
||||
|
||||
fun details(find: CLI.Commands.Project.IProjectIdentification?) {
|
||||
//
|
||||
val id = ensureProjectIdentification(find)
|
||||
tryAuthenticated {
|
||||
id.toId().bind {
|
||||
Client.callRoute(Routes.Project.get, it)
|
||||
}
|
||||
}.fold({
|
||||
System.err.println("Failed to get project details: ${it.msg}")
|
||||
}) {
|
||||
println("Project ${it.owner.name}/${it.name} [${it.id}]:")
|
||||
println("${it.usedLabels.size} labels:")
|
||||
it.usedLabels.forEach { l ->
|
||||
println(" - [${l.id}] ${l.name} (with color ${l.color})")
|
||||
}
|
||||
val map = it.usedLabels.associateBy { l -> l.id }
|
||||
|
||||
println("\n${it.entries.size} entries:")
|
||||
it.entries.forEach { e ->
|
||||
println(" - [${e.id}] Entry labeled ${map[e.labelId]?.name ?: "<invalid label>"} at ${e.timestamp.fmt()} " +
|
||||
"(${e.warmups.size} warmup data points, ${e.measurements.size} measurement data points; in ${e.unit.disp})")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun create(name: String?, isPublic: Boolean) {}
|
||||
fun create(name: String?, isPublic: Boolean) {
|
||||
val useName = name.maybePrompt("project name") { it }
|
||||
tryAuthenticated {
|
||||
Client.callRoute(Routes.Project.new, ProjectRequest(useName, isPublic))
|
||||
}.fold({
|
||||
System.err.println("Failed to create project '$useName': ${it.msg}")
|
||||
}) {
|
||||
println("Project ${it.owner.name}/${it.name} [${it.id}] created.")
|
||||
}
|
||||
}
|
||||
|
||||
fun newLabel(name: String?, color: String?, project: CLI.Commands.Project.IProjectIdentification?) {}
|
||||
fun newLabel(name: String?, color: String?, project: CLI.Commands.Project.IProjectIdentification?) {
|
||||
val id = ensureProjectIdentification(project)
|
||||
val useName = name.maybePrompt("label name") { it }
|
||||
val useColor = color.maybePrompt("color") {
|
||||
val check = it.length == 7 && it[0] == '#' && it.substring(1, it.length).all { c -> c.isDigit() || c in "ABCDEF" }
|
||||
if(check) it
|
||||
else {
|
||||
System.err.println("Invalid color format: '$it' (expected RGB-hex-format (#ABCDEF))")
|
||||
exitProcess(-1)
|
||||
}
|
||||
}
|
||||
|
||||
tryAuthenticated {
|
||||
id.toId().bind { projectId ->
|
||||
Client.callRoute(Routes.Label.new, LabelRequest(useName, useColor, projectId))
|
||||
}
|
||||
}.fold({
|
||||
System.err.println("Could not create label: ${it.msg}")
|
||||
}) {
|
||||
println("Label ${it.name} (${it.color}) [${it.id}] created.")
|
||||
}
|
||||
}
|
||||
|
||||
private fun CLI.Commands.Project.IData<Float>?.ensure(x: String): CLI.Commands.Project.IData<Float> {
|
||||
return maybePrompt("$x data file") {
|
||||
val path = Path(it)
|
||||
if(!path.exists() || !path.isRegularFile() || !path.isReadable()) {
|
||||
System.err.println("Path $it is not readable")
|
||||
exitProcess(-1)
|
||||
}
|
||||
CLI.Commands.Project.FileData.floatFile(path.inputStream())
|
||||
}
|
||||
}
|
||||
|
||||
fun newData(
|
||||
project: CLI.Commands.Project.IProjectIdentification?,
|
||||
@@ -33,5 +140,43 @@ object ProjectHandlers {
|
||||
warmup: CLI.Commands.Project.IData<Float>?,
|
||||
measurement: CLI.Commands.Project.IData<Float>?,
|
||||
unit: TimeUnit?
|
||||
) {}
|
||||
) {
|
||||
val projectId = ensureProjectIdentification(project)
|
||||
val labelId = label.maybePrompt("label name") {
|
||||
CLI.Commands.Project.LabelName(it)
|
||||
}
|
||||
val warmupData = warmup.ensure("warmup").toList()
|
||||
val measureData = measurement.ensure("measurements").toList()
|
||||
val timeUnit = unit.maybePrompt("time unit for data") {
|
||||
when(it) {
|
||||
in setOf("ns", "nano", "nanosec") -> TimeUnit.NANOS
|
||||
in setOf("us", "micro", "μs", "microsec") -> TimeUnit.MICROS
|
||||
in setOf("ms", "milli", "millis", "millisec") -> TimeUnit.MILLIS
|
||||
in setOf("s", "sec", "second") -> TimeUnit.SECONDS
|
||||
in setOf("min", "m", "minutes") -> TimeUnit.MINUTES
|
||||
in setOf("h", "hour") -> TimeUnit.HOURS
|
||||
else -> {
|
||||
System.err.println("Unknown time unit $it (see help message)")
|
||||
exitProcess(-1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tryAuthenticated {
|
||||
projectId.toId().bind { pId ->
|
||||
when(labelId) {
|
||||
is CLI.Commands.Project.ID -> labelId.id.value()
|
||||
is CLI.Commands.Project.LabelName -> Client.callRoute(Routes.Project.get, pId).bind {
|
||||
it.usedLabels.firstOrNull { l -> l.name == labelId.name }?.id?.value()
|
||||
?: ErrorResponse("Label ${labelId.name} does not exist in this project.").error()
|
||||
}
|
||||
}.map { pId to it }
|
||||
}.bind { (pId, lId) ->
|
||||
Client.callRoute(Routes.Entry.new, EntryRequest(
|
||||
lId, Clock.System.now(), pId,
|
||||
warmupData, measureData, timeUnit
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,15 @@ package com.jaytux.phoebench.clients.cli
|
||||
import com.jaytux.phoebench.clients.cli.CLI.Root
|
||||
import com.jaytux.phoebench.common.*
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.format
|
||||
import kotlinx.datetime.format.MonthNames
|
||||
import kotlinx.datetime.format.Padding
|
||||
import kotlinx.datetime.format.char
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import java.util.Locale.getDefault
|
||||
import kotlin.time.Instant
|
||||
|
||||
fun <T> T.ignore(): Unit {}
|
||||
|
||||
@@ -38,4 +46,14 @@ inline fun <reified V> tryAuthenticated(crossinline body: suspend () -> Either<E
|
||||
}) {
|
||||
it.value()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val systemTz = TimeZone.currentSystemDefault()
|
||||
|
||||
val formatter = LocalDateTime.Format {
|
||||
val noPad = Padding.NONE
|
||||
day(noPad); char(' '); monthName(MonthNames.ENGLISH_ABBREVIATED); char(' '); year(noPad); char(' ')
|
||||
hour(); char(':'); minute(); char(':'); second()
|
||||
}
|
||||
|
||||
fun Instant.fmt(): String = this.toLocalDateTime(systemTz).format(formatter)
|
||||
@@ -65,7 +65,7 @@ object ProjectHandler {
|
||||
}
|
||||
|
||||
fun createProject(user: User, req: ProjectRequest) = transaction {
|
||||
if(user.projectLimit != -1 && (user.projectLimit >= user.projects.count()))
|
||||
if(user.projectLimit != -1 && (user.projects.count() >= user.projectLimit))
|
||||
throw RouteError("You have reached your project limit.", HttpStatusCode.Conflict)
|
||||
|
||||
val proj = Project.new {
|
||||
|
||||
Reference in New Issue
Block a user