CLI client
This commit is contained in:
@@ -101,11 +101,35 @@ object CLI {
|
|||||||
data class LabelName(val name: String): ILabelIdentification
|
data class LabelName(val name: String): ILabelIdentification
|
||||||
data class ID(val id: Uuid) : IProjectIdentification, ILabelIdentification
|
data class ID(val id: Uuid) : IProjectIdentification, ILabelIdentification
|
||||||
|
|
||||||
sealed interface IData<T>
|
sealed interface IData<T> {
|
||||||
data class DirectData<T>(val data: List<T>) : IData<T>
|
abstract fun toList(): List<T>
|
||||||
data class FileData<T>(val file: InputStream, val parse: (String) -> T) : IData<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 {
|
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 }
|
val project by findOrSetObject { this }
|
||||||
|
|
||||||
override fun run() {}
|
override fun run() { project /* force initialization */ }
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
class ProjectList : CliktCommand(name = "list") {
|
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("--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("--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("--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("--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 }
|
option("--h", "--hour", help = "Set the time unit to hours").flag().convert { TimeUnit.HOURS }
|
||||||
).single()
|
).single()
|
||||||
|
|||||||
@@ -1,9 +1,35 @@
|
|||||||
package com.jaytux.phoebench.clients.cli
|
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.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.Routes
|
||||||
import com.jaytux.phoebench.common.TimeUnit
|
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.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 {
|
object ProjectHandlers {
|
||||||
fun list() {
|
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?) {
|
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(
|
fun newData(
|
||||||
project: CLI.Commands.Project.IProjectIdentification?,
|
project: CLI.Commands.Project.IProjectIdentification?,
|
||||||
@@ -33,5 +140,43 @@ object ProjectHandlers {
|
|||||||
warmup: CLI.Commands.Project.IData<Float>?,
|
warmup: CLI.Commands.Project.IData<Float>?,
|
||||||
measurement: CLI.Commands.Project.IData<Float>?,
|
measurement: CLI.Commands.Project.IData<Float>?,
|
||||||
unit: TimeUnit?
|
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.clients.cli.CLI.Root
|
||||||
import com.jaytux.phoebench.common.*
|
import com.jaytux.phoebench.common.*
|
||||||
import kotlinx.coroutines.runBlocking
|
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 java.util.Locale.getDefault
|
||||||
|
import kotlin.time.Instant
|
||||||
|
|
||||||
fun <T> T.ignore(): Unit {}
|
fun <T> T.ignore(): Unit {}
|
||||||
|
|
||||||
@@ -38,4 +46,14 @@ inline fun <reified V> tryAuthenticated(crossinline body: suspend () -> Either<E
|
|||||||
}) {
|
}) {
|
||||||
it.value()
|
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 {
|
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)
|
throw RouteError("You have reached your project limit.", HttpStatusCode.Conflict)
|
||||||
|
|
||||||
val proj = Project.new {
|
val proj = Project.new {
|
||||||
|
|||||||
Reference in New Issue
Block a user