From 35e4407e182cecba191a249a7837dc08cb414993 Mon Sep 17 00:00:00 2001 From: JosephTLockwood Date: Mon, 26 Jun 2023 14:07:45 -0400 Subject: [PATCH 01/14] Update DatasetExtension to work on unlevel columns. Also add String option --- .../org/imdc/extensions/client/ClientHook.kt | 55 ++ .../client/ClientProjectExtensions.kt | 13 + .../client/ClientProjectExtensions.properties | 2 + .../org/imdc/extensions/common/Constants.kt | 3 + .../extensions/common/DatasetExtensions.kt | 515 ++++++++++++++++ .../common/DatasetExtensions.properties | 49 ++ .../extensions/common/ExtensionDocProvider.kt | 34 ++ .../extensions/common/ProjectExtensions.kt | 7 + .../extensions/common/PyDatasetBuilder.kt | 58 ++ .../imdc/extensions/common/TagExtensions.kt | 63 ++ .../common/TagExtensions.properties | 4 + .../org/imdc/extensions/common/Utilities.kt | 55 ++ ...litiesExtensions$KeywordParseContext.class | Bin 0 -> 3814 bytes .../common/UtilitiesExtensions.class | Bin 0 -> 7168 bytes .../common/UtilitiesExtensions.properties | 10 + .../common/expressions/IsAvailableFunction.kt | 26 + .../common/expressions/LogicalPredicate.kt | 44 ++ .../org/imdc/extensions/common/DSBuilder.kt | 38 ++ .../extensions/common/DatasetEqualityTests.kt | 156 +++++ .../common/DatasetExtensionsTests.kt | 556 ++++++++++++++++++ .../imdc/extensions/common/ExpressionUtils.kt | 40 ++ .../org/imdc/extensions/common/JythonTest.kt | 93 +++ .../common/LogicalFunctionsTests.kt | 49 ++ .../common/UtilitiesExtensionsTests.kt | 50 ++ .../org/imdc/extensions/common/sample.xls | Bin 0 -> 35328 bytes .../org/imdc/extensions/common/sample.xlsx | Bin 0 -> 13410 bytes .../extensions/common/DatasetExtensions.kt | 105 +++- .../common/DatasetExtensions.properties | 3 +- .../imdc/extensions/designer/DesignerHook.kt | 55 ++ .../designer/DesignerProjectExtensions.kt | 38 ++ .../DesignerProjectExtensions.properties | 6 + .../imdc/extensions/gateway/GatewayHook.kt | 65 ++ .../gateway/GatewayProjectExtensions.kt | 24 + .../GatewayProjectExtensions.properties | 3 + .../gateway/GatewayTagExtensions.kt | 18 + .../imdc/extensions/gateway/HistoryServlet.kt | 162 +++++ 36 files changed, 2379 insertions(+), 20 deletions(-) create mode 100644 client/bin/main/org/imdc/extensions/client/ClientHook.kt create mode 100644 client/bin/main/org/imdc/extensions/client/ClientProjectExtensions.kt create mode 100644 client/bin/main/org/imdc/extensions/client/ClientProjectExtensions.properties create mode 100644 common/bin/main/org/imdc/extensions/common/Constants.kt create mode 100644 common/bin/main/org/imdc/extensions/common/DatasetExtensions.kt create mode 100644 common/bin/main/org/imdc/extensions/common/DatasetExtensions.properties create mode 100644 common/bin/main/org/imdc/extensions/common/ExtensionDocProvider.kt create mode 100644 common/bin/main/org/imdc/extensions/common/ProjectExtensions.kt create mode 100644 common/bin/main/org/imdc/extensions/common/PyDatasetBuilder.kt create mode 100644 common/bin/main/org/imdc/extensions/common/TagExtensions.kt create mode 100644 common/bin/main/org/imdc/extensions/common/TagExtensions.properties create mode 100644 common/bin/main/org/imdc/extensions/common/Utilities.kt create mode 100644 common/bin/main/org/imdc/extensions/common/UtilitiesExtensions$KeywordParseContext.class create mode 100644 common/bin/main/org/imdc/extensions/common/UtilitiesExtensions.class create mode 100644 common/bin/main/org/imdc/extensions/common/UtilitiesExtensions.properties create mode 100644 common/bin/main/org/imdc/extensions/common/expressions/IsAvailableFunction.kt create mode 100644 common/bin/main/org/imdc/extensions/common/expressions/LogicalPredicate.kt create mode 100644 common/bin/test/org/imdc/extensions/common/DSBuilder.kt create mode 100644 common/bin/test/org/imdc/extensions/common/DatasetEqualityTests.kt create mode 100644 common/bin/test/org/imdc/extensions/common/DatasetExtensionsTests.kt create mode 100644 common/bin/test/org/imdc/extensions/common/ExpressionUtils.kt create mode 100644 common/bin/test/org/imdc/extensions/common/JythonTest.kt create mode 100644 common/bin/test/org/imdc/extensions/common/LogicalFunctionsTests.kt create mode 100644 common/bin/test/org/imdc/extensions/common/UtilitiesExtensionsTests.kt create mode 100644 common/bin/test/org/imdc/extensions/common/sample.xls create mode 100644 common/bin/test/org/imdc/extensions/common/sample.xlsx create mode 100644 designer/bin/main/org/imdc/extensions/designer/DesignerHook.kt create mode 100644 designer/bin/main/org/imdc/extensions/designer/DesignerProjectExtensions.kt create mode 100644 designer/bin/main/org/imdc/extensions/designer/DesignerProjectExtensions.properties create mode 100644 gateway/bin/main/org/imdc/extensions/gateway/GatewayHook.kt create mode 100644 gateway/bin/main/org/imdc/extensions/gateway/GatewayProjectExtensions.kt create mode 100644 gateway/bin/main/org/imdc/extensions/gateway/GatewayProjectExtensions.properties create mode 100644 gateway/bin/main/org/imdc/extensions/gateway/GatewayTagExtensions.kt create mode 100644 gateway/bin/main/org/imdc/extensions/gateway/HistoryServlet.kt diff --git a/client/bin/main/org/imdc/extensions/client/ClientHook.kt b/client/bin/main/org/imdc/extensions/client/ClientHook.kt new file mode 100644 index 0000000..9d888fd --- /dev/null +++ b/client/bin/main/org/imdc/extensions/client/ClientHook.kt @@ -0,0 +1,55 @@ +package org.imdc.extensions.client + +import com.inductiveautomation.ignition.client.model.ClientContext +import com.inductiveautomation.ignition.common.BundleUtil +import com.inductiveautomation.ignition.common.expressions.ExpressionFunctionManager +import com.inductiveautomation.ignition.common.licensing.LicenseState +import com.inductiveautomation.ignition.common.script.ScriptManager +import com.inductiveautomation.vision.api.client.AbstractClientModuleHook +import org.imdc.extensions.common.DatasetExtensions +import org.imdc.extensions.common.ExtensionDocProvider +import org.imdc.extensions.common.PyDatasetBuilder +import org.imdc.extensions.common.UtilitiesExtensions +import org.imdc.extensions.common.addPropertyBundle +import org.imdc.extensions.common.expressions.IsAvailableFunction +import org.imdc.extensions.common.expressions.LogicalPredicate.Companion.registerLogicFunctions + +@Suppress("unused") +class ClientHook : AbstractClientModuleHook() { + private lateinit var context: ClientContext + + override fun startup(context: ClientContext, activationState: LicenseState) { + this.context = context + + BundleUtil.get().apply { + addPropertyBundle() + addPropertyBundle() + addPropertyBundle() + } + + PyDatasetBuilder.register() + } + + override fun shutdown() { + PyDatasetBuilder.unregister() + } + + override fun initializeScriptManager(manager: ScriptManager) { + manager.apply { + addScriptModule("system.dataset", DatasetExtensions, ExtensionDocProvider) + addScriptModule("system.util", UtilitiesExtensions(context), ExtensionDocProvider) + addScriptModule("system.project", ClientProjectExtensions(context), ExtensionDocProvider) + } + } + + override fun configureFunctionFactory(factory: ExpressionFunctionManager) { + factory.apply { + addFunction( + IsAvailableFunction.NAME, + IsAvailableFunction.CATEGORY, + IsAvailableFunction(), + ) + registerLogicFunctions() + } + } +} diff --git a/client/bin/main/org/imdc/extensions/client/ClientProjectExtensions.kt b/client/bin/main/org/imdc/extensions/client/ClientProjectExtensions.kt new file mode 100644 index 0000000..828b638 --- /dev/null +++ b/client/bin/main/org/imdc/extensions/client/ClientProjectExtensions.kt @@ -0,0 +1,13 @@ +package org.imdc.extensions.client + +import com.inductiveautomation.ignition.client.model.ClientContext +import com.inductiveautomation.ignition.common.project.Project +import com.inductiveautomation.ignition.common.script.hints.ScriptFunction +import org.imdc.extensions.common.ProjectExtensions + +class ClientProjectExtensions(private val context: ClientContext) : ProjectExtensions { + @ScriptFunction(docBundlePrefix = "ClientProjectExtensions") + override fun getProject(): Project { + return requireNotNull(context.project) + } +} diff --git a/client/bin/main/org/imdc/extensions/client/ClientProjectExtensions.properties b/client/bin/main/org/imdc/extensions/client/ClientProjectExtensions.properties new file mode 100644 index 0000000..776f5d2 --- /dev/null +++ b/client/bin/main/org/imdc/extensions/client/ClientProjectExtensions.properties @@ -0,0 +1,2 @@ +getProject.desc=Retrieves the current project. +getProject.returns=The current project as a RuntimeProject instance. diff --git a/common/bin/main/org/imdc/extensions/common/Constants.kt b/common/bin/main/org/imdc/extensions/common/Constants.kt new file mode 100644 index 0000000..def2b9d --- /dev/null +++ b/common/bin/main/org/imdc/extensions/common/Constants.kt @@ -0,0 +1,3 @@ +package org.imdc.extensions.common + +const val MODULE_ID = "io.github.paulgriffith.extensions.IgnitionExtensions" diff --git a/common/bin/main/org/imdc/extensions/common/DatasetExtensions.kt b/common/bin/main/org/imdc/extensions/common/DatasetExtensions.kt new file mode 100644 index 0000000..c9a8e14 --- /dev/null +++ b/common/bin/main/org/imdc/extensions/common/DatasetExtensions.kt @@ -0,0 +1,515 @@ +package org.imdc.extensions.common + +import com.inductiveautomation.ignition.common.Dataset +import com.inductiveautomation.ignition.common.TypeUtilities +import com.inductiveautomation.ignition.common.script.PyArgParser +import com.inductiveautomation.ignition.common.script.builtin.KeywordArgs +import com.inductiveautomation.ignition.common.script.hints.ScriptArg +import com.inductiveautomation.ignition.common.script.hints.ScriptFunction +import com.inductiveautomation.ignition.common.util.DatasetBuilder +import com.inductiveautomation.ignition.common.xmlserialization.ClassNameResolver +import org.apache.poi.ss.usermodel.CellType.BOOLEAN +import org.apache.poi.ss.usermodel.CellType.FORMULA +import org.apache.poi.ss.usermodel.CellType.NUMERIC +import org.apache.poi.ss.usermodel.CellType.STRING +import org.apache.poi.ss.usermodel.DateUtil +import org.apache.poi.ss.usermodel.WorkbookFactory +import org.python.core.Py +import org.python.core.PyBaseString +import org.python.core.PyBoolean +import org.python.core.PyFloat +import org.python.core.PyFunction +import org.python.core.PyInteger +import org.python.core.PyLong +import org.python.core.PyObject +import org.python.core.PyString +import org.python.core.PyType +import org.python.core.PyUnicode +import java.io.File +import java.math.BigDecimal +import java.util.Date +import kotlin.math.max + +object DatasetExtensions { + @Suppress("unused") + @ScriptFunction(docBundlePrefix = "DatasetExtensions") + @KeywordArgs( + names = ["dataset", "mapper", "preserveColumnTypes"], + types = [Dataset::class, PyFunction::class, Boolean::class], + ) + fun map(args: Array, keywords: Array): Dataset? { + val parsedArgs = PyArgParser.parseArgs( + args, + keywords, + arrayOf("dataset", "mapper", "preserveColumnTypes"), + arrayOf(Dataset::class.java, PyObject::class.java, Boolean::class.javaObjectType), + "map", + ) + val dataset = parsedArgs.requirePyObject("dataset").toJava() + val mapper = parsedArgs.requirePyObject("mapper") + val preserveColumnTypes = parsedArgs.getBoolean("preserveColumnTypes").filter { it }.isPresent + + val columnTypes = if (preserveColumnTypes) { + dataset.columnTypes + } else { + List(dataset.columnCount) { Any::class.java } + } + + val builder = DatasetBuilder.newBuilder().colNames(dataset.columnNames).colTypes(columnTypes) + + for (row in dataset.rowIndices) { + val columnValues = Array(dataset.columnCount) { col -> + Py.java2py(dataset[row, col]) + } + val returnValue = mapper.__call__(columnValues, dataset.columnNames.toTypedArray()) + + val newValues = returnValue.asIterable().map(TypeUtilities::pyToJava).toTypedArray() + builder.addRow(*newValues) + } + + return builder.build() + } + + @Suppress("unused") + @ScriptFunction(docBundlePrefix = "DatasetExtensions") + @KeywordArgs( + names = ["dataset", "filter"], + types = [Dataset::class, PyFunction::class], + ) + fun filter(args: Array, keywords: Array): Dataset? { + val parsedArgs = PyArgParser.parseArgs( + args, + keywords, + arrayOf("dataset", "filter"), + arrayOf(Dataset::class.java, PyObject::class.java), + "filter", + ) + val dataset = parsedArgs.requirePyObject("dataset").toJava() + val filter = parsedArgs.requirePyObject("filter") + + val builder = DatasetBuilder.newBuilder() + .colNames(dataset.columnNames) + .colTypes(dataset.columnTypes) + + for (row in dataset.rowIndices) { + val filterArgs = Array(dataset.columnCount + 1) { col -> + if (col == 0) { + Py.newInteger(row) + } else { + Py.java2py(dataset[row, col - 1]) + } + } + val filterKeywords = Array(dataset.columnCount + 1) { col -> + if (col == 0) { + "row" + } else { + dataset.getColumnName(col - 1) + } + } + val returnValue = filter.__call__(filterArgs, filterKeywords).__nonzero__() + if (returnValue) { + val columnValues = Array(dataset.columnCount) { col -> + dataset[row, col] + } + builder.addRow(*columnValues) + } + } + + return builder.build() + } + + @Suppress("unused") + @ScriptFunction(docBundlePrefix = "DatasetExtensions") + @KeywordArgs( + names = ["dataset", "output", "includeTypes"], + types = [Dataset::class, Appendable::class, Boolean::class], + ) + fun print(args: Array, keywords: Array) { + val parsedArgs = PyArgParser.parseArgs( + args, + keywords, + arrayOf("dataset", "output", "includeTypes"), + Array(3) { PyObject::class.java }, + "print", + ) + val dataset = parsedArgs.requirePyObject("dataset").toJava() + val appendable = parsedArgs.getPyObject("output") + .orElse(Py.getSystemState().stdout) + .let(::PyObjectAppendable) + val includeTypes = parsedArgs.getBoolean("includeTypes").orElse(false) + + return printDataset(appendable, dataset, includeTypes) + } + + internal fun printDataset(appendable: Appendable, dataset: Dataset, includeTypes: Boolean = false) { + val typeNames = List(dataset.columnCount) { column -> + if (includeTypes) { + dataset.getColumnType(column).simpleName + } else { + "" + } + } + + val columnWidths = IntArray(dataset.columnCount) { column -> + maxOf( + // longest value in a row + if (dataset.rowCount > 0) { + dataset.rowIndices.maxOf { row -> dataset[row, column].toString().length } + } else { + 0 + }, + // longest value in a header + if (includeTypes) { + dataset.getColumnName(column).length + typeNames[column].length + 3 // 3 = two parens and a space + } else { + dataset.getColumnName(column).length + }, + // absolute minimum width for markdown table (and human eyeballs) + 3, + ) + } + + val separator = "|" + + fun Sequence.joinToBuffer() { + joinTo( + buffer = appendable, + separator = " $separator ", + prefix = "$separator ", + postfix = " $separator\n", + ) + } + + // headers + sequence { + yield("Row") + for (column in dataset.columnIndices) { + val headerValue = buildString { + append(dataset.getColumnName(column)) + if (includeTypes) { + append(" (").append(typeNames[column]).append(")") + } + while (length < columnWidths[column]) { + insert(0, ' ') + } + } + yield(headerValue) + } + }.joinToBuffer() + + // separator + sequence { + yield("---") + for (column in dataset.columnIndices) { + yield("-".repeat(columnWidths[column])) + } + }.joinToBuffer() + + // data + val maxRowLength = dataset.rowCount.toString().length.coerceAtLeast(3) + for (row in dataset.rowIndices) { + sequence { + yield(row.toString().padStart(maxRowLength)) + for (column in dataset.columnIndices) { + yield(dataset[row, column].toString().padStart(columnWidths[column])) + } + }.joinToBuffer() + } + } + + @Suppress("unused") + @ScriptFunction(docBundlePrefix = "DatasetExtensions") + @KeywordArgs( + names = ["input", "headerRow", "sheetNumber", "firstRow", "lastRow", "firstColumn", "lastColumn", "stringAll"], + types = [ByteArray::class, Integer::class, Integer::class, Integer::class, Integer::class, Integer::class, Integer::class, Array::class], + ) + fun fromExcel(args: Array, keywords: Array): Dataset { + val parsedArgs = PyArgParser.parseArgs( + args, + keywords, + arrayOf( + "input", + "headerRow", + "sheetNumber", + "firstRow", + "lastRow", + "firstColumn", + "lastColumn", + "stringAll" + ), + Array(8) { Any::class.java }, + "fromExcel", + ) + + val tempStringAll = parsedArgs.getInteger("stringAll").orElse(0) + val stringAll = tempStringAll != 0 + when (val input = parsedArgs.requirePyObject("input").toJava()) { + is String -> WorkbookFactory.create(File(input)) + is ByteArray -> WorkbookFactory.create(input.inputStream().buffered()) + else -> throw Py.TypeError("Unable to create Workbook from input; should be string or binary data. Got ${input::class.simpleName} instead.") + }.use { workbook -> + val sheetNumber = parsedArgs.getInteger("sheetNumber").orElse(0) + val sheet = workbook.getSheetAt(sheetNumber) + + val headerRow = parsedArgs.getInteger("headerRow").orElse(-1) + val firstRow = parsedArgs.getInteger("firstRow").orElseGet { max(sheet.firstRowNum, headerRow + 1) } + val lastRow = parsedArgs.getInteger("lastRow").orElseGet { sheet.lastRowNum } + + val dataRange = firstRow..lastRow + + if (firstRow >= lastRow) { + throw Py.ValueError("firstRow ($firstRow) must be less than lastRow ($lastRow)") + } + if (headerRow >= 0 && headerRow in dataRange) { + throw Py.ValueError("headerRow must not be in firstRow..lastRow ($dataRange)") + } + + val columnRow = sheet.getRow(if (headerRow >= 0) headerRow else firstRow) + val firstColumn = parsedArgs.getInteger("firstColumn").orElseGet { columnRow.firstCellNum.toInt() } + val lastColumn = + parsedArgs.getInteger("lastColumn").map { it + 1 }.orElseGet { columnRow.lastCellNum.toInt() } + if (firstColumn >= lastColumn) { + throw Py.ValueError("firstColumn ($firstColumn) must be less than lastColumn ($lastColumn)") + } + + val columnCount = lastColumn - firstColumn + + val dataset = DatasetBuilder() + dataset.colNames( + List(columnCount) { + if (headerRow >= 0) { + columnRow.getCell(it + firstColumn).toString() + } else { + "Col $it" + } + }, + ) + + var typesSet = false + val columnTypes = mutableListOf>() + + for (i in dataRange) { + if (i == headerRow) { + continue + } + + val row = sheet.getRow(i) + + val rowValues = Array(columnCount) { j -> + val cell = row.getCell(j + firstColumn) + if(cell == null){ + if (!typesSet) { + if (stringAll) { + columnTypes.add(String::class.java) + } + else{ + columnTypes.add(Any::class.java) + } + } + if (stringAll) { + null.toString() + } + else{ + null + } + } + else{ + val cellType = cell.getCellType().toString() + + if(cellType == "NUMERIC") { + if (DateUtil.isCellDateFormatted(cell)) { + if (!typesSet) { + if(stringAll){ + columnTypes.add(String::class.java) + } + else{ + columnTypes.add(Date::class.java) + } + + } + val tempCell = cell.dateCellValue + if(stringAll){ + tempCell.toString() + } + else{ + tempCell + } + } else { + val numericCellValue = cell.numericCellValue + if (BigDecimal(numericCellValue).scale() == 0) { + if (!typesSet) { + if(stringAll){ + columnTypes.add(String::class.java) + } + else{ + columnTypes.add(Int::class.javaObjectType) + } + } + if(stringAll){ + numericCellValue.toInt().toString() + } + else{ + numericCellValue.toInt() + } + } else { + if (!typesSet) { + if(stringAll){ + columnTypes.add(String::class.java) + } + else{ + columnTypes.add(Double::class.javaObjectType) + } + } + if(stringAll){ + numericCellValue.toString() + } + else{ + numericCellValue + } + } + } + } + + else if(cellType == "STRING"){ + if (!typesSet) { + columnTypes.add(String::class.java) + } + cell.stringCellValue + } + + else if(cellType == "BOOLEAN"){ + if (!typesSet) { + columnTypes.add(Boolean::class.javaObjectType) + } + if(stringAll){ + cell.booleanCellValue.toString() + } + else{ + cell.booleanCellValue + } + } + + else{ + if (!typesSet) { + if (stringAll) { + columnTypes.add(String::class.java) + } + else{ + columnTypes.add(Any::class.java) + } + } + if (stringAll) { + null.toString() + } + else{ + null + } + } + } + } + + if (!typesSet) { + typesSet = true + dataset.colTypes(columnTypes) + } + + dataset.addRow(*rowValues) + } + + return dataset.build() + } + } + + @Suppress("unused") + @ScriptFunction(docBundlePrefix = "DatasetExtensions") + fun equals( + @ScriptArg("dataset1") ds1: Dataset, + @ScriptArg("dataset2") ds2: Dataset, + ): Boolean { + return ds1 === ds2 || (columnsEqual(ds1, ds2) && valuesEqual(ds1, ds2)) + } + + @Suppress("unused") + @ScriptFunction(docBundlePrefix = "DatasetExtensions") + fun valuesEqual( + @ScriptArg("dataset1") ds1: Dataset, + @ScriptArg("dataset2") ds2: Dataset, + ): Boolean { + if (ds1 === ds2) { + return true + } + if (ds1.rowCount != ds2.rowCount || ds1.columnCount != ds2.columnCount) { + return false + } + return ds1.rowIndices.all { row -> + ds1.columnIndices.all { col -> + ds1[row, col] == ds2[row, col] + } + } + } + + @Suppress("unused") + @ScriptFunction(docBundlePrefix = "DatasetExtensions") + @JvmOverloads + fun columnsEqual( + @ScriptArg("dataset1") ds1: Dataset, + @ScriptArg("dataset2") ds2: Dataset, + @ScriptArg("ignoreCase") ignoreCase: Boolean = false, + @ScriptArg("includeTypes") includeTypes: Boolean = true, + ): Boolean { + if (ds1 === ds2) { + return true + } + if (ds1.columnCount != ds2.columnCount) { + return false + } + + val columnNames = ds1.columnNames zip ds2.columnNames + val columnNamesMatch = columnNames.all { (left, right) -> + left.equals(right, ignoreCase) + } + if (!columnNamesMatch) { + return false + } + + if (!includeTypes) { + return true + } + + val columnTypes = ds1.columnTypes.asSequence() zip ds2.columnTypes.asSequence() + return columnTypes.all { (left, right) -> + left == right + } + } + + private val classNameResolver = ClassNameResolver.createBasic() + + @ScriptFunction(docBundlePrefix = "DatasetExtensions") + @KeywordArgs( + names = ["**columns"], + types = [KeywordArgs::class], + ) + fun builder(args: Array, keywords: Array): DatasetBuilder { + if (args.size != keywords.size) throw Py.ValueError("builder must be called with only keyword arguments") + val colNames = keywords.toList() + val colTypes = args.mapIndexed { i, type -> + try { + type.asJavaClass() + } catch (e: ClassCastException) { + throw Py.TypeError("${keywords[i]} was a ${type::class.simpleName}, but should be a type or valid string typecode") + } + } + return DatasetBuilder.newBuilder().colNames(colNames).colTypes(colTypes) + } + + fun PyObject.asJavaClass(): Class<*>? = when (this) { + is PyBaseString -> classNameResolver.classForName(asString()) + !is PyType -> throw ClassCastException() + PyString.TYPE, PyUnicode.TYPE -> String::class.java + PyBoolean.TYPE -> Boolean::class.java + PyInteger.TYPE -> Int::class.java + PyLong.TYPE -> Long::class.java + PyFloat.TYPE -> Double::class.java + else -> toJava>() + } +} diff --git a/common/bin/main/org/imdc/extensions/common/DatasetExtensions.properties b/common/bin/main/org/imdc/extensions/common/DatasetExtensions.properties new file mode 100644 index 0000000..7b07c62 --- /dev/null +++ b/common/bin/main/org/imdc/extensions/common/DatasetExtensions.properties @@ -0,0 +1,49 @@ +map.desc=Applies a function to each row in a dataset, allowing it to be transformed in place efficiently. +map.param.dataset=The dataset to transform. Must not be null. +map.param.mapper=A callable reference to invoke for each row. Will receive each column's value as a named argument. +map.param.preserveColumnTypes=True if the types of the output dataset should match the input. Otherwise, the output dataset will lose type information. +map.returns=A modified dataset. + +filter.desc=Runs a filtering function on each row in a dataset, returning a truncated dataset. +filter.param.dataset=The dataset to filter. Must not be null. +filter.param.filter=A function to run on each row. Will be called with keyword arguments matching column names. The first argument will be named 'row' and is the row index. Return True to keep the row in the output dataset. +filter.returns=A modified dataset. + +print.desc=Prints a dataset to standard output, or the provided buffer. +print.param.dataset=The dataset to print. Must not be null. +print.param.output=The output destination. Defaults to sys.stdout. +print.param.includeTypes=If True, includes the type of the column in the first row. Defaults to False. +print.returns=None. + +fromExcel.desc=Creates a dataset by reading select cells from an Excel spreadsheet. +fromExcel.param.input=The Excel document to read - either the path to a file on disk, or a byte array with the contents of a file. +fromExcel.param.headerRow=The row number to use for the column names in the output dataset. +fromExcel.param.sheetNumber=The sheet number (zero-indexed) in the Excel document to extract data from. +fromExcel.param.firstRow=The first row (zero-indexed) in the Excel document to retrieve data from. If not supplied, the first non-empty row will be used. +fromExcel.param.lastRow=The last row (zero-indexed) in the Excel document to retrieve data from. If not supplied, the last non-empty row will be used. +fromExcel.param.firstColumn=The first column (zero-indexed) in the Excel document to retrieve data from. If not supplied, the first non-empty column will be used. +fromExcel.param.lastColumn=The last column (zero-indexed) in the Excel document to retrieve data from. If not supplied, the last non-empty column will be used. +fromExcel.param.stringAll=List of columns to String. +fromExcel.returns=A Dataset created from the Excel document. Types are assumed based on the first row of input data. + +equals.desc=Compares two datasets for structural equality. +equals.param.dataset1=The first dataset. Must not be null. +equals.param.dataset2=The second dataset. Must not be null. +equals.returns=True if the two datasets have the same number of columns, with the same types, in the same order, with the same data in each row. + +valuesEqual.desc=Compares two datasets' content. +valuesEqual.param.dataset1=The first dataset. Must not be null. +valuesEqual.param.dataset2=The second dataset. Must not be null. +valuesEqual.returns=True if the two datasets have the same values. + +columnsEqual.desc=Compares two datasets' column definitions. +columnsEqual.param.dataset1=The first dataset. Must not be null. +columnsEqual.param.dataset2=The second dataset. Must not be null. +columnsEqual.param.ignoreCase=Pass True if the column names should be compared case-insensitive. Defaults to False. +columnsEqual.param.includeTypes=Pass True if the column types must match as well. Defaults to True. +columnsEqual.returns=True if the two datasets have the same columns, per additional parameters. + +builder.desc=Creates a new dataset using supplied column names and types. +builder.param.**columns=Optional. Keyword arguments can be supplied to predefine column names and types. The value of the argument should be string "typecode" (see system.dataset.fromCSV) or a Java or Python class instance. +builder.returns=A DatasetBuilder object. Use addRow(value, ...) to add new values, and build() to construct the final dataset. \ + If keyword arguments were not supplied, column names and types can be manually declared using colNames() and colTypes(). \ No newline at end of file diff --git a/common/bin/main/org/imdc/extensions/common/ExtensionDocProvider.kt b/common/bin/main/org/imdc/extensions/common/ExtensionDocProvider.kt new file mode 100644 index 0000000..85bcb02 --- /dev/null +++ b/common/bin/main/org/imdc/extensions/common/ExtensionDocProvider.kt @@ -0,0 +1,34 @@ +package org.imdc.extensions.common + +import com.inductiveautomation.ignition.common.script.hints.PropertiesFileDocProvider +import com.inductiveautomation.ignition.common.script.hints.ScriptFunctionDocProvider +import java.lang.reflect.Method + +private val propertiesFileDocProvider = PropertiesFileDocProvider() + +private val WARNING = """ + THIS IS AN UNOFFICIAL IGNITION EXTENSION. + IT MAY RELY ON OR EXPOSE UNDOCUMENTED OR DANGEROUS FUNCTIONALITY. + USE AT YOUR OWN RISK. +""".trimIndent() + +object ExtensionDocProvider : ScriptFunctionDocProvider by propertiesFileDocProvider { + override fun getMethodDescription(path: String, method: Method): String { + val methodDescription: String? = propertiesFileDocProvider.getMethodDescription(path, method) + val unsafeAnnotation = method.getAnnotation() + + return buildString { + if (unsafeAnnotation != null) { + append("") + append(WARNING) + if (unsafeAnnotation.note.isNotEmpty()) { + append("
").append(unsafeAnnotation.note) + } + append("


") + } + append(methodDescription.orEmpty()) + } + } +} + +annotation class UnsafeExtension(val note: String = "") diff --git a/common/bin/main/org/imdc/extensions/common/ProjectExtensions.kt b/common/bin/main/org/imdc/extensions/common/ProjectExtensions.kt new file mode 100644 index 0000000..d229648 --- /dev/null +++ b/common/bin/main/org/imdc/extensions/common/ProjectExtensions.kt @@ -0,0 +1,7 @@ +package org.imdc.extensions.common + +import com.inductiveautomation.ignition.common.project.Project + +interface ProjectExtensions { + fun getProject(): Project +} diff --git a/common/bin/main/org/imdc/extensions/common/PyDatasetBuilder.kt b/common/bin/main/org/imdc/extensions/common/PyDatasetBuilder.kt new file mode 100644 index 0000000..bcc6155 --- /dev/null +++ b/common/bin/main/org/imdc/extensions/common/PyDatasetBuilder.kt @@ -0,0 +1,58 @@ +package org.imdc.extensions.common + +import com.inductiveautomation.ignition.common.Dataset +import com.inductiveautomation.ignition.common.script.DisposablePyObjectAdapter +import com.inductiveautomation.ignition.common.util.DatasetBuilder +import com.inductiveautomation.ignition.common.xmlserialization.ClassNameResolver +import org.imdc.extensions.common.DatasetExtensions.asJavaClass +import org.python.core.Py +import org.python.core.PyObject +import org.python.core.adapter.PyObjectAdapter + +@Suppress("unused") +class PyDatasetBuilder(private val builder: DatasetBuilder) : PyObject() { + private val resolver = ClassNameResolver.createBasic() + + fun colNames(vararg names: String) = apply { + builder.colNames(names.toList()) + } + + fun colNames(names: List) = apply { + builder.colNames(names) + } + + fun colTypes(vararg types: PyObject) = apply { + if (types.singleOrNull()?.isSequenceType == true) { + builder.colTypes(types.first().asIterable().map { it.asJavaClass() }) + } else { + builder.colTypes(types.map { it.asJavaClass() }) + } + } + + fun colTypes(types: List>) = apply { + builder.colTypes(types) + } + + fun addRow(vararg values: Any?) = apply { + builder.addRow(*values) + } + + fun build(): Dataset = builder.build() + + companion object { + private val adapter = DisposablePyObjectAdapter(DatasetBuilderAdapter()) + + fun register() { + Py.getAdapter().addPostClass(adapter) + } + + fun unregister() { + adapter.dispose() + } + } +} + +class DatasetBuilderAdapter : PyObjectAdapter { + override fun adapt(o: Any?): PyObject = PyDatasetBuilder(o as DatasetBuilder) + override fun canAdapt(o: Any?): Boolean = o is DatasetBuilder +} diff --git a/common/bin/main/org/imdc/extensions/common/TagExtensions.kt b/common/bin/main/org/imdc/extensions/common/TagExtensions.kt new file mode 100644 index 0000000..589e9be --- /dev/null +++ b/common/bin/main/org/imdc/extensions/common/TagExtensions.kt @@ -0,0 +1,63 @@ +package org.imdc.extensions.common + +import com.inductiveautomation.ignition.common.config.PyTagDictionary +import com.inductiveautomation.ignition.common.config.PyTagList +import com.inductiveautomation.ignition.common.script.PyArgParser +import com.inductiveautomation.ignition.common.script.ScriptContext +import com.inductiveautomation.ignition.common.script.builtin.KeywordArgs +import com.inductiveautomation.ignition.common.script.hints.ScriptFunction +import com.inductiveautomation.ignition.common.tags.config.TagConfigurationModel +import com.inductiveautomation.ignition.common.tags.model.TagPath +import com.inductiveautomation.ignition.common.tags.paths.parser.TagPathParser +import org.python.core.PyDictionary +import org.python.core.PyObject + +abstract class TagExtensions { + @UnsafeExtension + @ScriptFunction(docBundlePrefix = "TagExtensions") + @KeywordArgs( + names = ["basePath", "recursive"], + types = [String::class, Boolean::class], + ) + fun getLocalConfiguration(args: Array, keywords: Array): PyTagList { + val parsedArgs = PyArgParser.parseArgs( + args, + keywords, + arrayOf("basePath", "recursive"), + arrayOf(Any::class.java, Any::class.java), + "getLocalConfiguration", + ) + val configurationModels = getConfigurationImpl( + parseTagPath(parsedArgs.requireString("basePath")), + parsedArgs.getBoolean("recursive").orElse(false), + ) + + return configurationModels.toPyTagList() + } + + protected open fun parseTagPath(path: String): TagPath { + val parsed = TagPathParser.parse(ScriptContext.defaultTagProvider(), path) + if (TagPathParser.isRelativePath(parsed) && ScriptContext.relativeTagPathRoot() != null) { + return TagPathParser.derelativize(parsed, ScriptContext.relativeTagPathRoot()) + } + return parsed + } + + private fun TagConfigurationModel.toPyDictionary(): PyDictionary { + return PyTagDictionary.Builder() + .setTagPath(path) + .setTagType(type) + .build(this).apply { + if (children.isNotEmpty()) { + put("tags", children.toPyTagList()) + } + } + } + + private fun List.toPyTagList() = fold(PyTagList()) { acc, childModel -> + acc.add(childModel.toPyDictionary()) + acc + } + + protected abstract fun getConfigurationImpl(basePath: TagPath, recursive: Boolean): List +} diff --git a/common/bin/main/org/imdc/extensions/common/TagExtensions.properties b/common/bin/main/org/imdc/extensions/common/TagExtensions.properties new file mode 100644 index 0000000..45f8560 --- /dev/null +++ b/common/bin/main/org/imdc/extensions/common/TagExtensions.properties @@ -0,0 +1,4 @@ +getLocalConfiguration.desc=WIP +getLocalConfiguration.param.basePath=WIP +getLocalConfiguration.param.recursive=WIP +getLocalConfiguration.returns=WIP diff --git a/common/bin/main/org/imdc/extensions/common/Utilities.kt b/common/bin/main/org/imdc/extensions/common/Utilities.kt new file mode 100644 index 0000000..16981f0 --- /dev/null +++ b/common/bin/main/org/imdc/extensions/common/Utilities.kt @@ -0,0 +1,55 @@ +package org.imdc.extensions.common + +import com.inductiveautomation.ignition.common.BundleUtil +import com.inductiveautomation.ignition.common.Dataset +import org.python.core.Py +import org.python.core.PyObject +import java.lang.reflect.Method + +class PyObjectAppendable(target: PyObject) : Appendable { + private val writeMethod = target.__getattr__("write") + + override fun append(csq: CharSequence?): Appendable = apply { + writeMethod.__call__(Py.newStringOrUnicode(csq.toString())) + } + + override fun append(csq: CharSequence?, start: Int, end: Int): Appendable = apply { + append(csq.toString().subSequence(start, end)) + } + + override fun append(c: Char): Appendable = apply { + append(c.toString()) + } +} + +val Dataset.rowIndices: IntRange + get() = (0 until rowCount) + +val Dataset.columnIndices: IntRange + get() = (0 until columnCount) + +operator fun Dataset.get(row: Int, col: Int): Any? { + return getValueAt(row, col) +} + +inline fun PyObject.toJava(): T { + try { + val cast = + this.__tojava__(T::class.java) ?: throw Py.TypeError("Expected ${T::class.java.simpleName}, got None") + return cast as T + } catch (e: ClassCastException) { + throw Py.TypeError("Expected ${T::class.java.simpleName}") + } +} + +inline fun BundleUtil.addPropertyBundle() { + addBundle( + T::class.java.simpleName, + T::class.java.classLoader, + T::class.java.name.replace('.', '/'), + ) +} + +inline fun Method.getAnnotation(): T? { + return getAnnotation(T::class.java) +} diff --git a/common/bin/main/org/imdc/extensions/common/UtilitiesExtensions$KeywordParseContext.class b/common/bin/main/org/imdc/extensions/common/UtilitiesExtensions$KeywordParseContext.class new file mode 100644 index 0000000000000000000000000000000000000000..a8aba4bbe31ebb6bd8cbf17c44489242baa39aef GIT binary patch literal 3814 zcmbtX`F9jm7XDr*P^43V7zp4p7!@%KP^h>zA|xajOahuk<1#aq>X)XVyQ-}vB#tv~ zxQ{dAF7C@epdJMx=cqsVga44f&6)4Lu5?$2o|%s3oYbqj@4b8P`|f?;z5UC-zyATi z9r)5fMqtnn3YJ^SJCjjQcljVB(*ooW)X~&UYvLl+X+=55Vd`V2n#d09S ziuK{@d)kV?hSL?>p}?jwZ7!yR*0^2H3A9a8r5(kA6u7N^_CV!R`!*Q`u2;yVBo%LS z2dQIybkYrX?i1KIwl-AM-UBXAbdZ4du61+0x4FIHzCOXh9(9+vCqh_|5Ft9FQhCKpw-ZP^l7oKiwGwips&*E#_sg0eb{GM!f~Q=;nLMc672Qi+)%tmQ2y^MySlF6}OnU75kZy zcDXFQyuhADOH((X&TLMpxZNZ_+{qgk`ISk5Exmo0ZQvHZQZ3baS-+$11mztJg=s4hxL@mXMNy!pK&buJDI)XUN163^N!7842u4pMS+>>M)5Y zi$gf7(7o@!xJh)6Va&k&0tcG)HFN^E99ffkNchskxB|B;FX!#J7){xQ6M?_rGUizv z#{&jV2pnzJKx7v}OJ#?8l9K~V>EpNN&cMWjm=sv2=LF``P*g&j=E6kM{O!`#OC+4) zirHKnrty#gGSS|(k*JA>G0Ur@@`Y7J;GU*0dBuYvU9}RBbM1H(j~jSQAlEFG4b+(U zBmTt9W+D;Saoe*CG7vb-+t94mdW;ekbkBxkU`}9T?beLBVPql?GI7SA7ucB&SAFK{ z(x=u_Qvt4llLYK{xn$xLPP34bh4Y8QCC?GKJF#CgmXLrJFi=h$1Y|Ue(3gucQbl7V zRz*0sjy;C@+tCIePZ%f*{GoQx)Yd67?KDfLi2$rJ#^mtO@rfhM*=tiJu)%tgPmD7L z7I?uMD!$AoMGA`o`+JZ5AI)i|OHfzgdQvI)iz0{71}hBwRbZ&u2vkyy+#bu&dLSmp z&$}`|qrThm6rMKm44x$cGw-x;H33&Ss|U|3&N?fwJ)JC5Cj11}0}))=|I zHF`W%e_j^Yp(k{49p%Lfg18(>7T}~f8hBOUriK6yFG`1r;CrdXe;2s1;bIkLKd#k2 zooMrt+2iQGqvl0ILhNJ|t&wYg<4sc}$U5J~`==pfa&@KxukVh4p!M!lF{qw}TOV4r zym?RlM|gFo*)bW}&S`e8y2Uc^{z`y!9I8IBNH76LVvp^!?~T|_Bf4M zB2AGWnfMqV2wX!$wIaG|CA+0bKMou@;;Jrj`}E~M4ECx33YepwCxfA)9fp#CZ*K5& zfmuBv!{|OgjL^eww1xlckU^JfO8MSK*2o~+*{RxBzPIqJ0~`6Q%xpp@rH}Kwh2y6F zf$wld{{?LB-~Ao7eyulb=X0Z;cDd;k-%sO8%C>S7x8N#V%~8E6lNnsYcax?ok}M@; zE9~HSEoH6z>QP3;ARTR`(AlpOX!`wg#OvC-50QzvYXH2whOrH$Nq1z zbzm8HXK)VPmvCq{5e#N<0V7{s#OT09OpH@{#kZJR#v=l|FXGS3n9pDfir>IHkLV}Z z6Ke0CmSrqu@DrX|#@~ciFn$R=v$Xl#k927e&*6Ezkc=-S<15Md8doStc4-8C;#y`$ z8(<$ou2^7Ka|T5$Qo|C?;z5y#tC_~<-1mi+PyYu2KzeZi literal 0 HcmV?d00001 diff --git a/common/bin/main/org/imdc/extensions/common/UtilitiesExtensions.class b/common/bin/main/org/imdc/extensions/common/UtilitiesExtensions.class new file mode 100644 index 0000000000000000000000000000000000000000..32ef24f0d28672841c5e16a747cab3ef08253baa GIT binary patch literal 7168 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: QualifiedValue { + val qualifiedValue = expressions[0].execute() + val value = qualifiedValue.quality.isNot(QualityCode.Bad_NotFound) && + qualifiedValue.quality.isNot(QualityCode.Bad_Disabled) + return BasicQualifiedValue(value) + } + + override fun getArgDocString(): String = "value" + override fun getFunctionDisplayName(): String = NAME + override fun getType(): Class<*> = Boolean::class.java + + companion object { + const val NAME = "isAvailable" + const val CATEGORY = "Logic" + } +} diff --git a/common/bin/main/org/imdc/extensions/common/expressions/LogicalPredicate.kt b/common/bin/main/org/imdc/extensions/common/expressions/LogicalPredicate.kt new file mode 100644 index 0000000..97f0011 --- /dev/null +++ b/common/bin/main/org/imdc/extensions/common/expressions/LogicalPredicate.kt @@ -0,0 +1,44 @@ +package org.imdc.extensions.common.expressions + +import com.inductiveautomation.ignition.common.TypeUtilities +import com.inductiveautomation.ignition.common.expressions.Expression +import com.inductiveautomation.ignition.common.expressions.ExpressionFunctionManager +import com.inductiveautomation.ignition.common.expressions.functions.AbstractFunction +import com.inductiveautomation.ignition.common.model.values.BasicQualifiedValue +import com.inductiveautomation.ignition.common.model.values.QualifiedValue + +sealed class LogicalPredicate(val name: String) : AbstractFunction() { + override fun execute(expressions: Array): QualifiedValue { + return BasicQualifiedValue(expressions.apply()) + } + + abstract fun Array.apply(): Boolean + + override fun getArgDocString(): String = "values..." + override fun getFunctionDisplayName(): String = name + override fun getType(): Class<*> = Boolean::class.java + + object AllOfFunction : LogicalPredicate("allOf") { + override fun Array.apply() = all { it.toBoolean() } + } + + object AnyOfFunction : LogicalPredicate("anyOf") { + override fun Array.apply() = any { it.toBoolean() } + } + + object NoneOfFunction : LogicalPredicate("noneOf") { + override fun Array.apply() = none { it.toBoolean() } + } + + companion object { + private const val CATEGORY = "Logic" + + private fun Expression.toBoolean(): Boolean = TypeUtilities.toBool(execute().value) + + fun ExpressionFunctionManager.registerLogicFunctions() { + addFunction(AllOfFunction.name, CATEGORY, AllOfFunction) + addFunction(AnyOfFunction.name, CATEGORY, AnyOfFunction) + addFunction(NoneOfFunction.name, CATEGORY, NoneOfFunction) + } + } +} diff --git a/common/bin/test/org/imdc/extensions/common/DSBuilder.kt b/common/bin/test/org/imdc/extensions/common/DSBuilder.kt new file mode 100644 index 0000000..9dcc1cf --- /dev/null +++ b/common/bin/test/org/imdc/extensions/common/DSBuilder.kt @@ -0,0 +1,38 @@ +package org.imdc.extensions.common + +import com.inductiveautomation.ignition.common.BasicDataset +import com.inductiveautomation.ignition.common.Dataset + +class DSBuilder { + data class Column(val name: String, val rows: List<*>, val type: Class<*>) + + val columns = mutableListOf() + + inline fun column(name: String, data: List) { + columns.add(Column(name, data, T::class.java)) + } + + inline fun column(name: String, builder: MutableList.() -> Unit) { + column(name, buildList(builder)) + } + + fun build(): Dataset { + val colCount = columns.size + val rowCount = columns.maxOf { it.rows.size } + val data = Array(colCount) { arrayOfNulls(rowCount) } + + for (c in 0 until colCount) { + for (r in 0 until rowCount) { + data[c][r] = columns.getOrNull(c)?.rows?.getOrNull(r) + } + } + + return BasicDataset(columns.map { it.name }, columns.map { it.type }, data) + } + + companion object { + fun dataset(block: DSBuilder.() -> Unit): Dataset { + return DSBuilder().apply(block).build() + } + } +} diff --git a/common/bin/test/org/imdc/extensions/common/DatasetEqualityTests.kt b/common/bin/test/org/imdc/extensions/common/DatasetEqualityTests.kt new file mode 100644 index 0000000..a9cfd00 --- /dev/null +++ b/common/bin/test/org/imdc/extensions/common/DatasetEqualityTests.kt @@ -0,0 +1,156 @@ +package org.imdc.extensions.common + +import com.inductiveautomation.ignition.common.BasicDataset +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import org.imdc.extensions.common.DSBuilder.Companion.dataset +import java.util.Date + +class DatasetEqualityTests : FunSpec( + { + val dataset1 = dataset { + column("a", listOf(1, 2, 3)) + column("b", listOf(3.14, 2.18, 4.96)) + column("c", listOf("string", "strung", "strang")) + } + val copyOfDs1 = BasicDataset(dataset1) + val ds1WithCaps = BasicDataset(listOf("A", "B", "C"), dataset1.columnTypes, dataset1) + val ds1WithOtherNames = BasicDataset(listOf("ant", "bat", "cat"), dataset1.columnTypes, dataset1) + val dataset2 = dataset { + column("j", listOf(3, 2, 1)) + column("k", listOf(1.0, 2.0, 3.0)) + column("l", listOf("chess", "chass", "chuss")) + } + val copyOfDs2 = BasicDataset(dataset2) + val ds2WithCaps = BasicDataset(listOf("J", "K", "L"), dataset2.columnTypes, dataset2) + val ds2WithOtherNames = BasicDataset(listOf("joe", "kim", "lee"), dataset2.columnTypes, dataset2) + + val dataset3 = dataset { + column("t_stamp", listOf(1667605391000, 1667605392000, 1667605393000, 1667605394000).map(::Date)) + column("tag_1", listOf(1.0, 2.0, 3.0, 4.0)) + column("tag_2", listOf(true, false, true, false)) + } + val copyOfDs3 = BasicDataset(dataset3) + val ds3WithAliases = BasicDataset(listOf("timestamp", "doubleTag", "boolTag"), dataset3.columnTypes, dataset3) + + context("General equality") { + test("Same dataset") { + DatasetExtensions.equals(dataset1, dataset1) shouldBe true + DatasetExtensions.equals(dataset2, dataset2) shouldBe true + DatasetExtensions.equals(dataset3, dataset3) shouldBe true + } + + test("Copied dataset") { + DatasetExtensions.equals(dataset1, copyOfDs1) shouldBe true + DatasetExtensions.equals(dataset2, copyOfDs2) shouldBe true + DatasetExtensions.equals(dataset3, copyOfDs3) shouldBe true + } + + test("Different datasets") { + DatasetExtensions.equals(dataset1, dataset2) shouldBe false + DatasetExtensions.equals(dataset1, dataset3) shouldBe false + DatasetExtensions.equals(dataset2, dataset3) shouldBe false + } + + test("Datasets with same structure, but different columns") { + DatasetExtensions.equals(dataset1, ds1WithCaps) shouldBe false + DatasetExtensions.equals(dataset1, ds1WithOtherNames) shouldBe false + DatasetExtensions.equals(dataset2, ds2WithCaps) shouldBe false + DatasetExtensions.equals(dataset2, ds2WithOtherNames) shouldBe false + DatasetExtensions.equals(dataset3, ds3WithAliases) shouldBe false + } + } + + context("Value equality") { + test("Same dataset") { + DatasetExtensions.valuesEqual(dataset1, dataset1) shouldBe true + DatasetExtensions.valuesEqual(dataset2, dataset2) shouldBe true + DatasetExtensions.valuesEqual(dataset3, dataset3) shouldBe true + } + + test("Copied dataset") { + DatasetExtensions.valuesEqual(dataset1, copyOfDs1) shouldBe true + DatasetExtensions.valuesEqual(dataset2, copyOfDs2) shouldBe true + DatasetExtensions.valuesEqual(dataset3, copyOfDs3) shouldBe true + } + + test("Different datasets") { + DatasetExtensions.valuesEqual(dataset1, dataset2) shouldBe false + DatasetExtensions.valuesEqual(dataset1, dataset3) shouldBe false + DatasetExtensions.valuesEqual(dataset2, dataset3) shouldBe false + } + + test("Datasets with same structure, but different columns") { + DatasetExtensions.valuesEqual(dataset1, ds1WithCaps) shouldBe true + DatasetExtensions.valuesEqual(dataset1, ds1WithOtherNames) shouldBe true + DatasetExtensions.valuesEqual(dataset2, ds2WithCaps) shouldBe true + DatasetExtensions.valuesEqual(dataset2, ds2WithOtherNames) shouldBe true + DatasetExtensions.valuesEqual(dataset3, ds3WithAliases) shouldBe true + } + } + + context("Column equality") { + test("Same dataset") { + DatasetExtensions.columnsEqual(dataset1, dataset1) shouldBe true + DatasetExtensions.columnsEqual(dataset2, dataset2) shouldBe true + DatasetExtensions.columnsEqual(dataset3, dataset3) shouldBe true + } + + test("Copied dataset") { + DatasetExtensions.columnsEqual(dataset1, copyOfDs1) shouldBe true + DatasetExtensions.columnsEqual(dataset2, copyOfDs2) shouldBe true + DatasetExtensions.columnsEqual(dataset3, copyOfDs3) shouldBe true + } + + test("Different datasets") { + DatasetExtensions.columnsEqual(dataset1, dataset2) shouldBe false + DatasetExtensions.columnsEqual(dataset1, dataset3) shouldBe false + DatasetExtensions.columnsEqual(dataset2, dataset3) shouldBe false + } + + test("Datasets with same structure, but different columns") { + DatasetExtensions.columnsEqual(dataset1, ds1WithCaps) shouldBe false + DatasetExtensions.columnsEqual(dataset1, ds1WithCaps, ignoreCase = true) shouldBe true + DatasetExtensions.columnsEqual(dataset1, ds1WithOtherNames) shouldBe false + DatasetExtensions.columnsEqual(dataset2, ds2WithCaps) shouldBe false + DatasetExtensions.columnsEqual(dataset2, ds2WithCaps, ignoreCase = true) shouldBe true + DatasetExtensions.columnsEqual(dataset2, ds2WithOtherNames) shouldBe false + DatasetExtensions.columnsEqual(dataset3, ds3WithAliases) shouldBe false + + val ds1WithDifferentTypes = BasicDataset( + dataset1.columnNames, + listOf(String::class.java, Int::class.java, Boolean::class.java), + dataset1, + ) + DatasetExtensions.columnsEqual(dataset1, ds1WithDifferentTypes) shouldBe false + DatasetExtensions.columnsEqual(dataset1, ds1WithDifferentTypes, ignoreCase = true) shouldBe false + DatasetExtensions.columnsEqual(dataset1, ds1WithDifferentTypes, includeTypes = false) shouldBe true + DatasetExtensions.columnsEqual( + dataset1, + ds1WithDifferentTypes, + includeTypes = false, + ignoreCase = true, + ) shouldBe true + + val ds1WithDifferentTypesAndCase = BasicDataset( + listOf("A", "B", "C"), + dataset1.columnTypes, + dataset1, + ) + DatasetExtensions.columnsEqual(dataset1, ds1WithDifferentTypesAndCase) shouldBe false + DatasetExtensions.columnsEqual(dataset1, ds1WithDifferentTypesAndCase, ignoreCase = true) shouldBe true + DatasetExtensions.columnsEqual( + dataset1, + ds1WithDifferentTypesAndCase, + includeTypes = false, + ) shouldBe false + DatasetExtensions.columnsEqual( + dataset1, + ds1WithDifferentTypesAndCase, + ignoreCase = true, + includeTypes = false, + ) shouldBe true + } + } + }, +) diff --git a/common/bin/test/org/imdc/extensions/common/DatasetExtensionsTests.kt b/common/bin/test/org/imdc/extensions/common/DatasetExtensionsTests.kt new file mode 100644 index 0000000..2cda029 --- /dev/null +++ b/common/bin/test/org/imdc/extensions/common/DatasetExtensionsTests.kt @@ -0,0 +1,556 @@ +package org.imdc.extensions.common + +import com.inductiveautomation.ignition.common.BasicDataset +import com.inductiveautomation.ignition.common.Dataset +import com.inductiveautomation.ignition.common.util.DatasetBuilder +import io.kotest.assertions.withClue +import io.kotest.matchers.shouldBe +import org.imdc.extensions.common.DatasetExtensions.printDataset +import org.python.core.Py +import java.awt.Color +import java.util.Date +import kotlin.io.path.createTempFile +import kotlin.io.path.writeBytes + +@Suppress("PyUnresolvedReferences", "PyInterpreter") +class DatasetExtensionsTests : JythonTest( + { globals -> + globals["utils"] = DatasetExtensions + globals["builder"] = DatasetBuilder.newBuilder() + globals["dataset"] = DatasetBuilder.newBuilder() + .colNames("a", "b", "c") + .colTypes(Int::class.javaObjectType, Double::class.javaObjectType, String::class.java) + .addRow(1, 3.14, "pi") + .addRow(2, 6.28, "tau") + .build() + + val excelSample = + DatasetExtensionsTests::class.java.getResourceAsStream("sample.xlsx")!!.readAllBytes() + val tempXlsx = createTempFile(suffix = "xlsx").also { + it.writeBytes(excelSample) + it.toFile().deleteOnExit() + } + globals["xlsxBytes"] = excelSample + globals["xlsxFile"] = tempXlsx.toString() + + val xlsSample = + DatasetExtensionsTests::class.java.getResourceAsStream("sample.xls")!!.readAllBytes() + val tempXls = createTempFile(suffix = "xls").also { + it.writeBytes(xlsSample) + it.toFile().deleteOnExit() + } + globals["xlsBytes"] = xlsSample + globals["xlsFile"] = tempXls.toString() + + globals["date"] = Date::class.java + globals["color"] = Color::class.java + globals["javaInt"] = Int::class.java + globals["javaString"] = String::class.java + globals["javaBool"] = Boolean::class.java + }, +) { + private fun Dataset.asClue(assertions: (Dataset) -> Unit) { + withClue( + lazy { + buildString { + printDataset(this, this@asClue, true) + } + }, + ) { + assertions(this) + } + } + + init { + PyDatasetBuilder.register() + + context("Map tests") { + test("Null dataset") { + shouldThrowPyException(Py.TypeError) { + eval("utils.map(None, None)") + } + } + + test("Simple mapper, preserving types") { + eval("utils.map(dataset, lambda a, b, c: (a * 2, b * 2, c * 2), True)").asClue { + it.columnNames shouldBe listOf("a", "b", "c") + it.columnTypes shouldBe listOf( + Int::class.javaObjectType, + Double::class.javaObjectType, + String::class.java, + ) + it.rowCount shouldBe 2 + it.getColumnAsList(0) shouldBe listOf(2, 4) + it.getColumnAsList(1) shouldBe listOf(6.28, 12.56) + it.getColumnAsList(2) shouldBe listOf("pipi", "tautau") + } + } + + test("Simple mapper, discarding types") { + eval("utils.map(dataset, lambda a, b, c: (a * 2, b * 2, c * 2), False)").asClue { + it.columnNames shouldBe listOf("a", "b", "c") + it.columnTypes shouldBe listOf(Any::class.java, Any::class.java, Any::class.java) + it.rowCount shouldBe 2 + it.getColumnAsList(0) shouldBe listOf(2, 4) + it.getColumnAsList(1) shouldBe listOf(6.28, 12.56) + it.getColumnAsList(2) shouldBe listOf("pipi", "tautau") + } + } + } + + context("Filter tests") { + test("Constant filter") { + eval("utils.filter(dataset, lambda **kwargs: False)").asClue { + it.columnNames shouldBe listOf("a", "b", "c") + it.columnTypes shouldBe listOf( + Int::class.javaObjectType, + Double::class.javaObjectType, + String::class.java, + ) + it.rowCount shouldBe 0 + } + } + + test("Conditional filter all kwargs") { + eval("utils.filter(dataset, lambda **kwargs: kwargs['row'] >= 1)").asClue { + it.columnNames shouldBe listOf("a", "b", "c") + it.columnTypes shouldBe listOf( + Int::class.javaObjectType, + Double::class.javaObjectType, + String::class.java, + ) + it.rowCount shouldBe 1 + } + } + + test("Conditional filter unpacking row") { + eval("utils.filter(dataset, lambda row, **kwargs: row >= 1)").asClue { + it.columnNames shouldBe listOf("a", "b", "c") + it.columnTypes shouldBe listOf( + Int::class.javaObjectType, + Double::class.javaObjectType, + String::class.java, + ) + it.rowCount shouldBe 1 + } + } + } + + context("Print tests") { + context("Basic dataset") { + test("Without types") { + buildString { + printDataset(this, globals["dataset"]) + } shouldBe """ + | Row | a | b | c | + | --- | --- | ---- | --- | + | 0 | 1 | 3.14 | pi | + | 1 | 2 | 6.28 | tau | + + """.trimIndent() + } + + test("With types") { + buildString { + printDataset(this, globals["dataset"], includeTypes = true) + } shouldBe """ + | Row | a (Integer) | b (Double) | c (String) | + | --- | ----------- | ---------- | ---------- | + | 0 | 1 | 3.14 | pi | + | 1 | 2 | 6.28 | tau | + + """.trimIndent() + } + } + + val emptyDataset = BasicDataset( + listOf("a", "b", "c"), + listOf(String::class.java, Int::class.java, Boolean::class.java), + ) + context("Empty dataset") { + test("Without types") { + buildString { + printDataset(this, emptyDataset) + } shouldBe """ + | Row | a | b | c | + | --- | --- | --- | --- | + + """.trimIndent() + } + + test("With types") { + buildString { + printDataset(this, emptyDataset, includeTypes = true) + } shouldBe """ + | Row | a (String) | b (int) | c (boolean) | + | --- | ---------- | ------- | ----------- | + + """.trimIndent() + } + } + } + + context("fromExcel") { + test("XLSX file") { + eval("utils.fromExcel(xlsxFile)").asClue { + it.rowCount shouldBe 100 + it.columnCount shouldBe 16 + } + } + test("XLS file") { + eval("utils.fromExcel(xlsFile)").asClue { + it.rowCount shouldBe 100 + it.columnCount shouldBe 16 + } + } + test("XLSX bytes") { + eval("utils.fromExcel(xlsxBytes)").asClue { + it.rowCount shouldBe 100 + it.columnCount shouldBe 16 + } + } + test("XLS bytes") { + eval("utils.fromExcel(xlsBytes)").asClue { + it.rowCount shouldBe 100 + it.columnCount shouldBe 16 + } + } + test("With headers") { + eval("utils.fromExcel(xlsxBytes, headerRow=0)").asClue { + it.rowCount shouldBe 99 + it.columnCount shouldBe 16 + it.columnNames shouldBe listOf( + "Segment", + "Country", + "Product", + "Discount Band", + "Units Sold", + "Manufacturing Price", + "Sale Price", + "Gross Sales", + "Discounts", + "Sales", + "COGS", + "Profit", + "Date", + "Month Number", + "Month Name", + "Year", + ) + } + } + test("First row") { + eval("utils.fromExcel(xlsxBytes, headerRow=0, firstRow=50)").asClue { + it.rowCount shouldBe 50 + it.columnCount shouldBe 16 + } + } + test("Last row") { + eval("utils.fromExcel(xlsxBytes, headerRow=0, lastRow=50)").asClue { + it.rowCount shouldBe 50 + it.columnCount shouldBe 16 + } + } + test("First & last row") { + eval("utils.fromExcel(xlsxBytes, headerRow=0, firstRow=5, lastRow=10)").asClue { + it.rowCount shouldBe 6 + it.columnCount shouldBe 16 + } + } + test("First column") { + eval("utils.fromExcel(xlsxBytes, headerRow=0, firstColumn=10)").asClue { + it.rowCount shouldBe 99 + it.columnCount shouldBe 6 + it.columnNames shouldBe listOf( + "COGS", + "Profit", + "Date", + "Month Number", + "Month Name", + "Year", + ) + } + } + test("Last column") { + eval("utils.fromExcel(xlsxBytes, headerRow=0, lastColumn=5)").asClue { + it.rowCount shouldBe 99 + it.columnCount shouldBe 6 + it.columnNames shouldBe listOf( + "Segment", + "Country", + "Product", + "Discount Band", + "Units Sold", + "Manufacturing Price", + ) + } + } + test("First & last column") { + eval("utils.fromExcel(xlsxBytes, headerRow=0, firstColumn=5, lastColumn=10)").asClue { + it.rowCount shouldBe 99 + it.columnCount shouldBe 6 + it.columnNames shouldBe listOf( + "Manufacturing Price", + "Sale Price", + "Gross Sales", + "Discounts", + "Sales", + "COGS", + ) + } + } + } + + context("Builder") { + test("Basic usage") { + eval("utils.builder(a=int, b=str, c=bool).addRow(1, '2', False).build()").asClue { + it.rowCount shouldBe 1 + it.columnCount shouldBe 3 + it.columnNames shouldBe listOf( + "a", + "b", + "c", + ) + it.columnTypes shouldBe listOf( + Int::class.java, + String::class.java, + Boolean::class.java, + ) + } + } + + test("String type codes in builder call") { + eval("utils.builder(a='i', b='str', c='b').addRow(1, '2', False).build()").asClue { + it.rowCount shouldBe 1 + it.columnCount shouldBe 3 + it.columnNames shouldBe listOf( + "a", + "b", + "c", + ) + it.columnTypes shouldBe listOf( + Int::class.java, + String::class.java, + Boolean::class.java, + ) + } + } + + test("Separate colTypes as java types") { + eval( + """ + utils.builder() \ + .colNames('a', 'b', 'c') \ + .colTypes(javaInt, javaString, javaBool) \ + .addRow(1, '2', False) \ + .build() + """.trimIndent(), + ).asClue { + it.rowCount shouldBe 1 + it.columnCount shouldBe 3 + it.columnNames shouldBe listOf( + "a", + "b", + "c", + ) + it.columnTypes shouldBe listOf( + Int::class.java, + String::class.java, + Boolean::class.java, + ) + } + } + + test("Separate colTypes as Python types") { + eval( + """ + utils.builder() \ + .colNames('a', 'b', 'c') \ + .colTypes(int, str, bool) \ + .addRow(1, '2', False) \ + .build() + """.trimIndent(), + ).asClue { + it.rowCount shouldBe 1 + it.columnCount shouldBe 3 + it.columnNames shouldBe listOf( + "a", + "b", + "c", + ) + it.columnTypes shouldBe listOf( + Int::class.java, + String::class.java, + Boolean::class.java, + ) + } + } + + test("Separate colTypes as string shortcodes") { + eval( + """ + utils.builder() \ + .colNames('a', 'b', 'c') \ + .colTypes('i', 'str', 'b') \ + .addRow(1, '2', False) \ + .build() + """.trimIndent(), + ).asClue { + it.rowCount shouldBe 1 + it.columnCount shouldBe 3 + it.columnNames shouldBe listOf( + "a", + "b", + "c", + ) + it.columnTypes shouldBe listOf( + Int::class.java, + String::class.java, + Boolean::class.java, + ) + } + } + + test("Complex types") { + eval( + """ + utils.builder(date=date, color=color, unicode=unicode) \ + .addRow(date(), color.RED, u'test') \ + .build() + """.trimIndent(), + ).asClue { + it.rowCount shouldBe 1 + it.columnCount shouldBe 3 + it.columnNames shouldBe listOf( + "date", + "color", + "unicode", + ) + it.columnTypes shouldBe listOf( + Date::class.java, + Color::class.java, + String::class.java, + ) + } + } + + test("Nulls in rows") { + eval( + """ + utils.builder(a=int, b=str, c=bool) \ + .addRow(1, '2', False) \ + .addRow(None, None, None) \ + .build() + """.trimIndent(), + ).asClue { + it.rowCount shouldBe 2 + it.columnCount shouldBe 3 + it.columnNames shouldBe listOf( + "a", + "b", + "c", + ) + it.columnTypes shouldBe listOf( + Int::class.java, + String::class.java, + Boolean::class.java, + ) + } + } + + test("Empty dataset") { + eval("utils.builder().build()").asClue { + it.rowCount shouldBe 0 + it.columnCount shouldBe 0 + } + } + + test("Add a row as a list") { + eval( + """ + utils.builder(a=int, b=str, c=bool) \ + .addRow([1, '2', False]) \ + .build() + """.trimIndent(), + ).asClue { + it.rowCount shouldBe 1 + it.columnCount shouldBe 3 + } + } + + test("Add a row as a tuple") { + eval( + """ + utils.builder(a=int, b=str, c=bool) \ + .addRow((1, '2', False)) \ + .build() + """.trimIndent(), + ).asClue { + it.rowCount shouldBe 1 + it.columnCount shouldBe 3 + } + } + + test("Columns with complex names") { + eval( + """ + utils.builder(**{'a space': int, u'😍': bool, r',./;\'[]-=<>?:"{}|_+!@#%^&*()`~': str}) \ + .addRow((1, '2', False)) \ + .build() + """.trimIndent(), + ).asClue { + it.rowCount shouldBe 1 + it.columnCount shouldBe 3 + it.columnNames shouldBe listOf( + "a space", + "\uD83D\uDE0D", + """,./;\'[]-=<>?:"{}|_+!@#%^&*()`~""", + ) + } + } + + test("Invalid types") { + shouldThrowPyException(Py.TypeError) { + eval("utils.builder(a=1).build()") + } + shouldThrowPyException(Py.TypeError) { + eval("utils.builder(a=None).build()") + } + } + + test("Row without enough vararg values") { + shouldThrowPyException(Py.TypeError) { + eval( + """ + utils.builder(a=int, b=str, c=bool) \ + .addRow(1, '2') \ + .build() + """.trimIndent(), + ) + } + } + + test("Row without enough list values") { + shouldThrowPyException(Py.TypeError) { + eval( + """ + utils.builder(a=int, b=str, c=bool) \ + .addRow([1, '2']) \ + .build() + """.trimIndent(), + ) + } + } + + test("Column types regression (issue #24)") { + eval( + """ + builder.colNames(dataset.columnNames) \ + .colTypes(dataset.columnTypes) \ + .build() + """.trimIndent(), + ) + } + } + } +} diff --git a/common/bin/test/org/imdc/extensions/common/ExpressionUtils.kt b/common/bin/test/org/imdc/extensions/common/ExpressionUtils.kt new file mode 100644 index 0000000..283fa44 --- /dev/null +++ b/common/bin/test/org/imdc/extensions/common/ExpressionUtils.kt @@ -0,0 +1,40 @@ +package org.imdc.extensions.common + +import com.inductiveautomation.ignition.common.expressions.AbstractFunctionFactory +import com.inductiveautomation.ignition.common.expressions.ConstantExpression +import com.inductiveautomation.ignition.common.expressions.Expression +import com.inductiveautomation.ignition.common.expressions.ExpressionParseContext +import com.inductiveautomation.ignition.common.expressions.FunctionFactory +import com.inductiveautomation.ignition.common.expressions.functions.Function +import com.inductiveautomation.ignition.common.expressions.parsing.ELParserHarness + +class ExpressionTestHarness : AbstractFunctionFactory(null) { + private val parser = ELParserHarness() + + fun evaluate(expression: String, constants: Map = emptyMap()): Any? { + val parseContext = StaticParseContext(constants, this) + return parser.parse(expression, parseContext).execute().value + } + + companion object { + suspend fun withFunction(name: String, function: Function, test: suspend ExpressionTestHarness.() -> Unit) { + ExpressionTestHarness().apply { + addFunction(name, "", function) + test() + } + } + } +} + +class StaticParseContext( + private val keywords: Map, + private val functionFactory: FunctionFactory, +) : ExpressionParseContext { + override fun createBoundExpression(reference: String): Expression { + return ConstantExpression(keywords.getValue(reference)) + } + + override fun getFunctionFactory(): FunctionFactory { + return functionFactory + } +} diff --git a/common/bin/test/org/imdc/extensions/common/JythonTest.kt b/common/bin/test/org/imdc/extensions/common/JythonTest.kt new file mode 100644 index 0000000..9144e89 --- /dev/null +++ b/common/bin/test/org/imdc/extensions/common/JythonTest.kt @@ -0,0 +1,93 @@ +package org.imdc.extensions.common + +import com.inductiveautomation.ignition.common.TypeUtilities +import io.kotest.assertions.fail +import io.kotest.core.listeners.BeforeEachListener +import io.kotest.core.spec.style.FunSpec +import io.kotest.core.test.TestCase +import io.kotest.matchers.Matcher +import io.kotest.matchers.MatcherResult +import org.intellij.lang.annotations.Language +import org.python.core.CompileMode +import org.python.core.CompilerFlags +import org.python.core.Py +import org.python.core.PyBaseException +import org.python.core.PyCode +import org.python.core.PyException +import org.python.core.PyObject +import org.python.core.PyStringMap +import org.python.core.PyType + +@Suppress("PyInterpreter") +abstract class JythonTest(init: (globals: PyStringMap) -> Unit) : FunSpec() { + protected var globals: PyStringMap = PyStringMap() + + init { + extension( + object : BeforeEachListener { + override suspend fun beforeEach(testCase: TestCase) { + globals.clear() + init(globals) + } + }, + ) + } + + protected inline fun eval(@Language("python") script: String): T { + exec("$RESULT = $script") + return globals[RESULT] + } + + protected fun exec(@Language("python") script: String?) { + val compiledCall = compile(script) + Py.runCode(compiledCall, null, globals) + } + + private fun compile(@Language("python") script: String?): PyCode { + return Py.compile_flags( + script, + "", + CompileMode.exec, + CompilerFlags(CompilerFlags.PyCF_SOURCE_IS_UTF8), + ) + } + + fun shouldThrowPyException(type: PyObject, case: () -> Unit) { + require(type is PyType) + require(type.isSubType(PyBaseException.TYPE)) + try { + case() + } catch (exception: PyException) { + PyExceptionTypeMatcher(exception).test(type) + return + } + fail("Expected a $type to be thrown, but nothing was thrown") + } + + companion object { + const val RESULT = "__RESULT" + + init { + Py.setSystemState(Py.defaultSystemState) + } + + inline operator fun PyStringMap.get(variable: String): T { + val value = this[Py.newStringOrUnicode(variable)] + return TypeUtilities.pyToJava(value) as T + } + + operator fun PyStringMap.set(key: String, value: Any?) { + return __setitem__(key, Py.java2py(value)) + } + + class PyExceptionTypeMatcher(val expected: PyException) : Matcher { + override fun test(value: PyObject): MatcherResult { + return MatcherResult( + expected.match(value), + failureMessageFn = { "Expected a $expected, but was $value" }, + negatedFailureMessageFn = { "Result should not be a $expected, but was $value" }, + ) + } + } + } +} diff --git a/common/bin/test/org/imdc/extensions/common/LogicalFunctionsTests.kt b/common/bin/test/org/imdc/extensions/common/LogicalFunctionsTests.kt new file mode 100644 index 0000000..6aba617 --- /dev/null +++ b/common/bin/test/org/imdc/extensions/common/LogicalFunctionsTests.kt @@ -0,0 +1,49 @@ +package org.imdc.extensions.common + +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import org.imdc.extensions.common.ExpressionTestHarness.Companion.withFunction +import org.imdc.extensions.common.expressions.LogicalPredicate.AllOfFunction +import org.imdc.extensions.common.expressions.LogicalPredicate.AnyOfFunction +import org.imdc.extensions.common.expressions.LogicalPredicate.NoneOfFunction + +class LogicalFunctionsTests : FunSpec() { + init { + context("AllOf") { + withFunction("allOf", AllOfFunction) { + test("No arguments") { + evaluate("allOf()") shouldBe true + } + test("Simple booleans") { + evaluate("allOf(true, true)") shouldBe true + evaluate("allOf(true, false)") shouldBe false + evaluate("allOf(false, false)") shouldBe false + } + } + } + context("AnyOf") { + withFunction("anyOf", AnyOfFunction) { + test("No arguments") { + evaluate("anyOf()") shouldBe false + } + test("Simple booleans") { + evaluate("anyOf(true, true)") shouldBe true + evaluate("anyOf(true, false)") shouldBe true + evaluate("anyOf(false, false)") shouldBe false + } + } + } + context("NoneOf") { + withFunction("noneOf", NoneOfFunction) { + test("No arguments") { + evaluate("noneOf()") shouldBe true + } + test("Simple booleans") { + evaluate("noneOf(true, true)") shouldBe false + evaluate("noneOf(true, false)") shouldBe false + evaluate("noneOf(false, false)") shouldBe true + } + } + } + } +} diff --git a/common/bin/test/org/imdc/extensions/common/UtilitiesExtensionsTests.kt b/common/bin/test/org/imdc/extensions/common/UtilitiesExtensionsTests.kt new file mode 100644 index 0000000..a853fdd --- /dev/null +++ b/common/bin/test/org/imdc/extensions/common/UtilitiesExtensionsTests.kt @@ -0,0 +1,50 @@ +package org.imdc.extensions.common + +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.shouldBe +import io.mockk.mockk +import org.python.core.Py +import org.python.core.PyDictionary +import org.python.core.PyList + +@Suppress("PyUnresolvedReferences", "PyInterpreter") +class UtilitiesExtensionsTests : JythonTest( + { globals -> + globals["utils"] = UtilitiesExtensions(mockk()) + }, +) { + init { + context("Deep copy tests") { + test("No input") { + shouldThrowPyException(Py.TypeError) { + eval("utils.deepCopy()") + } + } + test("Null input") { + eval("utils.deepCopy(None)").shouldBeNull() + } + test("String input") { + eval("utils.deepCopy('abc')") shouldBe "abc" + eval("utils.deepCopy(u'abc')") shouldBe "abc" + } + context("Simple conversions") { + test("List") { + eval("utils.deepCopy([1, 2, 3])") shouldBe listOf(1, 2, 3) + } + test("Tuple") { + eval("utils.deepCopy((1, 2, 3))") shouldBe listOf(1, 2, 3) + } + test("Set") { + eval("utils.deepCopy({1, 2, 3})") shouldBe listOf(1, 2, 3) + } + test("Dict") { + eval("utils.deepCopy({'a': 1, 'b': 2, 'c': 3})\n") shouldBe mapOf( + "a" to 1, + "b" to 2, + "c" to 3, + ) + } + } + } + } +} diff --git a/common/bin/test/org/imdc/extensions/common/sample.xls b/common/bin/test/org/imdc/extensions/common/sample.xls new file mode 100644 index 0000000000000000000000000000000000000000..7ec3bda64796095198e906eae5bed8da70097a96 GIT binary patch literal 35328 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 B$cF#` literal 0 HcmV?d00001 diff --git a/common/bin/test/org/imdc/extensions/common/sample.xlsx b/common/bin/test/org/imdc/extensions/common/sample.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..15fd838ccc7e46c30f13085d89ba9154dbcfeec5 GIT binary patch literal 13410 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::class], ) fun fromExcel(args: Array, keywords: Array): Dataset { val parsedArgs = PyArgParser.parseArgs( @@ -235,11 +235,14 @@ object DatasetExtensions { "lastRow", "firstColumn", "lastColumn", + "stringAll" ), - Array(7) { Any::class.java }, + Array(8) { Any::class.java }, "fromExcel", ) + val tempStringAll = parsedArgs.getInteger("stringAll").orElse(0) + val stringAll = tempStringAll != 0 when (val input = parsedArgs.requirePyObject("input").toJava()) { is String -> WorkbookFactory.create(File(input)) is ByteArray -> WorkbookFactory.create(input.inputStream().buffered()) @@ -291,52 +294,116 @@ object DatasetExtensions { } val row = sheet.getRow(i) - + val rowValues = Array(columnCount) { j -> val cell = row.getCell(j + firstColumn) - - when (cell?.cellType.takeUnless { it == FORMULA } ?: cell.cachedFormulaResultType) { - NUMERIC -> { + if(cell == null){ + if (!typesSet) { + if (stringAll) { + columnTypes.add(String::class.java) + } + else{ + columnTypes.add(Any::class.java) + } + } + if (stringAll) { + null.toString() + } + else{ + null + } + } + else{ + val cellType = cell.getCellType().toString() + + if(cellType == "NUMERIC") { if (DateUtil.isCellDateFormatted(cell)) { if (!typesSet) { - columnTypes.add(Date::class.java) + if(stringAll){ + columnTypes.add(String::class.java) + } + else{ + columnTypes.add(Date::class.java) + } + + } + val tempCell = cell.dateCellValue + if(stringAll){ + tempCell.toString() + } + else{ + tempCell } - cell.dateCellValue } else { val numericCellValue = cell.numericCellValue if (BigDecimal(numericCellValue).scale() == 0) { if (!typesSet) { - columnTypes.add(Int::class.javaObjectType) + if(stringAll){ + columnTypes.add(String::class.java) + } + else{ + columnTypes.add(Int::class.javaObjectType) + } + } + if(stringAll){ + numericCellValue.toInt().toString() } - numericCellValue.toInt() + else{ + numericCellValue.toInt() + } } else { if (!typesSet) { - columnTypes.add(Double::class.javaObjectType) + if(stringAll){ + columnTypes.add(String::class.java) + } + else{ + columnTypes.add(Double::class.javaObjectType) + } + } + if(stringAll){ + numericCellValue.toString() + } + else{ + numericCellValue } - numericCellValue } } } - STRING -> { + else if(cellType == "STRING"){ if (!typesSet) { columnTypes.add(String::class.java) } cell.stringCellValue } - BOOLEAN -> { + else if(cellType == "BOOLEAN"){ if (!typesSet) { columnTypes.add(Boolean::class.javaObjectType) } - cell.booleanCellValue + if(stringAll){ + cell.booleanCellValue.toString() + } + else{ + cell.booleanCellValue + } } - else -> { + else{ if (!typesSet) { - columnTypes.add(Any::class.java) + if (stringAll) { + columnTypes.add(String::class.java) + } + else{ + columnTypes.add(Any::class.java) + } + } + if (stringAll) { + null.toString() + } + else{ + null } - null } } } diff --git a/common/src/main/resources/org/imdc/extensions/common/DatasetExtensions.properties b/common/src/main/resources/org/imdc/extensions/common/DatasetExtensions.properties index 990ae7d..7b07c62 100644 --- a/common/src/main/resources/org/imdc/extensions/common/DatasetExtensions.properties +++ b/common/src/main/resources/org/imdc/extensions/common/DatasetExtensions.properties @@ -23,6 +23,7 @@ fromExcel.param.firstRow=The first row (zero-indexed) in the Excel document to r fromExcel.param.lastRow=The last row (zero-indexed) in the Excel document to retrieve data from. If not supplied, the last non-empty row will be used. fromExcel.param.firstColumn=The first column (zero-indexed) in the Excel document to retrieve data from. If not supplied, the first non-empty column will be used. fromExcel.param.lastColumn=The last column (zero-indexed) in the Excel document to retrieve data from. If not supplied, the last non-empty column will be used. +fromExcel.param.stringAll=List of columns to String. fromExcel.returns=A Dataset created from the Excel document. Types are assumed based on the first row of input data. equals.desc=Compares two datasets for structural equality. @@ -45,4 +46,4 @@ columnsEqual.returns=True if the two datasets have the same columns, per additio builder.desc=Creates a new dataset using supplied column names and types. builder.param.**columns=Optional. Keyword arguments can be supplied to predefine column names and types. The value of the argument should be string "typecode" (see system.dataset.fromCSV) or a Java or Python class instance. builder.returns=A DatasetBuilder object. Use addRow(value, ...) to add new values, and build() to construct the final dataset. \ - If keyword arguments were not supplied, column names and types can be manually declared using colNames() and colTypes(). + If keyword arguments were not supplied, column names and types can be manually declared using colNames() and colTypes(). \ No newline at end of file diff --git a/designer/bin/main/org/imdc/extensions/designer/DesignerHook.kt b/designer/bin/main/org/imdc/extensions/designer/DesignerHook.kt new file mode 100644 index 0000000..4bdc050 --- /dev/null +++ b/designer/bin/main/org/imdc/extensions/designer/DesignerHook.kt @@ -0,0 +1,55 @@ +package org.imdc.extensions.designer + +import com.inductiveautomation.ignition.common.BundleUtil +import com.inductiveautomation.ignition.common.expressions.ExpressionFunctionManager +import com.inductiveautomation.ignition.common.licensing.LicenseState +import com.inductiveautomation.ignition.common.script.ScriptManager +import com.inductiveautomation.ignition.designer.model.AbstractDesignerModuleHook +import com.inductiveautomation.ignition.designer.model.DesignerContext +import org.imdc.extensions.common.DatasetExtensions +import org.imdc.extensions.common.ExtensionDocProvider +import org.imdc.extensions.common.PyDatasetBuilder +import org.imdc.extensions.common.UtilitiesExtensions +import org.imdc.extensions.common.addPropertyBundle +import org.imdc.extensions.common.expressions.IsAvailableFunction +import org.imdc.extensions.common.expressions.LogicalPredicate.Companion.registerLogicFunctions + +@Suppress("unused") +class DesignerHook : AbstractDesignerModuleHook() { + private lateinit var context: DesignerContext + + override fun startup(context: DesignerContext, activationState: LicenseState) { + this.context = context + + BundleUtil.get().apply { + addPropertyBundle() + addPropertyBundle() + addPropertyBundle() + } + + PyDatasetBuilder.register() + } + + override fun shutdown() { + PyDatasetBuilder.unregister() + } + + override fun initializeScriptManager(manager: ScriptManager) { + manager.apply { + addScriptModule("system.dataset", DatasetExtensions, ExtensionDocProvider) + addScriptModule("system.util", UtilitiesExtensions(context), ExtensionDocProvider) + addScriptModule("system.project", DesignerProjectExtensions(context), ExtensionDocProvider) + } + } + + override fun configureFunctionFactory(factory: ExpressionFunctionManager) { + factory.apply { + addFunction( + IsAvailableFunction.NAME, + IsAvailableFunction.CATEGORY, + IsAvailableFunction(), + ) + registerLogicFunctions() + } + } +} diff --git a/designer/bin/main/org/imdc/extensions/designer/DesignerProjectExtensions.kt b/designer/bin/main/org/imdc/extensions/designer/DesignerProjectExtensions.kt new file mode 100644 index 0000000..df22708 --- /dev/null +++ b/designer/bin/main/org/imdc/extensions/designer/DesignerProjectExtensions.kt @@ -0,0 +1,38 @@ +package org.imdc.extensions.designer + +import com.inductiveautomation.ignition.common.script.hints.ScriptFunction +import com.inductiveautomation.ignition.designer.IgnitionDesigner +import com.inductiveautomation.ignition.designer.model.DesignerContext +import com.inductiveautomation.ignition.designer.project.DesignableProject +import org.apache.commons.lang3.reflect.MethodUtils +import org.imdc.extensions.common.ProjectExtensions +import org.imdc.extensions.common.UnsafeExtension + +class DesignerProjectExtensions(private val context: DesignerContext) : ProjectExtensions { + @ScriptFunction(docBundlePrefix = "DesignerProjectExtensions") + @UnsafeExtension + override fun getProject(): DesignableProject { + return requireNotNull(context.project) + } + + @ScriptFunction(docBundlePrefix = "DesignerProjectExtensions") + @UnsafeExtension + fun save() { + MethodUtils.invokeMethod( + context.frame, + true, // forceAccess + "handleSave", + false, // saveAs + null, // newName + false, // commitOnly + false, // skipReopen + false, // showDialog + ) + } + + @ScriptFunction(docBundlePrefix = "DesignerProjectExtensions") + @UnsafeExtension + fun update() { + (context.frame as IgnitionDesigner).updateProject() + } +} diff --git a/designer/bin/main/org/imdc/extensions/designer/DesignerProjectExtensions.properties b/designer/bin/main/org/imdc/extensions/designer/DesignerProjectExtensions.properties new file mode 100644 index 0000000..c5ac1cc --- /dev/null +++ b/designer/bin/main/org/imdc/extensions/designer/DesignerProjectExtensions.properties @@ -0,0 +1,6 @@ +getProject.desc=Retrieves the current project. +getProject.returns=The current project as a DesignableProject instance. +update.desc=Pulls in external changes made to this project from the gateway. +update.returns=None +save.desc=Programmatically invokes the save action. +save.returns=None diff --git a/gateway/bin/main/org/imdc/extensions/gateway/GatewayHook.kt b/gateway/bin/main/org/imdc/extensions/gateway/GatewayHook.kt new file mode 100644 index 0000000..36cffab --- /dev/null +++ b/gateway/bin/main/org/imdc/extensions/gateway/GatewayHook.kt @@ -0,0 +1,65 @@ +package org.imdc.extensions.gateway + +import com.inductiveautomation.ignition.common.BundleUtil +import com.inductiveautomation.ignition.common.expressions.ExpressionFunctionManager +import com.inductiveautomation.ignition.common.licensing.LicenseState +import com.inductiveautomation.ignition.common.script.ScriptManager +import com.inductiveautomation.ignition.gateway.model.AbstractGatewayModuleHook +import com.inductiveautomation.ignition.gateway.model.GatewayContext +import org.imdc.extensions.common.DatasetExtensions +import org.imdc.extensions.common.ExtensionDocProvider +import org.imdc.extensions.common.PyDatasetBuilder +import org.imdc.extensions.common.UtilitiesExtensions +import org.imdc.extensions.common.addPropertyBundle +import org.imdc.extensions.common.expressions.IsAvailableFunction +import org.imdc.extensions.common.expressions.LogicalPredicate.Companion.registerLogicFunctions + +@Suppress("unused") +class GatewayHook : AbstractGatewayModuleHook() { + private lateinit var context: GatewayContext + + override fun setup(context: GatewayContext) { + this.context = context + + BundleUtil.get().apply { + addPropertyBundle() + addPropertyBundle() + addPropertyBundle() + addPropertyBundle() + } + + PyDatasetBuilder.register() + + context.webResourceManager.addServlet(HistoryServlet.PATH, HistoryServlet::class.java) + } + + override fun startup(activationState: LicenseState) = Unit + + override fun shutdown() { + PyDatasetBuilder.unregister() + context.webResourceManager.removeServlet(HistoryServlet.PATH) + } + + override fun initializeScriptManager(manager: ScriptManager) { + manager.apply { + addScriptModule("system.dataset", DatasetExtensions, ExtensionDocProvider) + addScriptModule("system.util", UtilitiesExtensions(context), ExtensionDocProvider) + addScriptModule("system.project", GatewayProjectExtensions(context), ExtensionDocProvider) + addScriptModule("system.tag", GatewayTagExtensions(context), ExtensionDocProvider) + } + } + + override fun configureFunctionFactory(factory: ExpressionFunctionManager) { + factory.apply { + addFunction( + IsAvailableFunction.NAME, + IsAvailableFunction.CATEGORY, + IsAvailableFunction(), + ) + registerLogicFunctions() + } + } + + override fun isFreeModule() = true + override fun isMakerEditionCompatible() = true +} diff --git a/gateway/bin/main/org/imdc/extensions/gateway/GatewayProjectExtensions.kt b/gateway/bin/main/org/imdc/extensions/gateway/GatewayProjectExtensions.kt new file mode 100644 index 0000000..69950a2 --- /dev/null +++ b/gateway/bin/main/org/imdc/extensions/gateway/GatewayProjectExtensions.kt @@ -0,0 +1,24 @@ +package org.imdc.extensions.gateway + +import com.inductiveautomation.ignition.common.project.RuntimeProject +import com.inductiveautomation.ignition.common.script.ScriptContext +import com.inductiveautomation.ignition.common.script.hints.ScriptArg +import com.inductiveautomation.ignition.common.script.hints.ScriptFunction +import com.inductiveautomation.ignition.gateway.model.GatewayContext +import org.imdc.extensions.common.ProjectExtensions +import org.python.core.Py + +class GatewayProjectExtensions(private val context: GatewayContext) : ProjectExtensions { + @ScriptFunction(docBundlePrefix = "GatewayProjectExtensions") + override fun getProject(): RuntimeProject { + val defaultProject = ScriptContext.defaultProject() ?: throw Py.EnvironmentError("No context project populated") + return requireNotNull(getProject(defaultProject)) { "No such project $defaultProject" } + } + + @ScriptFunction(docBundlePrefix = "GatewayProjectExtensions") + fun getProject( + @ScriptArg("project", optional = true) project: String, + ): RuntimeProject? { + return context.projectManager.getProject(project).orElse(null) + } +} diff --git a/gateway/bin/main/org/imdc/extensions/gateway/GatewayProjectExtensions.properties b/gateway/bin/main/org/imdc/extensions/gateway/GatewayProjectExtensions.properties new file mode 100644 index 0000000..4be8f31 --- /dev/null +++ b/gateway/bin/main/org/imdc/extensions/gateway/GatewayProjectExtensions.properties @@ -0,0 +1,3 @@ +getProject.desc=Retrieves a project by name, or None. +getProject.param.project=The name of the project to retrieve. Defaults to the current project, if known. +getProject.returns=The specified project as a RuntimeProject instance. diff --git a/gateway/bin/main/org/imdc/extensions/gateway/GatewayTagExtensions.kt b/gateway/bin/main/org/imdc/extensions/gateway/GatewayTagExtensions.kt new file mode 100644 index 0000000..d8b3509 --- /dev/null +++ b/gateway/bin/main/org/imdc/extensions/gateway/GatewayTagExtensions.kt @@ -0,0 +1,18 @@ +package org.imdc.extensions.gateway + +import com.inductiveautomation.ignition.common.tags.config.TagConfigurationModel +import com.inductiveautomation.ignition.common.tags.model.TagPath +import com.inductiveautomation.ignition.gateway.model.GatewayContext +import org.imdc.extensions.common.TagExtensions +import org.python.core.Py + +class GatewayTagExtensions(private val context: GatewayContext) : TagExtensions() { + override fun getConfigurationImpl(basePath: TagPath, recursive: Boolean): List { + val provider = ( + context.tagManager.getTagProvider(basePath.source) + ?: throw Py.ValueError("No such tag provider ${basePath.source}") + ) + + return provider.getTagConfigsAsync(listOf(basePath), recursive, true).get() + } +} diff --git a/gateway/bin/main/org/imdc/extensions/gateway/HistoryServlet.kt b/gateway/bin/main/org/imdc/extensions/gateway/HistoryServlet.kt new file mode 100644 index 0000000..725639f --- /dev/null +++ b/gateway/bin/main/org/imdc/extensions/gateway/HistoryServlet.kt @@ -0,0 +1,162 @@ +package org.imdc.extensions.gateway + +import com.inductiveautomation.ignition.common.QualifiedPathUtils +import com.inductiveautomation.ignition.common.StreamingDatasetWriter +import com.inductiveautomation.ignition.common.gson.stream.JsonWriter +import com.inductiveautomation.ignition.common.model.values.QualityCode +import com.inductiveautomation.ignition.common.sqltags.history.AggregationMode +import com.inductiveautomation.ignition.common.sqltags.history.BasicTagHistoryQueryParams +import com.inductiveautomation.ignition.common.sqltags.history.ReturnFormat +import com.inductiveautomation.ignition.common.util.LoggerEx +import com.inductiveautomation.ignition.gateway.model.GatewayContext +import org.apache.http.entity.ContentType +import java.io.PrintWriter +import java.text.SimpleDateFormat +import java.time.Instant +import java.time.format.DateTimeFormatter +import java.time.temporal.ChronoUnit +import java.util.Date +import javax.servlet.http.HttpServlet +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +class HistoryServlet : HttpServlet() { + private lateinit var context: GatewayContext + + override fun init() { + context = servletContext.getAttribute(GatewayContext.SERVLET_CONTEXT_KEY) as GatewayContext + } + + override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { + resp.contentType = ContentType.APPLICATION_JSON.toString() + resp.writer.use { writer -> + val historyQuery: BasicTagHistoryQueryParams = + try { + val paths = + req.getParameterValues("path") + ?: throw IllegalArgumentException("Must specify at least one path") + val startDate = + req.getParameter("startDate")?.toDate() ?: Date.from(Instant.now().minus(8, ChronoUnit.HOURS)) + val endDate = req.getParameter("endDate")?.toDate() ?: Date() + val returnSize = req.getParameter("returnSize")?.toInt() ?: -1 + val aggregationMode = + req.getParameter("aggregationMode")?.let(AggregationMode::valueOf) ?: AggregationMode.Average + val aliases = req.getParameter("aliases")?.split(',') + + BasicTagHistoryQueryParams( + paths.map(QualifiedPathUtils::toPathFromHistoricalString), + startDate, + endDate, + returnSize, + aggregationMode, + ReturnFormat.Wide, + aliases, + emptyList(), + ) + } catch (e: Exception) { + resp.status = HttpServletResponse.SC_BAD_REQUEST + e.printStackTrace(PrintWriter(writer)) + return + } + + try { + context.tagHistoryManager.queryHistory( + historyQuery, + StreamingJsonWriter( + JsonWriter(writer), + ), + ) + } catch (e: TrialExpiredException) { + resp.status = HttpServletResponse.SC_PAYMENT_REQUIRED + logger.error("Tag historian module reported trial expired", e) + } catch (e: Exception) { + resp.status = HttpServletResponse.SC_INTERNAL_SERVER_ERROR + logger.error("Unexpected exception writing JSON content to servlet", e) + } + } + } + + class TrialExpiredException : Exception() + + class StreamingJsonWriter(private val jsonWriter: JsonWriter) : StreamingDatasetWriter { + private lateinit var names: Array + private lateinit var types: Array> + + private val test = DateTimeFormatter.ISO_INSTANT + + override fun initialize( + columnNames: Array, + columnTypes: Array>, + hasQuality: Boolean, + expectedRows: Int, + ) { + this.names = columnNames + this.types = columnTypes + + jsonWriter.beginArray() + } + + override fun write(data: Array, quality: Array): Unit = jsonWriter.run { + if (quality.any { it.`is`(QualityCode.Bad_TrialExpired) }) { + throw TrialExpiredException() + } + + writeObject { + for (index in data.indices) { + name(names[index]) + when (val value = data[index]) { + is Number -> { + when (types[index]) { + Float::class.java, Double::class.java -> value(value.toDouble()) + else -> value(value.toLong()) + } + } + + is Date -> { + value(test.format(value.toInstant())) + } + + is String -> value(value) + is Boolean -> value(value) + + null -> nullValue() + } + } + } + } + + override fun finish() { + jsonWriter.endArray() + } + + override fun finishWithError(exception: java.lang.Exception) = throw exception + + private inline fun JsonWriter.writeObject(block: JsonWriter.() -> Unit) { + beginObject() + block() + endObject() + } + } + + companion object { + const val PATH = "history-extension" + + private val logger = LoggerEx.newBuilder().build(HistoryServlet::class.java) + + private val parsingStrategies = listOf<(String) -> Date>( + SimpleDateFormat.getDateTimeInstance()::parse, + SimpleDateFormat.getInstance()::parse, + { Date(it.toLong()) }, + ) + + private fun String.toDate(): Date? { + return parsingStrategies.firstNotNullOfOrNull { strategy -> + try { + strategy.invoke(this) + } catch (e: Exception) { + null + } + } + } + } +} From 8d5bcf2ed0be2cde197aec9bd3eeabe4414b20b2 Mon Sep 17 00:00:00 2001 From: JosephTLockwood Date: Mon, 26 Jun 2023 14:51:36 -0400 Subject: [PATCH 02/14] Update String Column Options --- .../extensions/common/DatasetExtensions.kt | 34 ++++++++++--------- .../common/DatasetExtensions.properties | 2 +- .../extensions/common/DatasetExtensions.kt | 34 ++++++++++--------- .../common/DatasetExtensions.properties | 2 +- 4 files changed, 38 insertions(+), 34 deletions(-) diff --git a/common/bin/main/org/imdc/extensions/common/DatasetExtensions.kt b/common/bin/main/org/imdc/extensions/common/DatasetExtensions.kt index c9a8e14..a7dfd6b 100644 --- a/common/bin/main/org/imdc/extensions/common/DatasetExtensions.kt +++ b/common/bin/main/org/imdc/extensions/common/DatasetExtensions.kt @@ -220,8 +220,8 @@ object DatasetExtensions { @Suppress("unused") @ScriptFunction(docBundlePrefix = "DatasetExtensions") @KeywordArgs( - names = ["input", "headerRow", "sheetNumber", "firstRow", "lastRow", "firstColumn", "lastColumn", "stringAll"], - types = [ByteArray::class, Integer::class, Integer::class, Integer::class, Integer::class, Integer::class, Integer::class, Array::class], + names = ["input", "headerRow", "sheetNumber", "firstRow", "lastRow", "firstColumn", "lastColumn", "stringColumns"], + types = [ByteArray::class, Integer::class, Integer::class, Integer::class, Integer::class, Integer::class, Integer::class, Array::class], ) fun fromExcel(args: Array, keywords: Array): Dataset { val parsedArgs = PyArgParser.parseArgs( @@ -235,14 +235,15 @@ object DatasetExtensions { "lastRow", "firstColumn", "lastColumn", - "stringAll" + "stringColumns" ), Array(8) { Any::class.java }, "fromExcel", ) - val tempStringAll = parsedArgs.getInteger("stringAll").orElse(0) - val stringAll = tempStringAll != 0 + //val tempstringColumn = parsedArgs.getInteger("stringColumn").orElse(0) + val stringColumns = parsedArgs.getPyObject("stringColumns").map { it.toJava>() }.orElse(arrayOf()) + //val stringColumn = tempstringColumn != 0 when (val input = parsedArgs.requirePyObject("input").toJava()) { is String -> WorkbookFactory.create(File(input)) is ByteArray -> WorkbookFactory.create(input.inputStream().buffered()) @@ -297,16 +298,17 @@ object DatasetExtensions { val rowValues = Array(columnCount) { j -> val cell = row.getCell(j + firstColumn) + val stringColumn = j in stringColumns if(cell == null){ if (!typesSet) { - if (stringAll) { + if (stringColumn) { columnTypes.add(String::class.java) } else{ columnTypes.add(Any::class.java) } } - if (stringAll) { + if (stringColumn) { null.toString() } else{ @@ -319,7 +321,7 @@ object DatasetExtensions { if(cellType == "NUMERIC") { if (DateUtil.isCellDateFormatted(cell)) { if (!typesSet) { - if(stringAll){ + if(stringColumn){ columnTypes.add(String::class.java) } else{ @@ -328,7 +330,7 @@ object DatasetExtensions { } val tempCell = cell.dateCellValue - if(stringAll){ + if(stringColumn){ tempCell.toString() } else{ @@ -338,14 +340,14 @@ object DatasetExtensions { val numericCellValue = cell.numericCellValue if (BigDecimal(numericCellValue).scale() == 0) { if (!typesSet) { - if(stringAll){ + if(stringColumn){ columnTypes.add(String::class.java) } else{ columnTypes.add(Int::class.javaObjectType) } } - if(stringAll){ + if(stringColumn){ numericCellValue.toInt().toString() } else{ @@ -353,14 +355,14 @@ object DatasetExtensions { } } else { if (!typesSet) { - if(stringAll){ + if(stringColumn){ columnTypes.add(String::class.java) } else{ columnTypes.add(Double::class.javaObjectType) } } - if(stringAll){ + if(stringColumn){ numericCellValue.toString() } else{ @@ -381,7 +383,7 @@ object DatasetExtensions { if (!typesSet) { columnTypes.add(Boolean::class.javaObjectType) } - if(stringAll){ + if(stringColumn){ cell.booleanCellValue.toString() } else{ @@ -391,14 +393,14 @@ object DatasetExtensions { else{ if (!typesSet) { - if (stringAll) { + if (stringColumn) { columnTypes.add(String::class.java) } else{ columnTypes.add(Any::class.java) } } - if (stringAll) { + if (stringColumn) { null.toString() } else{ diff --git a/common/bin/main/org/imdc/extensions/common/DatasetExtensions.properties b/common/bin/main/org/imdc/extensions/common/DatasetExtensions.properties index 7b07c62..b6a6185 100644 --- a/common/bin/main/org/imdc/extensions/common/DatasetExtensions.properties +++ b/common/bin/main/org/imdc/extensions/common/DatasetExtensions.properties @@ -23,7 +23,7 @@ fromExcel.param.firstRow=The first row (zero-indexed) in the Excel document to r fromExcel.param.lastRow=The last row (zero-indexed) in the Excel document to retrieve data from. If not supplied, the last non-empty row will be used. fromExcel.param.firstColumn=The first column (zero-indexed) in the Excel document to retrieve data from. If not supplied, the first non-empty column will be used. fromExcel.param.lastColumn=The last column (zero-indexed) in the Excel document to retrieve data from. If not supplied, the last non-empty column will be used. -fromExcel.param.stringAll=List of columns to String. +fromExcel.param.stringColumns=List of columns to String. fromExcel.returns=A Dataset created from the Excel document. Types are assumed based on the first row of input data. equals.desc=Compares two datasets for structural equality. diff --git a/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt b/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt index c9a8e14..a7dfd6b 100644 --- a/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt +++ b/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt @@ -220,8 +220,8 @@ object DatasetExtensions { @Suppress("unused") @ScriptFunction(docBundlePrefix = "DatasetExtensions") @KeywordArgs( - names = ["input", "headerRow", "sheetNumber", "firstRow", "lastRow", "firstColumn", "lastColumn", "stringAll"], - types = [ByteArray::class, Integer::class, Integer::class, Integer::class, Integer::class, Integer::class, Integer::class, Array::class], + names = ["input", "headerRow", "sheetNumber", "firstRow", "lastRow", "firstColumn", "lastColumn", "stringColumns"], + types = [ByteArray::class, Integer::class, Integer::class, Integer::class, Integer::class, Integer::class, Integer::class, Array::class], ) fun fromExcel(args: Array, keywords: Array): Dataset { val parsedArgs = PyArgParser.parseArgs( @@ -235,14 +235,15 @@ object DatasetExtensions { "lastRow", "firstColumn", "lastColumn", - "stringAll" + "stringColumns" ), Array(8) { Any::class.java }, "fromExcel", ) - val tempStringAll = parsedArgs.getInteger("stringAll").orElse(0) - val stringAll = tempStringAll != 0 + //val tempstringColumn = parsedArgs.getInteger("stringColumn").orElse(0) + val stringColumns = parsedArgs.getPyObject("stringColumns").map { it.toJava>() }.orElse(arrayOf()) + //val stringColumn = tempstringColumn != 0 when (val input = parsedArgs.requirePyObject("input").toJava()) { is String -> WorkbookFactory.create(File(input)) is ByteArray -> WorkbookFactory.create(input.inputStream().buffered()) @@ -297,16 +298,17 @@ object DatasetExtensions { val rowValues = Array(columnCount) { j -> val cell = row.getCell(j + firstColumn) + val stringColumn = j in stringColumns if(cell == null){ if (!typesSet) { - if (stringAll) { + if (stringColumn) { columnTypes.add(String::class.java) } else{ columnTypes.add(Any::class.java) } } - if (stringAll) { + if (stringColumn) { null.toString() } else{ @@ -319,7 +321,7 @@ object DatasetExtensions { if(cellType == "NUMERIC") { if (DateUtil.isCellDateFormatted(cell)) { if (!typesSet) { - if(stringAll){ + if(stringColumn){ columnTypes.add(String::class.java) } else{ @@ -328,7 +330,7 @@ object DatasetExtensions { } val tempCell = cell.dateCellValue - if(stringAll){ + if(stringColumn){ tempCell.toString() } else{ @@ -338,14 +340,14 @@ object DatasetExtensions { val numericCellValue = cell.numericCellValue if (BigDecimal(numericCellValue).scale() == 0) { if (!typesSet) { - if(stringAll){ + if(stringColumn){ columnTypes.add(String::class.java) } else{ columnTypes.add(Int::class.javaObjectType) } } - if(stringAll){ + if(stringColumn){ numericCellValue.toInt().toString() } else{ @@ -353,14 +355,14 @@ object DatasetExtensions { } } else { if (!typesSet) { - if(stringAll){ + if(stringColumn){ columnTypes.add(String::class.java) } else{ columnTypes.add(Double::class.javaObjectType) } } - if(stringAll){ + if(stringColumn){ numericCellValue.toString() } else{ @@ -381,7 +383,7 @@ object DatasetExtensions { if (!typesSet) { columnTypes.add(Boolean::class.javaObjectType) } - if(stringAll){ + if(stringColumn){ cell.booleanCellValue.toString() } else{ @@ -391,14 +393,14 @@ object DatasetExtensions { else{ if (!typesSet) { - if (stringAll) { + if (stringColumn) { columnTypes.add(String::class.java) } else{ columnTypes.add(Any::class.java) } } - if (stringAll) { + if (stringColumn) { null.toString() } else{ diff --git a/common/src/main/resources/org/imdc/extensions/common/DatasetExtensions.properties b/common/src/main/resources/org/imdc/extensions/common/DatasetExtensions.properties index 7b07c62..b6a6185 100644 --- a/common/src/main/resources/org/imdc/extensions/common/DatasetExtensions.properties +++ b/common/src/main/resources/org/imdc/extensions/common/DatasetExtensions.properties @@ -23,7 +23,7 @@ fromExcel.param.firstRow=The first row (zero-indexed) in the Excel document to r fromExcel.param.lastRow=The last row (zero-indexed) in the Excel document to retrieve data from. If not supplied, the last non-empty row will be used. fromExcel.param.firstColumn=The first column (zero-indexed) in the Excel document to retrieve data from. If not supplied, the first non-empty column will be used. fromExcel.param.lastColumn=The last column (zero-indexed) in the Excel document to retrieve data from. If not supplied, the last non-empty column will be used. -fromExcel.param.stringAll=List of columns to String. +fromExcel.param.stringColumns=List of columns to String. fromExcel.returns=A Dataset created from the Excel document. Types are assumed based on the first row of input data. equals.desc=Compares two datasets for structural equality. From c56670b5ef3ad538a0cae5dba90038d893166792 Mon Sep 17 00:00:00 2001 From: JosephTLockwood Date: Tue, 27 Jun 2023 12:55:39 -0400 Subject: [PATCH 03/14] Add to Dict --- .../extensions/common/DatasetExtensions.kt | 49 ++++++++++++++++--- .../common/DatasetExtensions.properties | 5 ++ .../extensions/common/DatasetExtensions.kt | 49 ++++++++++++++++--- .../common/DatasetExtensions.properties | 5 ++ 4 files changed, 94 insertions(+), 14 deletions(-) diff --git a/common/bin/main/org/imdc/extensions/common/DatasetExtensions.kt b/common/bin/main/org/imdc/extensions/common/DatasetExtensions.kt index a7dfd6b..f5e576b 100644 --- a/common/bin/main/org/imdc/extensions/common/DatasetExtensions.kt +++ b/common/bin/main/org/imdc/extensions/common/DatasetExtensions.kt @@ -17,6 +17,7 @@ import org.apache.poi.ss.usermodel.WorkbookFactory import org.python.core.Py import org.python.core.PyBaseString import org.python.core.PyBoolean +import org.python.core.PyDictionary import org.python.core.PyFloat import org.python.core.PyFunction import org.python.core.PyInteger @@ -217,11 +218,50 @@ object DatasetExtensions { } } + @Suppress("unused") + @ScriptFunction(docBundlePrefix = "DatasetExtensions") + @KeywordArgs( + names = ["dataset", "filterNull"], + types = [Dataset::class, Boolean::class], + ) + fun toDict(args: Array, keywords: Array): PyDictionary { + val parsedArgs = PyArgParser.parseArgs( + args, + keywords, + arrayOf( + "dataset", + "filterNull", + ), + Array(2) { Any::class.java }, + "toDict", + ) + val dataset = parsedArgs.requirePyObject("dataset").toJava() + val filterNull = parsedArgs.getBoolean("filterNull").orElse(false) + val pyDict = PyDictionary() + for (column in 0 until dataset.columnCount) { + val rowArray = if (filterNull) { + (0 until dataset.rowCount) + .mapNotNull { row -> + dataset.getValueAt(row, column) + } + .toTypedArray() + } else { + (0 until dataset.rowCount) + .map { row -> + dataset.getValueAt(row, column) + } + .toTypedArray() + } + pyDict[dataset.columnNames[column]] = rowArray + } + return pyDict + } + @Suppress("unused") @ScriptFunction(docBundlePrefix = "DatasetExtensions") @KeywordArgs( names = ["input", "headerRow", "sheetNumber", "firstRow", "lastRow", "firstColumn", "lastColumn", "stringColumns"], - types = [ByteArray::class, Integer::class, Integer::class, Integer::class, Integer::class, Integer::class, Integer::class, Array::class], + types = [ByteArray::class, Int::class, Int::class, Int::class, Int::class, Int::class, Int::class, Array::class], ) fun fromExcel(args: Array, keywords: Array): Dataset { val parsedArgs = PyArgParser.parseArgs( @@ -308,12 +348,7 @@ object DatasetExtensions { columnTypes.add(Any::class.java) } } - if (stringColumn) { - null.toString() - } - else{ - null - } + null } else{ val cellType = cell.getCellType().toString() diff --git a/common/bin/main/org/imdc/extensions/common/DatasetExtensions.properties b/common/bin/main/org/imdc/extensions/common/DatasetExtensions.properties index b6a6185..2c0518a 100644 --- a/common/bin/main/org/imdc/extensions/common/DatasetExtensions.properties +++ b/common/bin/main/org/imdc/extensions/common/DatasetExtensions.properties @@ -15,6 +15,11 @@ print.param.output=The output destination. Defaults to sys.stdout. print.param.includeTypes=If True, includes the type of the column in the first row. Defaults to False. print.returns=None. +toDict.desc=Converts a dataset to dict +toDict.param.dataset=The dataset to convert. Must not be null +toDict.param.filterNull=Gives the option to filter Null/None +toDict.returns=dict of dataset + fromExcel.desc=Creates a dataset by reading select cells from an Excel spreadsheet. fromExcel.param.input=The Excel document to read - either the path to a file on disk, or a byte array with the contents of a file. fromExcel.param.headerRow=The row number to use for the column names in the output dataset. diff --git a/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt b/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt index a7dfd6b..f5e576b 100644 --- a/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt +++ b/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt @@ -17,6 +17,7 @@ import org.apache.poi.ss.usermodel.WorkbookFactory import org.python.core.Py import org.python.core.PyBaseString import org.python.core.PyBoolean +import org.python.core.PyDictionary import org.python.core.PyFloat import org.python.core.PyFunction import org.python.core.PyInteger @@ -217,11 +218,50 @@ object DatasetExtensions { } } + @Suppress("unused") + @ScriptFunction(docBundlePrefix = "DatasetExtensions") + @KeywordArgs( + names = ["dataset", "filterNull"], + types = [Dataset::class, Boolean::class], + ) + fun toDict(args: Array, keywords: Array): PyDictionary { + val parsedArgs = PyArgParser.parseArgs( + args, + keywords, + arrayOf( + "dataset", + "filterNull", + ), + Array(2) { Any::class.java }, + "toDict", + ) + val dataset = parsedArgs.requirePyObject("dataset").toJava() + val filterNull = parsedArgs.getBoolean("filterNull").orElse(false) + val pyDict = PyDictionary() + for (column in 0 until dataset.columnCount) { + val rowArray = if (filterNull) { + (0 until dataset.rowCount) + .mapNotNull { row -> + dataset.getValueAt(row, column) + } + .toTypedArray() + } else { + (0 until dataset.rowCount) + .map { row -> + dataset.getValueAt(row, column) + } + .toTypedArray() + } + pyDict[dataset.columnNames[column]] = rowArray + } + return pyDict + } + @Suppress("unused") @ScriptFunction(docBundlePrefix = "DatasetExtensions") @KeywordArgs( names = ["input", "headerRow", "sheetNumber", "firstRow", "lastRow", "firstColumn", "lastColumn", "stringColumns"], - types = [ByteArray::class, Integer::class, Integer::class, Integer::class, Integer::class, Integer::class, Integer::class, Array::class], + types = [ByteArray::class, Int::class, Int::class, Int::class, Int::class, Int::class, Int::class, Array::class], ) fun fromExcel(args: Array, keywords: Array): Dataset { val parsedArgs = PyArgParser.parseArgs( @@ -308,12 +348,7 @@ object DatasetExtensions { columnTypes.add(Any::class.java) } } - if (stringColumn) { - null.toString() - } - else{ - null - } + null } else{ val cellType = cell.getCellType().toString() diff --git a/common/src/main/resources/org/imdc/extensions/common/DatasetExtensions.properties b/common/src/main/resources/org/imdc/extensions/common/DatasetExtensions.properties index b6a6185..2c0518a 100644 --- a/common/src/main/resources/org/imdc/extensions/common/DatasetExtensions.properties +++ b/common/src/main/resources/org/imdc/extensions/common/DatasetExtensions.properties @@ -15,6 +15,11 @@ print.param.output=The output destination. Defaults to sys.stdout. print.param.includeTypes=If True, includes the type of the column in the first row. Defaults to False. print.returns=None. +toDict.desc=Converts a dataset to dict +toDict.param.dataset=The dataset to convert. Must not be null +toDict.param.filterNull=Gives the option to filter Null/None +toDict.returns=dict of dataset + fromExcel.desc=Creates a dataset by reading select cells from an Excel spreadsheet. fromExcel.param.input=The Excel document to read - either the path to a file on disk, or a byte array with the contents of a file. fromExcel.param.headerRow=The row number to use for the column names in the output dataset. From da6dc21eed9babcef5f9df56c1c8b7687e45c479 Mon Sep 17 00:00:00 2001 From: JosephTLockwood Date: Tue, 27 Jun 2023 16:06:19 -0400 Subject: [PATCH 04/14] Change to PyList --- .../bin/main/org/imdc/extensions/common/DatasetExtensions.kt | 4 ++-- .../kotlin/org/imdc/extensions/common/DatasetExtensions.kt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/common/bin/main/org/imdc/extensions/common/DatasetExtensions.kt b/common/bin/main/org/imdc/extensions/common/DatasetExtensions.kt index f5e576b..8684099 100644 --- a/common/bin/main/org/imdc/extensions/common/DatasetExtensions.kt +++ b/common/bin/main/org/imdc/extensions/common/DatasetExtensions.kt @@ -244,13 +244,13 @@ object DatasetExtensions { .mapNotNull { row -> dataset.getValueAt(row, column) } - .toTypedArray() + .toList() } else { (0 until dataset.rowCount) .map { row -> dataset.getValueAt(row, column) } - .toTypedArray() + .toList() } pyDict[dataset.columnNames[column]] = rowArray } diff --git a/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt b/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt index f5e576b..8684099 100644 --- a/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt +++ b/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt @@ -244,13 +244,13 @@ object DatasetExtensions { .mapNotNull { row -> dataset.getValueAt(row, column) } - .toTypedArray() + .toList() } else { (0 until dataset.rowCount) .map { row -> dataset.getValueAt(row, column) } - .toTypedArray() + .toList() } pyDict[dataset.columnNames[column]] = rowArray } From 27eacc303de8ef6e1233bbd230f17c43103acc22 Mon Sep 17 00:00:00 2001 From: Paul Griffith Date: Tue, 27 Jun 2023 14:49:36 -0700 Subject: [PATCH 05/14] Remove built artifacts --- .../org/imdc/extensions/client/ClientHook.kt | 55 -- .../client/ClientProjectExtensions.kt | 13 - .../client/ClientProjectExtensions.properties | 2 - .../org/imdc/extensions/common/Constants.kt | 3 - .../extensions/common/DatasetExtensions.kt | 552 ----------------- .../common/DatasetExtensions.properties | 54 -- .../extensions/common/ExtensionDocProvider.kt | 34 -- .../extensions/common/ProjectExtensions.kt | 7 - .../extensions/common/PyDatasetBuilder.kt | 58 -- .../imdc/extensions/common/TagExtensions.kt | 63 -- .../common/TagExtensions.properties | 4 - .../org/imdc/extensions/common/Utilities.kt | 55 -- ...litiesExtensions$KeywordParseContext.class | Bin 3814 -> 0 bytes .../common/UtilitiesExtensions.class | Bin 7168 -> 0 bytes .../common/UtilitiesExtensions.properties | 10 - .../common/expressions/IsAvailableFunction.kt | 26 - .../common/expressions/LogicalPredicate.kt | 44 -- .../org/imdc/extensions/common/DSBuilder.kt | 38 -- .../extensions/common/DatasetEqualityTests.kt | 156 ----- .../common/DatasetExtensionsTests.kt | 556 ------------------ .../imdc/extensions/common/ExpressionUtils.kt | 40 -- .../org/imdc/extensions/common/JythonTest.kt | 93 --- .../common/LogicalFunctionsTests.kt | 49 -- .../common/UtilitiesExtensionsTests.kt | 50 -- .../org/imdc/extensions/common/sample.xls | Bin 35328 -> 0 bytes .../org/imdc/extensions/common/sample.xlsx | Bin 13410 -> 0 bytes .../imdc/extensions/designer/DesignerHook.kt | 55 -- .../designer/DesignerProjectExtensions.kt | 38 -- .../DesignerProjectExtensions.properties | 6 - .../imdc/extensions/gateway/GatewayHook.kt | 65 -- .../gateway/GatewayProjectExtensions.kt | 24 - .../GatewayProjectExtensions.properties | 3 - .../gateway/GatewayTagExtensions.kt | 18 - .../imdc/extensions/gateway/HistoryServlet.kt | 162 ----- 34 files changed, 2333 deletions(-) delete mode 100644 client/bin/main/org/imdc/extensions/client/ClientHook.kt delete mode 100644 client/bin/main/org/imdc/extensions/client/ClientProjectExtensions.kt delete mode 100644 client/bin/main/org/imdc/extensions/client/ClientProjectExtensions.properties delete mode 100644 common/bin/main/org/imdc/extensions/common/Constants.kt delete mode 100644 common/bin/main/org/imdc/extensions/common/DatasetExtensions.kt delete mode 100644 common/bin/main/org/imdc/extensions/common/DatasetExtensions.properties delete mode 100644 common/bin/main/org/imdc/extensions/common/ExtensionDocProvider.kt delete mode 100644 common/bin/main/org/imdc/extensions/common/ProjectExtensions.kt delete mode 100644 common/bin/main/org/imdc/extensions/common/PyDatasetBuilder.kt delete mode 100644 common/bin/main/org/imdc/extensions/common/TagExtensions.kt delete mode 100644 common/bin/main/org/imdc/extensions/common/TagExtensions.properties delete mode 100644 common/bin/main/org/imdc/extensions/common/Utilities.kt delete mode 100644 common/bin/main/org/imdc/extensions/common/UtilitiesExtensions$KeywordParseContext.class delete mode 100644 common/bin/main/org/imdc/extensions/common/UtilitiesExtensions.class delete mode 100644 common/bin/main/org/imdc/extensions/common/UtilitiesExtensions.properties delete mode 100644 common/bin/main/org/imdc/extensions/common/expressions/IsAvailableFunction.kt delete mode 100644 common/bin/main/org/imdc/extensions/common/expressions/LogicalPredicate.kt delete mode 100644 common/bin/test/org/imdc/extensions/common/DSBuilder.kt delete mode 100644 common/bin/test/org/imdc/extensions/common/DatasetEqualityTests.kt delete mode 100644 common/bin/test/org/imdc/extensions/common/DatasetExtensionsTests.kt delete mode 100644 common/bin/test/org/imdc/extensions/common/ExpressionUtils.kt delete mode 100644 common/bin/test/org/imdc/extensions/common/JythonTest.kt delete mode 100644 common/bin/test/org/imdc/extensions/common/LogicalFunctionsTests.kt delete mode 100644 common/bin/test/org/imdc/extensions/common/UtilitiesExtensionsTests.kt delete mode 100644 common/bin/test/org/imdc/extensions/common/sample.xls delete mode 100644 common/bin/test/org/imdc/extensions/common/sample.xlsx delete mode 100644 designer/bin/main/org/imdc/extensions/designer/DesignerHook.kt delete mode 100644 designer/bin/main/org/imdc/extensions/designer/DesignerProjectExtensions.kt delete mode 100644 designer/bin/main/org/imdc/extensions/designer/DesignerProjectExtensions.properties delete mode 100644 gateway/bin/main/org/imdc/extensions/gateway/GatewayHook.kt delete mode 100644 gateway/bin/main/org/imdc/extensions/gateway/GatewayProjectExtensions.kt delete mode 100644 gateway/bin/main/org/imdc/extensions/gateway/GatewayProjectExtensions.properties delete mode 100644 gateway/bin/main/org/imdc/extensions/gateway/GatewayTagExtensions.kt delete mode 100644 gateway/bin/main/org/imdc/extensions/gateway/HistoryServlet.kt diff --git a/client/bin/main/org/imdc/extensions/client/ClientHook.kt b/client/bin/main/org/imdc/extensions/client/ClientHook.kt deleted file mode 100644 index 9d888fd..0000000 --- a/client/bin/main/org/imdc/extensions/client/ClientHook.kt +++ /dev/null @@ -1,55 +0,0 @@ -package org.imdc.extensions.client - -import com.inductiveautomation.ignition.client.model.ClientContext -import com.inductiveautomation.ignition.common.BundleUtil -import com.inductiveautomation.ignition.common.expressions.ExpressionFunctionManager -import com.inductiveautomation.ignition.common.licensing.LicenseState -import com.inductiveautomation.ignition.common.script.ScriptManager -import com.inductiveautomation.vision.api.client.AbstractClientModuleHook -import org.imdc.extensions.common.DatasetExtensions -import org.imdc.extensions.common.ExtensionDocProvider -import org.imdc.extensions.common.PyDatasetBuilder -import org.imdc.extensions.common.UtilitiesExtensions -import org.imdc.extensions.common.addPropertyBundle -import org.imdc.extensions.common.expressions.IsAvailableFunction -import org.imdc.extensions.common.expressions.LogicalPredicate.Companion.registerLogicFunctions - -@Suppress("unused") -class ClientHook : AbstractClientModuleHook() { - private lateinit var context: ClientContext - - override fun startup(context: ClientContext, activationState: LicenseState) { - this.context = context - - BundleUtil.get().apply { - addPropertyBundle() - addPropertyBundle() - addPropertyBundle() - } - - PyDatasetBuilder.register() - } - - override fun shutdown() { - PyDatasetBuilder.unregister() - } - - override fun initializeScriptManager(manager: ScriptManager) { - manager.apply { - addScriptModule("system.dataset", DatasetExtensions, ExtensionDocProvider) - addScriptModule("system.util", UtilitiesExtensions(context), ExtensionDocProvider) - addScriptModule("system.project", ClientProjectExtensions(context), ExtensionDocProvider) - } - } - - override fun configureFunctionFactory(factory: ExpressionFunctionManager) { - factory.apply { - addFunction( - IsAvailableFunction.NAME, - IsAvailableFunction.CATEGORY, - IsAvailableFunction(), - ) - registerLogicFunctions() - } - } -} diff --git a/client/bin/main/org/imdc/extensions/client/ClientProjectExtensions.kt b/client/bin/main/org/imdc/extensions/client/ClientProjectExtensions.kt deleted file mode 100644 index 828b638..0000000 --- a/client/bin/main/org/imdc/extensions/client/ClientProjectExtensions.kt +++ /dev/null @@ -1,13 +0,0 @@ -package org.imdc.extensions.client - -import com.inductiveautomation.ignition.client.model.ClientContext -import com.inductiveautomation.ignition.common.project.Project -import com.inductiveautomation.ignition.common.script.hints.ScriptFunction -import org.imdc.extensions.common.ProjectExtensions - -class ClientProjectExtensions(private val context: ClientContext) : ProjectExtensions { - @ScriptFunction(docBundlePrefix = "ClientProjectExtensions") - override fun getProject(): Project { - return requireNotNull(context.project) - } -} diff --git a/client/bin/main/org/imdc/extensions/client/ClientProjectExtensions.properties b/client/bin/main/org/imdc/extensions/client/ClientProjectExtensions.properties deleted file mode 100644 index 776f5d2..0000000 --- a/client/bin/main/org/imdc/extensions/client/ClientProjectExtensions.properties +++ /dev/null @@ -1,2 +0,0 @@ -getProject.desc=Retrieves the current project. -getProject.returns=The current project as a RuntimeProject instance. diff --git a/common/bin/main/org/imdc/extensions/common/Constants.kt b/common/bin/main/org/imdc/extensions/common/Constants.kt deleted file mode 100644 index def2b9d..0000000 --- a/common/bin/main/org/imdc/extensions/common/Constants.kt +++ /dev/null @@ -1,3 +0,0 @@ -package org.imdc.extensions.common - -const val MODULE_ID = "io.github.paulgriffith.extensions.IgnitionExtensions" diff --git a/common/bin/main/org/imdc/extensions/common/DatasetExtensions.kt b/common/bin/main/org/imdc/extensions/common/DatasetExtensions.kt deleted file mode 100644 index 8684099..0000000 --- a/common/bin/main/org/imdc/extensions/common/DatasetExtensions.kt +++ /dev/null @@ -1,552 +0,0 @@ -package org.imdc.extensions.common - -import com.inductiveautomation.ignition.common.Dataset -import com.inductiveautomation.ignition.common.TypeUtilities -import com.inductiveautomation.ignition.common.script.PyArgParser -import com.inductiveautomation.ignition.common.script.builtin.KeywordArgs -import com.inductiveautomation.ignition.common.script.hints.ScriptArg -import com.inductiveautomation.ignition.common.script.hints.ScriptFunction -import com.inductiveautomation.ignition.common.util.DatasetBuilder -import com.inductiveautomation.ignition.common.xmlserialization.ClassNameResolver -import org.apache.poi.ss.usermodel.CellType.BOOLEAN -import org.apache.poi.ss.usermodel.CellType.FORMULA -import org.apache.poi.ss.usermodel.CellType.NUMERIC -import org.apache.poi.ss.usermodel.CellType.STRING -import org.apache.poi.ss.usermodel.DateUtil -import org.apache.poi.ss.usermodel.WorkbookFactory -import org.python.core.Py -import org.python.core.PyBaseString -import org.python.core.PyBoolean -import org.python.core.PyDictionary -import org.python.core.PyFloat -import org.python.core.PyFunction -import org.python.core.PyInteger -import org.python.core.PyLong -import org.python.core.PyObject -import org.python.core.PyString -import org.python.core.PyType -import org.python.core.PyUnicode -import java.io.File -import java.math.BigDecimal -import java.util.Date -import kotlin.math.max - -object DatasetExtensions { - @Suppress("unused") - @ScriptFunction(docBundlePrefix = "DatasetExtensions") - @KeywordArgs( - names = ["dataset", "mapper", "preserveColumnTypes"], - types = [Dataset::class, PyFunction::class, Boolean::class], - ) - fun map(args: Array, keywords: Array): Dataset? { - val parsedArgs = PyArgParser.parseArgs( - args, - keywords, - arrayOf("dataset", "mapper", "preserveColumnTypes"), - arrayOf(Dataset::class.java, PyObject::class.java, Boolean::class.javaObjectType), - "map", - ) - val dataset = parsedArgs.requirePyObject("dataset").toJava() - val mapper = parsedArgs.requirePyObject("mapper") - val preserveColumnTypes = parsedArgs.getBoolean("preserveColumnTypes").filter { it }.isPresent - - val columnTypes = if (preserveColumnTypes) { - dataset.columnTypes - } else { - List(dataset.columnCount) { Any::class.java } - } - - val builder = DatasetBuilder.newBuilder().colNames(dataset.columnNames).colTypes(columnTypes) - - for (row in dataset.rowIndices) { - val columnValues = Array(dataset.columnCount) { col -> - Py.java2py(dataset[row, col]) - } - val returnValue = mapper.__call__(columnValues, dataset.columnNames.toTypedArray()) - - val newValues = returnValue.asIterable().map(TypeUtilities::pyToJava).toTypedArray() - builder.addRow(*newValues) - } - - return builder.build() - } - - @Suppress("unused") - @ScriptFunction(docBundlePrefix = "DatasetExtensions") - @KeywordArgs( - names = ["dataset", "filter"], - types = [Dataset::class, PyFunction::class], - ) - fun filter(args: Array, keywords: Array): Dataset? { - val parsedArgs = PyArgParser.parseArgs( - args, - keywords, - arrayOf("dataset", "filter"), - arrayOf(Dataset::class.java, PyObject::class.java), - "filter", - ) - val dataset = parsedArgs.requirePyObject("dataset").toJava() - val filter = parsedArgs.requirePyObject("filter") - - val builder = DatasetBuilder.newBuilder() - .colNames(dataset.columnNames) - .colTypes(dataset.columnTypes) - - for (row in dataset.rowIndices) { - val filterArgs = Array(dataset.columnCount + 1) { col -> - if (col == 0) { - Py.newInteger(row) - } else { - Py.java2py(dataset[row, col - 1]) - } - } - val filterKeywords = Array(dataset.columnCount + 1) { col -> - if (col == 0) { - "row" - } else { - dataset.getColumnName(col - 1) - } - } - val returnValue = filter.__call__(filterArgs, filterKeywords).__nonzero__() - if (returnValue) { - val columnValues = Array(dataset.columnCount) { col -> - dataset[row, col] - } - builder.addRow(*columnValues) - } - } - - return builder.build() - } - - @Suppress("unused") - @ScriptFunction(docBundlePrefix = "DatasetExtensions") - @KeywordArgs( - names = ["dataset", "output", "includeTypes"], - types = [Dataset::class, Appendable::class, Boolean::class], - ) - fun print(args: Array, keywords: Array) { - val parsedArgs = PyArgParser.parseArgs( - args, - keywords, - arrayOf("dataset", "output", "includeTypes"), - Array(3) { PyObject::class.java }, - "print", - ) - val dataset = parsedArgs.requirePyObject("dataset").toJava() - val appendable = parsedArgs.getPyObject("output") - .orElse(Py.getSystemState().stdout) - .let(::PyObjectAppendable) - val includeTypes = parsedArgs.getBoolean("includeTypes").orElse(false) - - return printDataset(appendable, dataset, includeTypes) - } - - internal fun printDataset(appendable: Appendable, dataset: Dataset, includeTypes: Boolean = false) { - val typeNames = List(dataset.columnCount) { column -> - if (includeTypes) { - dataset.getColumnType(column).simpleName - } else { - "" - } - } - - val columnWidths = IntArray(dataset.columnCount) { column -> - maxOf( - // longest value in a row - if (dataset.rowCount > 0) { - dataset.rowIndices.maxOf { row -> dataset[row, column].toString().length } - } else { - 0 - }, - // longest value in a header - if (includeTypes) { - dataset.getColumnName(column).length + typeNames[column].length + 3 // 3 = two parens and a space - } else { - dataset.getColumnName(column).length - }, - // absolute minimum width for markdown table (and human eyeballs) - 3, - ) - } - - val separator = "|" - - fun Sequence.joinToBuffer() { - joinTo( - buffer = appendable, - separator = " $separator ", - prefix = "$separator ", - postfix = " $separator\n", - ) - } - - // headers - sequence { - yield("Row") - for (column in dataset.columnIndices) { - val headerValue = buildString { - append(dataset.getColumnName(column)) - if (includeTypes) { - append(" (").append(typeNames[column]).append(")") - } - while (length < columnWidths[column]) { - insert(0, ' ') - } - } - yield(headerValue) - } - }.joinToBuffer() - - // separator - sequence { - yield("---") - for (column in dataset.columnIndices) { - yield("-".repeat(columnWidths[column])) - } - }.joinToBuffer() - - // data - val maxRowLength = dataset.rowCount.toString().length.coerceAtLeast(3) - for (row in dataset.rowIndices) { - sequence { - yield(row.toString().padStart(maxRowLength)) - for (column in dataset.columnIndices) { - yield(dataset[row, column].toString().padStart(columnWidths[column])) - } - }.joinToBuffer() - } - } - - @Suppress("unused") - @ScriptFunction(docBundlePrefix = "DatasetExtensions") - @KeywordArgs( - names = ["dataset", "filterNull"], - types = [Dataset::class, Boolean::class], - ) - fun toDict(args: Array, keywords: Array): PyDictionary { - val parsedArgs = PyArgParser.parseArgs( - args, - keywords, - arrayOf( - "dataset", - "filterNull", - ), - Array(2) { Any::class.java }, - "toDict", - ) - val dataset = parsedArgs.requirePyObject("dataset").toJava() - val filterNull = parsedArgs.getBoolean("filterNull").orElse(false) - val pyDict = PyDictionary() - for (column in 0 until dataset.columnCount) { - val rowArray = if (filterNull) { - (0 until dataset.rowCount) - .mapNotNull { row -> - dataset.getValueAt(row, column) - } - .toList() - } else { - (0 until dataset.rowCount) - .map { row -> - dataset.getValueAt(row, column) - } - .toList() - } - pyDict[dataset.columnNames[column]] = rowArray - } - return pyDict - } - - @Suppress("unused") - @ScriptFunction(docBundlePrefix = "DatasetExtensions") - @KeywordArgs( - names = ["input", "headerRow", "sheetNumber", "firstRow", "lastRow", "firstColumn", "lastColumn", "stringColumns"], - types = [ByteArray::class, Int::class, Int::class, Int::class, Int::class, Int::class, Int::class, Array::class], - ) - fun fromExcel(args: Array, keywords: Array): Dataset { - val parsedArgs = PyArgParser.parseArgs( - args, - keywords, - arrayOf( - "input", - "headerRow", - "sheetNumber", - "firstRow", - "lastRow", - "firstColumn", - "lastColumn", - "stringColumns" - ), - Array(8) { Any::class.java }, - "fromExcel", - ) - - //val tempstringColumn = parsedArgs.getInteger("stringColumn").orElse(0) - val stringColumns = parsedArgs.getPyObject("stringColumns").map { it.toJava>() }.orElse(arrayOf()) - //val stringColumn = tempstringColumn != 0 - when (val input = parsedArgs.requirePyObject("input").toJava()) { - is String -> WorkbookFactory.create(File(input)) - is ByteArray -> WorkbookFactory.create(input.inputStream().buffered()) - else -> throw Py.TypeError("Unable to create Workbook from input; should be string or binary data. Got ${input::class.simpleName} instead.") - }.use { workbook -> - val sheetNumber = parsedArgs.getInteger("sheetNumber").orElse(0) - val sheet = workbook.getSheetAt(sheetNumber) - - val headerRow = parsedArgs.getInteger("headerRow").orElse(-1) - val firstRow = parsedArgs.getInteger("firstRow").orElseGet { max(sheet.firstRowNum, headerRow + 1) } - val lastRow = parsedArgs.getInteger("lastRow").orElseGet { sheet.lastRowNum } - - val dataRange = firstRow..lastRow - - if (firstRow >= lastRow) { - throw Py.ValueError("firstRow ($firstRow) must be less than lastRow ($lastRow)") - } - if (headerRow >= 0 && headerRow in dataRange) { - throw Py.ValueError("headerRow must not be in firstRow..lastRow ($dataRange)") - } - - val columnRow = sheet.getRow(if (headerRow >= 0) headerRow else firstRow) - val firstColumn = parsedArgs.getInteger("firstColumn").orElseGet { columnRow.firstCellNum.toInt() } - val lastColumn = - parsedArgs.getInteger("lastColumn").map { it + 1 }.orElseGet { columnRow.lastCellNum.toInt() } - if (firstColumn >= lastColumn) { - throw Py.ValueError("firstColumn ($firstColumn) must be less than lastColumn ($lastColumn)") - } - - val columnCount = lastColumn - firstColumn - - val dataset = DatasetBuilder() - dataset.colNames( - List(columnCount) { - if (headerRow >= 0) { - columnRow.getCell(it + firstColumn).toString() - } else { - "Col $it" - } - }, - ) - - var typesSet = false - val columnTypes = mutableListOf>() - - for (i in dataRange) { - if (i == headerRow) { - continue - } - - val row = sheet.getRow(i) - - val rowValues = Array(columnCount) { j -> - val cell = row.getCell(j + firstColumn) - val stringColumn = j in stringColumns - if(cell == null){ - if (!typesSet) { - if (stringColumn) { - columnTypes.add(String::class.java) - } - else{ - columnTypes.add(Any::class.java) - } - } - null - } - else{ - val cellType = cell.getCellType().toString() - - if(cellType == "NUMERIC") { - if (DateUtil.isCellDateFormatted(cell)) { - if (!typesSet) { - if(stringColumn){ - columnTypes.add(String::class.java) - } - else{ - columnTypes.add(Date::class.java) - } - - } - val tempCell = cell.dateCellValue - if(stringColumn){ - tempCell.toString() - } - else{ - tempCell - } - } else { - val numericCellValue = cell.numericCellValue - if (BigDecimal(numericCellValue).scale() == 0) { - if (!typesSet) { - if(stringColumn){ - columnTypes.add(String::class.java) - } - else{ - columnTypes.add(Int::class.javaObjectType) - } - } - if(stringColumn){ - numericCellValue.toInt().toString() - } - else{ - numericCellValue.toInt() - } - } else { - if (!typesSet) { - if(stringColumn){ - columnTypes.add(String::class.java) - } - else{ - columnTypes.add(Double::class.javaObjectType) - } - } - if(stringColumn){ - numericCellValue.toString() - } - else{ - numericCellValue - } - } - } - } - - else if(cellType == "STRING"){ - if (!typesSet) { - columnTypes.add(String::class.java) - } - cell.stringCellValue - } - - else if(cellType == "BOOLEAN"){ - if (!typesSet) { - columnTypes.add(Boolean::class.javaObjectType) - } - if(stringColumn){ - cell.booleanCellValue.toString() - } - else{ - cell.booleanCellValue - } - } - - else{ - if (!typesSet) { - if (stringColumn) { - columnTypes.add(String::class.java) - } - else{ - columnTypes.add(Any::class.java) - } - } - if (stringColumn) { - null.toString() - } - else{ - null - } - } - } - } - - if (!typesSet) { - typesSet = true - dataset.colTypes(columnTypes) - } - - dataset.addRow(*rowValues) - } - - return dataset.build() - } - } - - @Suppress("unused") - @ScriptFunction(docBundlePrefix = "DatasetExtensions") - fun equals( - @ScriptArg("dataset1") ds1: Dataset, - @ScriptArg("dataset2") ds2: Dataset, - ): Boolean { - return ds1 === ds2 || (columnsEqual(ds1, ds2) && valuesEqual(ds1, ds2)) - } - - @Suppress("unused") - @ScriptFunction(docBundlePrefix = "DatasetExtensions") - fun valuesEqual( - @ScriptArg("dataset1") ds1: Dataset, - @ScriptArg("dataset2") ds2: Dataset, - ): Boolean { - if (ds1 === ds2) { - return true - } - if (ds1.rowCount != ds2.rowCount || ds1.columnCount != ds2.columnCount) { - return false - } - return ds1.rowIndices.all { row -> - ds1.columnIndices.all { col -> - ds1[row, col] == ds2[row, col] - } - } - } - - @Suppress("unused") - @ScriptFunction(docBundlePrefix = "DatasetExtensions") - @JvmOverloads - fun columnsEqual( - @ScriptArg("dataset1") ds1: Dataset, - @ScriptArg("dataset2") ds2: Dataset, - @ScriptArg("ignoreCase") ignoreCase: Boolean = false, - @ScriptArg("includeTypes") includeTypes: Boolean = true, - ): Boolean { - if (ds1 === ds2) { - return true - } - if (ds1.columnCount != ds2.columnCount) { - return false - } - - val columnNames = ds1.columnNames zip ds2.columnNames - val columnNamesMatch = columnNames.all { (left, right) -> - left.equals(right, ignoreCase) - } - if (!columnNamesMatch) { - return false - } - - if (!includeTypes) { - return true - } - - val columnTypes = ds1.columnTypes.asSequence() zip ds2.columnTypes.asSequence() - return columnTypes.all { (left, right) -> - left == right - } - } - - private val classNameResolver = ClassNameResolver.createBasic() - - @ScriptFunction(docBundlePrefix = "DatasetExtensions") - @KeywordArgs( - names = ["**columns"], - types = [KeywordArgs::class], - ) - fun builder(args: Array, keywords: Array): DatasetBuilder { - if (args.size != keywords.size) throw Py.ValueError("builder must be called with only keyword arguments") - val colNames = keywords.toList() - val colTypes = args.mapIndexed { i, type -> - try { - type.asJavaClass() - } catch (e: ClassCastException) { - throw Py.TypeError("${keywords[i]} was a ${type::class.simpleName}, but should be a type or valid string typecode") - } - } - return DatasetBuilder.newBuilder().colNames(colNames).colTypes(colTypes) - } - - fun PyObject.asJavaClass(): Class<*>? = when (this) { - is PyBaseString -> classNameResolver.classForName(asString()) - !is PyType -> throw ClassCastException() - PyString.TYPE, PyUnicode.TYPE -> String::class.java - PyBoolean.TYPE -> Boolean::class.java - PyInteger.TYPE -> Int::class.java - PyLong.TYPE -> Long::class.java - PyFloat.TYPE -> Double::class.java - else -> toJava>() - } -} diff --git a/common/bin/main/org/imdc/extensions/common/DatasetExtensions.properties b/common/bin/main/org/imdc/extensions/common/DatasetExtensions.properties deleted file mode 100644 index 2c0518a..0000000 --- a/common/bin/main/org/imdc/extensions/common/DatasetExtensions.properties +++ /dev/null @@ -1,54 +0,0 @@ -map.desc=Applies a function to each row in a dataset, allowing it to be transformed in place efficiently. -map.param.dataset=The dataset to transform. Must not be null. -map.param.mapper=A callable reference to invoke for each row. Will receive each column's value as a named argument. -map.param.preserveColumnTypes=True if the types of the output dataset should match the input. Otherwise, the output dataset will lose type information. -map.returns=A modified dataset. - -filter.desc=Runs a filtering function on each row in a dataset, returning a truncated dataset. -filter.param.dataset=The dataset to filter. Must not be null. -filter.param.filter=A function to run on each row. Will be called with keyword arguments matching column names. The first argument will be named 'row' and is the row index. Return True to keep the row in the output dataset. -filter.returns=A modified dataset. - -print.desc=Prints a dataset to standard output, or the provided buffer. -print.param.dataset=The dataset to print. Must not be null. -print.param.output=The output destination. Defaults to sys.stdout. -print.param.includeTypes=If True, includes the type of the column in the first row. Defaults to False. -print.returns=None. - -toDict.desc=Converts a dataset to dict -toDict.param.dataset=The dataset to convert. Must not be null -toDict.param.filterNull=Gives the option to filter Null/None -toDict.returns=dict of dataset - -fromExcel.desc=Creates a dataset by reading select cells from an Excel spreadsheet. -fromExcel.param.input=The Excel document to read - either the path to a file on disk, or a byte array with the contents of a file. -fromExcel.param.headerRow=The row number to use for the column names in the output dataset. -fromExcel.param.sheetNumber=The sheet number (zero-indexed) in the Excel document to extract data from. -fromExcel.param.firstRow=The first row (zero-indexed) in the Excel document to retrieve data from. If not supplied, the first non-empty row will be used. -fromExcel.param.lastRow=The last row (zero-indexed) in the Excel document to retrieve data from. If not supplied, the last non-empty row will be used. -fromExcel.param.firstColumn=The first column (zero-indexed) in the Excel document to retrieve data from. If not supplied, the first non-empty column will be used. -fromExcel.param.lastColumn=The last column (zero-indexed) in the Excel document to retrieve data from. If not supplied, the last non-empty column will be used. -fromExcel.param.stringColumns=List of columns to String. -fromExcel.returns=A Dataset created from the Excel document. Types are assumed based on the first row of input data. - -equals.desc=Compares two datasets for structural equality. -equals.param.dataset1=The first dataset. Must not be null. -equals.param.dataset2=The second dataset. Must not be null. -equals.returns=True if the two datasets have the same number of columns, with the same types, in the same order, with the same data in each row. - -valuesEqual.desc=Compares two datasets' content. -valuesEqual.param.dataset1=The first dataset. Must not be null. -valuesEqual.param.dataset2=The second dataset. Must not be null. -valuesEqual.returns=True if the two datasets have the same values. - -columnsEqual.desc=Compares two datasets' column definitions. -columnsEqual.param.dataset1=The first dataset. Must not be null. -columnsEqual.param.dataset2=The second dataset. Must not be null. -columnsEqual.param.ignoreCase=Pass True if the column names should be compared case-insensitive. Defaults to False. -columnsEqual.param.includeTypes=Pass True if the column types must match as well. Defaults to True. -columnsEqual.returns=True if the two datasets have the same columns, per additional parameters. - -builder.desc=Creates a new dataset using supplied column names and types. -builder.param.**columns=Optional. Keyword arguments can be supplied to predefine column names and types. The value of the argument should be string "typecode" (see system.dataset.fromCSV) or a Java or Python class instance. -builder.returns=A DatasetBuilder object. Use addRow(value, ...) to add new values, and build() to construct the final dataset. \ - If keyword arguments were not supplied, column names and types can be manually declared using colNames() and colTypes(). \ No newline at end of file diff --git a/common/bin/main/org/imdc/extensions/common/ExtensionDocProvider.kt b/common/bin/main/org/imdc/extensions/common/ExtensionDocProvider.kt deleted file mode 100644 index 85bcb02..0000000 --- a/common/bin/main/org/imdc/extensions/common/ExtensionDocProvider.kt +++ /dev/null @@ -1,34 +0,0 @@ -package org.imdc.extensions.common - -import com.inductiveautomation.ignition.common.script.hints.PropertiesFileDocProvider -import com.inductiveautomation.ignition.common.script.hints.ScriptFunctionDocProvider -import java.lang.reflect.Method - -private val propertiesFileDocProvider = PropertiesFileDocProvider() - -private val WARNING = """ - THIS IS AN UNOFFICIAL IGNITION EXTENSION. - IT MAY RELY ON OR EXPOSE UNDOCUMENTED OR DANGEROUS FUNCTIONALITY. - USE AT YOUR OWN RISK. -""".trimIndent() - -object ExtensionDocProvider : ScriptFunctionDocProvider by propertiesFileDocProvider { - override fun getMethodDescription(path: String, method: Method): String { - val methodDescription: String? = propertiesFileDocProvider.getMethodDescription(path, method) - val unsafeAnnotation = method.getAnnotation() - - return buildString { - if (unsafeAnnotation != null) { - append("") - append(WARNING) - if (unsafeAnnotation.note.isNotEmpty()) { - append("
").append(unsafeAnnotation.note) - } - append("


") - } - append(methodDescription.orEmpty()) - } - } -} - -annotation class UnsafeExtension(val note: String = "") diff --git a/common/bin/main/org/imdc/extensions/common/ProjectExtensions.kt b/common/bin/main/org/imdc/extensions/common/ProjectExtensions.kt deleted file mode 100644 index d229648..0000000 --- a/common/bin/main/org/imdc/extensions/common/ProjectExtensions.kt +++ /dev/null @@ -1,7 +0,0 @@ -package org.imdc.extensions.common - -import com.inductiveautomation.ignition.common.project.Project - -interface ProjectExtensions { - fun getProject(): Project -} diff --git a/common/bin/main/org/imdc/extensions/common/PyDatasetBuilder.kt b/common/bin/main/org/imdc/extensions/common/PyDatasetBuilder.kt deleted file mode 100644 index bcc6155..0000000 --- a/common/bin/main/org/imdc/extensions/common/PyDatasetBuilder.kt +++ /dev/null @@ -1,58 +0,0 @@ -package org.imdc.extensions.common - -import com.inductiveautomation.ignition.common.Dataset -import com.inductiveautomation.ignition.common.script.DisposablePyObjectAdapter -import com.inductiveautomation.ignition.common.util.DatasetBuilder -import com.inductiveautomation.ignition.common.xmlserialization.ClassNameResolver -import org.imdc.extensions.common.DatasetExtensions.asJavaClass -import org.python.core.Py -import org.python.core.PyObject -import org.python.core.adapter.PyObjectAdapter - -@Suppress("unused") -class PyDatasetBuilder(private val builder: DatasetBuilder) : PyObject() { - private val resolver = ClassNameResolver.createBasic() - - fun colNames(vararg names: String) = apply { - builder.colNames(names.toList()) - } - - fun colNames(names: List) = apply { - builder.colNames(names) - } - - fun colTypes(vararg types: PyObject) = apply { - if (types.singleOrNull()?.isSequenceType == true) { - builder.colTypes(types.first().asIterable().map { it.asJavaClass() }) - } else { - builder.colTypes(types.map { it.asJavaClass() }) - } - } - - fun colTypes(types: List>) = apply { - builder.colTypes(types) - } - - fun addRow(vararg values: Any?) = apply { - builder.addRow(*values) - } - - fun build(): Dataset = builder.build() - - companion object { - private val adapter = DisposablePyObjectAdapter(DatasetBuilderAdapter()) - - fun register() { - Py.getAdapter().addPostClass(adapter) - } - - fun unregister() { - adapter.dispose() - } - } -} - -class DatasetBuilderAdapter : PyObjectAdapter { - override fun adapt(o: Any?): PyObject = PyDatasetBuilder(o as DatasetBuilder) - override fun canAdapt(o: Any?): Boolean = o is DatasetBuilder -} diff --git a/common/bin/main/org/imdc/extensions/common/TagExtensions.kt b/common/bin/main/org/imdc/extensions/common/TagExtensions.kt deleted file mode 100644 index 589e9be..0000000 --- a/common/bin/main/org/imdc/extensions/common/TagExtensions.kt +++ /dev/null @@ -1,63 +0,0 @@ -package org.imdc.extensions.common - -import com.inductiveautomation.ignition.common.config.PyTagDictionary -import com.inductiveautomation.ignition.common.config.PyTagList -import com.inductiveautomation.ignition.common.script.PyArgParser -import com.inductiveautomation.ignition.common.script.ScriptContext -import com.inductiveautomation.ignition.common.script.builtin.KeywordArgs -import com.inductiveautomation.ignition.common.script.hints.ScriptFunction -import com.inductiveautomation.ignition.common.tags.config.TagConfigurationModel -import com.inductiveautomation.ignition.common.tags.model.TagPath -import com.inductiveautomation.ignition.common.tags.paths.parser.TagPathParser -import org.python.core.PyDictionary -import org.python.core.PyObject - -abstract class TagExtensions { - @UnsafeExtension - @ScriptFunction(docBundlePrefix = "TagExtensions") - @KeywordArgs( - names = ["basePath", "recursive"], - types = [String::class, Boolean::class], - ) - fun getLocalConfiguration(args: Array, keywords: Array): PyTagList { - val parsedArgs = PyArgParser.parseArgs( - args, - keywords, - arrayOf("basePath", "recursive"), - arrayOf(Any::class.java, Any::class.java), - "getLocalConfiguration", - ) - val configurationModels = getConfigurationImpl( - parseTagPath(parsedArgs.requireString("basePath")), - parsedArgs.getBoolean("recursive").orElse(false), - ) - - return configurationModels.toPyTagList() - } - - protected open fun parseTagPath(path: String): TagPath { - val parsed = TagPathParser.parse(ScriptContext.defaultTagProvider(), path) - if (TagPathParser.isRelativePath(parsed) && ScriptContext.relativeTagPathRoot() != null) { - return TagPathParser.derelativize(parsed, ScriptContext.relativeTagPathRoot()) - } - return parsed - } - - private fun TagConfigurationModel.toPyDictionary(): PyDictionary { - return PyTagDictionary.Builder() - .setTagPath(path) - .setTagType(type) - .build(this).apply { - if (children.isNotEmpty()) { - put("tags", children.toPyTagList()) - } - } - } - - private fun List.toPyTagList() = fold(PyTagList()) { acc, childModel -> - acc.add(childModel.toPyDictionary()) - acc - } - - protected abstract fun getConfigurationImpl(basePath: TagPath, recursive: Boolean): List -} diff --git a/common/bin/main/org/imdc/extensions/common/TagExtensions.properties b/common/bin/main/org/imdc/extensions/common/TagExtensions.properties deleted file mode 100644 index 45f8560..0000000 --- a/common/bin/main/org/imdc/extensions/common/TagExtensions.properties +++ /dev/null @@ -1,4 +0,0 @@ -getLocalConfiguration.desc=WIP -getLocalConfiguration.param.basePath=WIP -getLocalConfiguration.param.recursive=WIP -getLocalConfiguration.returns=WIP diff --git a/common/bin/main/org/imdc/extensions/common/Utilities.kt b/common/bin/main/org/imdc/extensions/common/Utilities.kt deleted file mode 100644 index 16981f0..0000000 --- a/common/bin/main/org/imdc/extensions/common/Utilities.kt +++ /dev/null @@ -1,55 +0,0 @@ -package org.imdc.extensions.common - -import com.inductiveautomation.ignition.common.BundleUtil -import com.inductiveautomation.ignition.common.Dataset -import org.python.core.Py -import org.python.core.PyObject -import java.lang.reflect.Method - -class PyObjectAppendable(target: PyObject) : Appendable { - private val writeMethod = target.__getattr__("write") - - override fun append(csq: CharSequence?): Appendable = apply { - writeMethod.__call__(Py.newStringOrUnicode(csq.toString())) - } - - override fun append(csq: CharSequence?, start: Int, end: Int): Appendable = apply { - append(csq.toString().subSequence(start, end)) - } - - override fun append(c: Char): Appendable = apply { - append(c.toString()) - } -} - -val Dataset.rowIndices: IntRange - get() = (0 until rowCount) - -val Dataset.columnIndices: IntRange - get() = (0 until columnCount) - -operator fun Dataset.get(row: Int, col: Int): Any? { - return getValueAt(row, col) -} - -inline fun PyObject.toJava(): T { - try { - val cast = - this.__tojava__(T::class.java) ?: throw Py.TypeError("Expected ${T::class.java.simpleName}, got None") - return cast as T - } catch (e: ClassCastException) { - throw Py.TypeError("Expected ${T::class.java.simpleName}") - } -} - -inline fun BundleUtil.addPropertyBundle() { - addBundle( - T::class.java.simpleName, - T::class.java.classLoader, - T::class.java.name.replace('.', '/'), - ) -} - -inline fun Method.getAnnotation(): T? { - return getAnnotation(T::class.java) -} diff --git a/common/bin/main/org/imdc/extensions/common/UtilitiesExtensions$KeywordParseContext.class b/common/bin/main/org/imdc/extensions/common/UtilitiesExtensions$KeywordParseContext.class deleted file mode 100644 index a8aba4bbe31ebb6bd8cbf17c44489242baa39aef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3814 zcmbtX`F9jm7XDr*P^43V7zp4p7!@%KP^h>zA|xajOahuk<1#aq>X)XVyQ-}vB#tv~ zxQ{dAF7C@epdJMx=cqsVga44f&6)4Lu5?$2o|%s3oYbqj@4b8P`|f?;z5UC-zyATi z9r)5fMqtnn3YJ^SJCjjQcljVB(*ooW)X~&UYvLl+X+=55Vd`V2n#d09S ziuK{@d)kV?hSL?>p}?jwZ7!yR*0^2H3A9a8r5(kA6u7N^_CV!R`!*Q`u2;yVBo%LS z2dQIybkYrX?i1KIwl-AM-UBXAbdZ4du61+0x4FIHzCOXh9(9+vCqh_|5Ft9FQhCKpw-ZP^l7oKiwGwips&*E#_sg0eb{GM!f~Q=;nLMc672Qi+)%tmQ2y^MySlF6}OnU75kZy zcDXFQyuhADOH((X&TLMpxZNZ_+{qgk`ISk5Exmo0ZQvHZQZ3baS-+$11mztJg=s4hxL@mXMNy!pK&buJDI)XUN163^N!7842u4pMS+>>M)5Y zi$gf7(7o@!xJh)6Va&k&0tcG)HFN^E99ffkNchskxB|B;FX!#J7){xQ6M?_rGUizv z#{&jV2pnzJKx7v}OJ#?8l9K~V>EpNN&cMWjm=sv2=LF``P*g&j=E6kM{O!`#OC+4) zirHKnrty#gGSS|(k*JA>G0Ur@@`Y7J;GU*0dBuYvU9}RBbM1H(j~jSQAlEFG4b+(U zBmTt9W+D;Saoe*CG7vb-+t94mdW;ekbkBxkU`}9T?beLBVPql?GI7SA7ucB&SAFK{ z(x=u_Qvt4llLYK{xn$xLPP34bh4Y8QCC?GKJF#CgmXLrJFi=h$1Y|Ue(3gucQbl7V zRz*0sjy;C@+tCIePZ%f*{GoQx)Yd67?KDfLi2$rJ#^mtO@rfhM*=tiJu)%tgPmD7L z7I?uMD!$AoMGA`o`+JZ5AI)i|OHfzgdQvI)iz0{71}hBwRbZ&u2vkyy+#bu&dLSmp z&$}`|qrThm6rMKm44x$cGw-x;H33&Ss|U|3&N?fwJ)JC5Cj11}0}))=|I zHF`W%e_j^Yp(k{49p%Lfg18(>7T}~f8hBOUriK6yFG`1r;CrdXe;2s1;bIkLKd#k2 zooMrt+2iQGqvl0ILhNJ|t&wYg<4sc}$U5J~`==pfa&@KxukVh4p!M!lF{qw}TOV4r zym?RlM|gFo*)bW}&S`e8y2Uc^{z`y!9I8IBNH76LVvp^!?~T|_Bf4M zB2AGWnfMqV2wX!$wIaG|CA+0bKMou@;;Jrj`}E~M4ECx33YepwCxfA)9fp#CZ*K5& zfmuBv!{|OgjL^eww1xlckU^JfO8MSK*2o~+*{RxBzPIqJ0~`6Q%xpp@rH}Kwh2y6F zf$wld{{?LB-~Ao7eyulb=X0Z;cDd;k-%sO8%C>S7x8N#V%~8E6lNnsYcax?ok}M@; zE9~HSEoH6z>QP3;ARTR`(AlpOX!`wg#OvC-50QzvYXH2whOrH$Nq1z zbzm8HXK)VPmvCq{5e#N<0V7{s#OT09OpH@{#kZJR#v=l|FXGS3n9pDfir>IHkLV}Z z6Ke0CmSrqu@DrX|#@~ciFn$R=v$Xl#k927e&*6Ezkc=-S<15Md8doStc4-8C;#y`$ z8(<$ou2^7Ka|T5$Qo|C?;z5y#tC_~<-1mi+PyYu2KzeZi diff --git a/common/bin/main/org/imdc/extensions/common/UtilitiesExtensions.class b/common/bin/main/org/imdc/extensions/common/UtilitiesExtensions.class deleted file mode 100644 index 32ef24f0d28672841c5e16a747cab3ef08253baa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7168 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: QualifiedValue { - val qualifiedValue = expressions[0].execute() - val value = qualifiedValue.quality.isNot(QualityCode.Bad_NotFound) && - qualifiedValue.quality.isNot(QualityCode.Bad_Disabled) - return BasicQualifiedValue(value) - } - - override fun getArgDocString(): String = "value" - override fun getFunctionDisplayName(): String = NAME - override fun getType(): Class<*> = Boolean::class.java - - companion object { - const val NAME = "isAvailable" - const val CATEGORY = "Logic" - } -} diff --git a/common/bin/main/org/imdc/extensions/common/expressions/LogicalPredicate.kt b/common/bin/main/org/imdc/extensions/common/expressions/LogicalPredicate.kt deleted file mode 100644 index 97f0011..0000000 --- a/common/bin/main/org/imdc/extensions/common/expressions/LogicalPredicate.kt +++ /dev/null @@ -1,44 +0,0 @@ -package org.imdc.extensions.common.expressions - -import com.inductiveautomation.ignition.common.TypeUtilities -import com.inductiveautomation.ignition.common.expressions.Expression -import com.inductiveautomation.ignition.common.expressions.ExpressionFunctionManager -import com.inductiveautomation.ignition.common.expressions.functions.AbstractFunction -import com.inductiveautomation.ignition.common.model.values.BasicQualifiedValue -import com.inductiveautomation.ignition.common.model.values.QualifiedValue - -sealed class LogicalPredicate(val name: String) : AbstractFunction() { - override fun execute(expressions: Array): QualifiedValue { - return BasicQualifiedValue(expressions.apply()) - } - - abstract fun Array.apply(): Boolean - - override fun getArgDocString(): String = "values..." - override fun getFunctionDisplayName(): String = name - override fun getType(): Class<*> = Boolean::class.java - - object AllOfFunction : LogicalPredicate("allOf") { - override fun Array.apply() = all { it.toBoolean() } - } - - object AnyOfFunction : LogicalPredicate("anyOf") { - override fun Array.apply() = any { it.toBoolean() } - } - - object NoneOfFunction : LogicalPredicate("noneOf") { - override fun Array.apply() = none { it.toBoolean() } - } - - companion object { - private const val CATEGORY = "Logic" - - private fun Expression.toBoolean(): Boolean = TypeUtilities.toBool(execute().value) - - fun ExpressionFunctionManager.registerLogicFunctions() { - addFunction(AllOfFunction.name, CATEGORY, AllOfFunction) - addFunction(AnyOfFunction.name, CATEGORY, AnyOfFunction) - addFunction(NoneOfFunction.name, CATEGORY, NoneOfFunction) - } - } -} diff --git a/common/bin/test/org/imdc/extensions/common/DSBuilder.kt b/common/bin/test/org/imdc/extensions/common/DSBuilder.kt deleted file mode 100644 index 9dcc1cf..0000000 --- a/common/bin/test/org/imdc/extensions/common/DSBuilder.kt +++ /dev/null @@ -1,38 +0,0 @@ -package org.imdc.extensions.common - -import com.inductiveautomation.ignition.common.BasicDataset -import com.inductiveautomation.ignition.common.Dataset - -class DSBuilder { - data class Column(val name: String, val rows: List<*>, val type: Class<*>) - - val columns = mutableListOf() - - inline fun column(name: String, data: List) { - columns.add(Column(name, data, T::class.java)) - } - - inline fun column(name: String, builder: MutableList.() -> Unit) { - column(name, buildList(builder)) - } - - fun build(): Dataset { - val colCount = columns.size - val rowCount = columns.maxOf { it.rows.size } - val data = Array(colCount) { arrayOfNulls(rowCount) } - - for (c in 0 until colCount) { - for (r in 0 until rowCount) { - data[c][r] = columns.getOrNull(c)?.rows?.getOrNull(r) - } - } - - return BasicDataset(columns.map { it.name }, columns.map { it.type }, data) - } - - companion object { - fun dataset(block: DSBuilder.() -> Unit): Dataset { - return DSBuilder().apply(block).build() - } - } -} diff --git a/common/bin/test/org/imdc/extensions/common/DatasetEqualityTests.kt b/common/bin/test/org/imdc/extensions/common/DatasetEqualityTests.kt deleted file mode 100644 index a9cfd00..0000000 --- a/common/bin/test/org/imdc/extensions/common/DatasetEqualityTests.kt +++ /dev/null @@ -1,156 +0,0 @@ -package org.imdc.extensions.common - -import com.inductiveautomation.ignition.common.BasicDataset -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe -import org.imdc.extensions.common.DSBuilder.Companion.dataset -import java.util.Date - -class DatasetEqualityTests : FunSpec( - { - val dataset1 = dataset { - column("a", listOf(1, 2, 3)) - column("b", listOf(3.14, 2.18, 4.96)) - column("c", listOf("string", "strung", "strang")) - } - val copyOfDs1 = BasicDataset(dataset1) - val ds1WithCaps = BasicDataset(listOf("A", "B", "C"), dataset1.columnTypes, dataset1) - val ds1WithOtherNames = BasicDataset(listOf("ant", "bat", "cat"), dataset1.columnTypes, dataset1) - val dataset2 = dataset { - column("j", listOf(3, 2, 1)) - column("k", listOf(1.0, 2.0, 3.0)) - column("l", listOf("chess", "chass", "chuss")) - } - val copyOfDs2 = BasicDataset(dataset2) - val ds2WithCaps = BasicDataset(listOf("J", "K", "L"), dataset2.columnTypes, dataset2) - val ds2WithOtherNames = BasicDataset(listOf("joe", "kim", "lee"), dataset2.columnTypes, dataset2) - - val dataset3 = dataset { - column("t_stamp", listOf(1667605391000, 1667605392000, 1667605393000, 1667605394000).map(::Date)) - column("tag_1", listOf(1.0, 2.0, 3.0, 4.0)) - column("tag_2", listOf(true, false, true, false)) - } - val copyOfDs3 = BasicDataset(dataset3) - val ds3WithAliases = BasicDataset(listOf("timestamp", "doubleTag", "boolTag"), dataset3.columnTypes, dataset3) - - context("General equality") { - test("Same dataset") { - DatasetExtensions.equals(dataset1, dataset1) shouldBe true - DatasetExtensions.equals(dataset2, dataset2) shouldBe true - DatasetExtensions.equals(dataset3, dataset3) shouldBe true - } - - test("Copied dataset") { - DatasetExtensions.equals(dataset1, copyOfDs1) shouldBe true - DatasetExtensions.equals(dataset2, copyOfDs2) shouldBe true - DatasetExtensions.equals(dataset3, copyOfDs3) shouldBe true - } - - test("Different datasets") { - DatasetExtensions.equals(dataset1, dataset2) shouldBe false - DatasetExtensions.equals(dataset1, dataset3) shouldBe false - DatasetExtensions.equals(dataset2, dataset3) shouldBe false - } - - test("Datasets with same structure, but different columns") { - DatasetExtensions.equals(dataset1, ds1WithCaps) shouldBe false - DatasetExtensions.equals(dataset1, ds1WithOtherNames) shouldBe false - DatasetExtensions.equals(dataset2, ds2WithCaps) shouldBe false - DatasetExtensions.equals(dataset2, ds2WithOtherNames) shouldBe false - DatasetExtensions.equals(dataset3, ds3WithAliases) shouldBe false - } - } - - context("Value equality") { - test("Same dataset") { - DatasetExtensions.valuesEqual(dataset1, dataset1) shouldBe true - DatasetExtensions.valuesEqual(dataset2, dataset2) shouldBe true - DatasetExtensions.valuesEqual(dataset3, dataset3) shouldBe true - } - - test("Copied dataset") { - DatasetExtensions.valuesEqual(dataset1, copyOfDs1) shouldBe true - DatasetExtensions.valuesEqual(dataset2, copyOfDs2) shouldBe true - DatasetExtensions.valuesEqual(dataset3, copyOfDs3) shouldBe true - } - - test("Different datasets") { - DatasetExtensions.valuesEqual(dataset1, dataset2) shouldBe false - DatasetExtensions.valuesEqual(dataset1, dataset3) shouldBe false - DatasetExtensions.valuesEqual(dataset2, dataset3) shouldBe false - } - - test("Datasets with same structure, but different columns") { - DatasetExtensions.valuesEqual(dataset1, ds1WithCaps) shouldBe true - DatasetExtensions.valuesEqual(dataset1, ds1WithOtherNames) shouldBe true - DatasetExtensions.valuesEqual(dataset2, ds2WithCaps) shouldBe true - DatasetExtensions.valuesEqual(dataset2, ds2WithOtherNames) shouldBe true - DatasetExtensions.valuesEqual(dataset3, ds3WithAliases) shouldBe true - } - } - - context("Column equality") { - test("Same dataset") { - DatasetExtensions.columnsEqual(dataset1, dataset1) shouldBe true - DatasetExtensions.columnsEqual(dataset2, dataset2) shouldBe true - DatasetExtensions.columnsEqual(dataset3, dataset3) shouldBe true - } - - test("Copied dataset") { - DatasetExtensions.columnsEqual(dataset1, copyOfDs1) shouldBe true - DatasetExtensions.columnsEqual(dataset2, copyOfDs2) shouldBe true - DatasetExtensions.columnsEqual(dataset3, copyOfDs3) shouldBe true - } - - test("Different datasets") { - DatasetExtensions.columnsEqual(dataset1, dataset2) shouldBe false - DatasetExtensions.columnsEqual(dataset1, dataset3) shouldBe false - DatasetExtensions.columnsEqual(dataset2, dataset3) shouldBe false - } - - test("Datasets with same structure, but different columns") { - DatasetExtensions.columnsEqual(dataset1, ds1WithCaps) shouldBe false - DatasetExtensions.columnsEqual(dataset1, ds1WithCaps, ignoreCase = true) shouldBe true - DatasetExtensions.columnsEqual(dataset1, ds1WithOtherNames) shouldBe false - DatasetExtensions.columnsEqual(dataset2, ds2WithCaps) shouldBe false - DatasetExtensions.columnsEqual(dataset2, ds2WithCaps, ignoreCase = true) shouldBe true - DatasetExtensions.columnsEqual(dataset2, ds2WithOtherNames) shouldBe false - DatasetExtensions.columnsEqual(dataset3, ds3WithAliases) shouldBe false - - val ds1WithDifferentTypes = BasicDataset( - dataset1.columnNames, - listOf(String::class.java, Int::class.java, Boolean::class.java), - dataset1, - ) - DatasetExtensions.columnsEqual(dataset1, ds1WithDifferentTypes) shouldBe false - DatasetExtensions.columnsEqual(dataset1, ds1WithDifferentTypes, ignoreCase = true) shouldBe false - DatasetExtensions.columnsEqual(dataset1, ds1WithDifferentTypes, includeTypes = false) shouldBe true - DatasetExtensions.columnsEqual( - dataset1, - ds1WithDifferentTypes, - includeTypes = false, - ignoreCase = true, - ) shouldBe true - - val ds1WithDifferentTypesAndCase = BasicDataset( - listOf("A", "B", "C"), - dataset1.columnTypes, - dataset1, - ) - DatasetExtensions.columnsEqual(dataset1, ds1WithDifferentTypesAndCase) shouldBe false - DatasetExtensions.columnsEqual(dataset1, ds1WithDifferentTypesAndCase, ignoreCase = true) shouldBe true - DatasetExtensions.columnsEqual( - dataset1, - ds1WithDifferentTypesAndCase, - includeTypes = false, - ) shouldBe false - DatasetExtensions.columnsEqual( - dataset1, - ds1WithDifferentTypesAndCase, - ignoreCase = true, - includeTypes = false, - ) shouldBe true - } - } - }, -) diff --git a/common/bin/test/org/imdc/extensions/common/DatasetExtensionsTests.kt b/common/bin/test/org/imdc/extensions/common/DatasetExtensionsTests.kt deleted file mode 100644 index 2cda029..0000000 --- a/common/bin/test/org/imdc/extensions/common/DatasetExtensionsTests.kt +++ /dev/null @@ -1,556 +0,0 @@ -package org.imdc.extensions.common - -import com.inductiveautomation.ignition.common.BasicDataset -import com.inductiveautomation.ignition.common.Dataset -import com.inductiveautomation.ignition.common.util.DatasetBuilder -import io.kotest.assertions.withClue -import io.kotest.matchers.shouldBe -import org.imdc.extensions.common.DatasetExtensions.printDataset -import org.python.core.Py -import java.awt.Color -import java.util.Date -import kotlin.io.path.createTempFile -import kotlin.io.path.writeBytes - -@Suppress("PyUnresolvedReferences", "PyInterpreter") -class DatasetExtensionsTests : JythonTest( - { globals -> - globals["utils"] = DatasetExtensions - globals["builder"] = DatasetBuilder.newBuilder() - globals["dataset"] = DatasetBuilder.newBuilder() - .colNames("a", "b", "c") - .colTypes(Int::class.javaObjectType, Double::class.javaObjectType, String::class.java) - .addRow(1, 3.14, "pi") - .addRow(2, 6.28, "tau") - .build() - - val excelSample = - DatasetExtensionsTests::class.java.getResourceAsStream("sample.xlsx")!!.readAllBytes() - val tempXlsx = createTempFile(suffix = "xlsx").also { - it.writeBytes(excelSample) - it.toFile().deleteOnExit() - } - globals["xlsxBytes"] = excelSample - globals["xlsxFile"] = tempXlsx.toString() - - val xlsSample = - DatasetExtensionsTests::class.java.getResourceAsStream("sample.xls")!!.readAllBytes() - val tempXls = createTempFile(suffix = "xls").also { - it.writeBytes(xlsSample) - it.toFile().deleteOnExit() - } - globals["xlsBytes"] = xlsSample - globals["xlsFile"] = tempXls.toString() - - globals["date"] = Date::class.java - globals["color"] = Color::class.java - globals["javaInt"] = Int::class.java - globals["javaString"] = String::class.java - globals["javaBool"] = Boolean::class.java - }, -) { - private fun Dataset.asClue(assertions: (Dataset) -> Unit) { - withClue( - lazy { - buildString { - printDataset(this, this@asClue, true) - } - }, - ) { - assertions(this) - } - } - - init { - PyDatasetBuilder.register() - - context("Map tests") { - test("Null dataset") { - shouldThrowPyException(Py.TypeError) { - eval("utils.map(None, None)") - } - } - - test("Simple mapper, preserving types") { - eval("utils.map(dataset, lambda a, b, c: (a * 2, b * 2, c * 2), True)").asClue { - it.columnNames shouldBe listOf("a", "b", "c") - it.columnTypes shouldBe listOf( - Int::class.javaObjectType, - Double::class.javaObjectType, - String::class.java, - ) - it.rowCount shouldBe 2 - it.getColumnAsList(0) shouldBe listOf(2, 4) - it.getColumnAsList(1) shouldBe listOf(6.28, 12.56) - it.getColumnAsList(2) shouldBe listOf("pipi", "tautau") - } - } - - test("Simple mapper, discarding types") { - eval("utils.map(dataset, lambda a, b, c: (a * 2, b * 2, c * 2), False)").asClue { - it.columnNames shouldBe listOf("a", "b", "c") - it.columnTypes shouldBe listOf(Any::class.java, Any::class.java, Any::class.java) - it.rowCount shouldBe 2 - it.getColumnAsList(0) shouldBe listOf(2, 4) - it.getColumnAsList(1) shouldBe listOf(6.28, 12.56) - it.getColumnAsList(2) shouldBe listOf("pipi", "tautau") - } - } - } - - context("Filter tests") { - test("Constant filter") { - eval("utils.filter(dataset, lambda **kwargs: False)").asClue { - it.columnNames shouldBe listOf("a", "b", "c") - it.columnTypes shouldBe listOf( - Int::class.javaObjectType, - Double::class.javaObjectType, - String::class.java, - ) - it.rowCount shouldBe 0 - } - } - - test("Conditional filter all kwargs") { - eval("utils.filter(dataset, lambda **kwargs: kwargs['row'] >= 1)").asClue { - it.columnNames shouldBe listOf("a", "b", "c") - it.columnTypes shouldBe listOf( - Int::class.javaObjectType, - Double::class.javaObjectType, - String::class.java, - ) - it.rowCount shouldBe 1 - } - } - - test("Conditional filter unpacking row") { - eval("utils.filter(dataset, lambda row, **kwargs: row >= 1)").asClue { - it.columnNames shouldBe listOf("a", "b", "c") - it.columnTypes shouldBe listOf( - Int::class.javaObjectType, - Double::class.javaObjectType, - String::class.java, - ) - it.rowCount shouldBe 1 - } - } - } - - context("Print tests") { - context("Basic dataset") { - test("Without types") { - buildString { - printDataset(this, globals["dataset"]) - } shouldBe """ - | Row | a | b | c | - | --- | --- | ---- | --- | - | 0 | 1 | 3.14 | pi | - | 1 | 2 | 6.28 | tau | - - """.trimIndent() - } - - test("With types") { - buildString { - printDataset(this, globals["dataset"], includeTypes = true) - } shouldBe """ - | Row | a (Integer) | b (Double) | c (String) | - | --- | ----------- | ---------- | ---------- | - | 0 | 1 | 3.14 | pi | - | 1 | 2 | 6.28 | tau | - - """.trimIndent() - } - } - - val emptyDataset = BasicDataset( - listOf("a", "b", "c"), - listOf(String::class.java, Int::class.java, Boolean::class.java), - ) - context("Empty dataset") { - test("Without types") { - buildString { - printDataset(this, emptyDataset) - } shouldBe """ - | Row | a | b | c | - | --- | --- | --- | --- | - - """.trimIndent() - } - - test("With types") { - buildString { - printDataset(this, emptyDataset, includeTypes = true) - } shouldBe """ - | Row | a (String) | b (int) | c (boolean) | - | --- | ---------- | ------- | ----------- | - - """.trimIndent() - } - } - } - - context("fromExcel") { - test("XLSX file") { - eval("utils.fromExcel(xlsxFile)").asClue { - it.rowCount shouldBe 100 - it.columnCount shouldBe 16 - } - } - test("XLS file") { - eval("utils.fromExcel(xlsFile)").asClue { - it.rowCount shouldBe 100 - it.columnCount shouldBe 16 - } - } - test("XLSX bytes") { - eval("utils.fromExcel(xlsxBytes)").asClue { - it.rowCount shouldBe 100 - it.columnCount shouldBe 16 - } - } - test("XLS bytes") { - eval("utils.fromExcel(xlsBytes)").asClue { - it.rowCount shouldBe 100 - it.columnCount shouldBe 16 - } - } - test("With headers") { - eval("utils.fromExcel(xlsxBytes, headerRow=0)").asClue { - it.rowCount shouldBe 99 - it.columnCount shouldBe 16 - it.columnNames shouldBe listOf( - "Segment", - "Country", - "Product", - "Discount Band", - "Units Sold", - "Manufacturing Price", - "Sale Price", - "Gross Sales", - "Discounts", - "Sales", - "COGS", - "Profit", - "Date", - "Month Number", - "Month Name", - "Year", - ) - } - } - test("First row") { - eval("utils.fromExcel(xlsxBytes, headerRow=0, firstRow=50)").asClue { - it.rowCount shouldBe 50 - it.columnCount shouldBe 16 - } - } - test("Last row") { - eval("utils.fromExcel(xlsxBytes, headerRow=0, lastRow=50)").asClue { - it.rowCount shouldBe 50 - it.columnCount shouldBe 16 - } - } - test("First & last row") { - eval("utils.fromExcel(xlsxBytes, headerRow=0, firstRow=5, lastRow=10)").asClue { - it.rowCount shouldBe 6 - it.columnCount shouldBe 16 - } - } - test("First column") { - eval("utils.fromExcel(xlsxBytes, headerRow=0, firstColumn=10)").asClue { - it.rowCount shouldBe 99 - it.columnCount shouldBe 6 - it.columnNames shouldBe listOf( - "COGS", - "Profit", - "Date", - "Month Number", - "Month Name", - "Year", - ) - } - } - test("Last column") { - eval("utils.fromExcel(xlsxBytes, headerRow=0, lastColumn=5)").asClue { - it.rowCount shouldBe 99 - it.columnCount shouldBe 6 - it.columnNames shouldBe listOf( - "Segment", - "Country", - "Product", - "Discount Band", - "Units Sold", - "Manufacturing Price", - ) - } - } - test("First & last column") { - eval("utils.fromExcel(xlsxBytes, headerRow=0, firstColumn=5, lastColumn=10)").asClue { - it.rowCount shouldBe 99 - it.columnCount shouldBe 6 - it.columnNames shouldBe listOf( - "Manufacturing Price", - "Sale Price", - "Gross Sales", - "Discounts", - "Sales", - "COGS", - ) - } - } - } - - context("Builder") { - test("Basic usage") { - eval("utils.builder(a=int, b=str, c=bool).addRow(1, '2', False).build()").asClue { - it.rowCount shouldBe 1 - it.columnCount shouldBe 3 - it.columnNames shouldBe listOf( - "a", - "b", - "c", - ) - it.columnTypes shouldBe listOf( - Int::class.java, - String::class.java, - Boolean::class.java, - ) - } - } - - test("String type codes in builder call") { - eval("utils.builder(a='i', b='str', c='b').addRow(1, '2', False).build()").asClue { - it.rowCount shouldBe 1 - it.columnCount shouldBe 3 - it.columnNames shouldBe listOf( - "a", - "b", - "c", - ) - it.columnTypes shouldBe listOf( - Int::class.java, - String::class.java, - Boolean::class.java, - ) - } - } - - test("Separate colTypes as java types") { - eval( - """ - utils.builder() \ - .colNames('a', 'b', 'c') \ - .colTypes(javaInt, javaString, javaBool) \ - .addRow(1, '2', False) \ - .build() - """.trimIndent(), - ).asClue { - it.rowCount shouldBe 1 - it.columnCount shouldBe 3 - it.columnNames shouldBe listOf( - "a", - "b", - "c", - ) - it.columnTypes shouldBe listOf( - Int::class.java, - String::class.java, - Boolean::class.java, - ) - } - } - - test("Separate colTypes as Python types") { - eval( - """ - utils.builder() \ - .colNames('a', 'b', 'c') \ - .colTypes(int, str, bool) \ - .addRow(1, '2', False) \ - .build() - """.trimIndent(), - ).asClue { - it.rowCount shouldBe 1 - it.columnCount shouldBe 3 - it.columnNames shouldBe listOf( - "a", - "b", - "c", - ) - it.columnTypes shouldBe listOf( - Int::class.java, - String::class.java, - Boolean::class.java, - ) - } - } - - test("Separate colTypes as string shortcodes") { - eval( - """ - utils.builder() \ - .colNames('a', 'b', 'c') \ - .colTypes('i', 'str', 'b') \ - .addRow(1, '2', False) \ - .build() - """.trimIndent(), - ).asClue { - it.rowCount shouldBe 1 - it.columnCount shouldBe 3 - it.columnNames shouldBe listOf( - "a", - "b", - "c", - ) - it.columnTypes shouldBe listOf( - Int::class.java, - String::class.java, - Boolean::class.java, - ) - } - } - - test("Complex types") { - eval( - """ - utils.builder(date=date, color=color, unicode=unicode) \ - .addRow(date(), color.RED, u'test') \ - .build() - """.trimIndent(), - ).asClue { - it.rowCount shouldBe 1 - it.columnCount shouldBe 3 - it.columnNames shouldBe listOf( - "date", - "color", - "unicode", - ) - it.columnTypes shouldBe listOf( - Date::class.java, - Color::class.java, - String::class.java, - ) - } - } - - test("Nulls in rows") { - eval( - """ - utils.builder(a=int, b=str, c=bool) \ - .addRow(1, '2', False) \ - .addRow(None, None, None) \ - .build() - """.trimIndent(), - ).asClue { - it.rowCount shouldBe 2 - it.columnCount shouldBe 3 - it.columnNames shouldBe listOf( - "a", - "b", - "c", - ) - it.columnTypes shouldBe listOf( - Int::class.java, - String::class.java, - Boolean::class.java, - ) - } - } - - test("Empty dataset") { - eval("utils.builder().build()").asClue { - it.rowCount shouldBe 0 - it.columnCount shouldBe 0 - } - } - - test("Add a row as a list") { - eval( - """ - utils.builder(a=int, b=str, c=bool) \ - .addRow([1, '2', False]) \ - .build() - """.trimIndent(), - ).asClue { - it.rowCount shouldBe 1 - it.columnCount shouldBe 3 - } - } - - test("Add a row as a tuple") { - eval( - """ - utils.builder(a=int, b=str, c=bool) \ - .addRow((1, '2', False)) \ - .build() - """.trimIndent(), - ).asClue { - it.rowCount shouldBe 1 - it.columnCount shouldBe 3 - } - } - - test("Columns with complex names") { - eval( - """ - utils.builder(**{'a space': int, u'😍': bool, r',./;\'[]-=<>?:"{}|_+!@#%^&*()`~': str}) \ - .addRow((1, '2', False)) \ - .build() - """.trimIndent(), - ).asClue { - it.rowCount shouldBe 1 - it.columnCount shouldBe 3 - it.columnNames shouldBe listOf( - "a space", - "\uD83D\uDE0D", - """,./;\'[]-=<>?:"{}|_+!@#%^&*()`~""", - ) - } - } - - test("Invalid types") { - shouldThrowPyException(Py.TypeError) { - eval("utils.builder(a=1).build()") - } - shouldThrowPyException(Py.TypeError) { - eval("utils.builder(a=None).build()") - } - } - - test("Row without enough vararg values") { - shouldThrowPyException(Py.TypeError) { - eval( - """ - utils.builder(a=int, b=str, c=bool) \ - .addRow(1, '2') \ - .build() - """.trimIndent(), - ) - } - } - - test("Row without enough list values") { - shouldThrowPyException(Py.TypeError) { - eval( - """ - utils.builder(a=int, b=str, c=bool) \ - .addRow([1, '2']) \ - .build() - """.trimIndent(), - ) - } - } - - test("Column types regression (issue #24)") { - eval( - """ - builder.colNames(dataset.columnNames) \ - .colTypes(dataset.columnTypes) \ - .build() - """.trimIndent(), - ) - } - } - } -} diff --git a/common/bin/test/org/imdc/extensions/common/ExpressionUtils.kt b/common/bin/test/org/imdc/extensions/common/ExpressionUtils.kt deleted file mode 100644 index 283fa44..0000000 --- a/common/bin/test/org/imdc/extensions/common/ExpressionUtils.kt +++ /dev/null @@ -1,40 +0,0 @@ -package org.imdc.extensions.common - -import com.inductiveautomation.ignition.common.expressions.AbstractFunctionFactory -import com.inductiveautomation.ignition.common.expressions.ConstantExpression -import com.inductiveautomation.ignition.common.expressions.Expression -import com.inductiveautomation.ignition.common.expressions.ExpressionParseContext -import com.inductiveautomation.ignition.common.expressions.FunctionFactory -import com.inductiveautomation.ignition.common.expressions.functions.Function -import com.inductiveautomation.ignition.common.expressions.parsing.ELParserHarness - -class ExpressionTestHarness : AbstractFunctionFactory(null) { - private val parser = ELParserHarness() - - fun evaluate(expression: String, constants: Map = emptyMap()): Any? { - val parseContext = StaticParseContext(constants, this) - return parser.parse(expression, parseContext).execute().value - } - - companion object { - suspend fun withFunction(name: String, function: Function, test: suspend ExpressionTestHarness.() -> Unit) { - ExpressionTestHarness().apply { - addFunction(name, "", function) - test() - } - } - } -} - -class StaticParseContext( - private val keywords: Map, - private val functionFactory: FunctionFactory, -) : ExpressionParseContext { - override fun createBoundExpression(reference: String): Expression { - return ConstantExpression(keywords.getValue(reference)) - } - - override fun getFunctionFactory(): FunctionFactory { - return functionFactory - } -} diff --git a/common/bin/test/org/imdc/extensions/common/JythonTest.kt b/common/bin/test/org/imdc/extensions/common/JythonTest.kt deleted file mode 100644 index 9144e89..0000000 --- a/common/bin/test/org/imdc/extensions/common/JythonTest.kt +++ /dev/null @@ -1,93 +0,0 @@ -package org.imdc.extensions.common - -import com.inductiveautomation.ignition.common.TypeUtilities -import io.kotest.assertions.fail -import io.kotest.core.listeners.BeforeEachListener -import io.kotest.core.spec.style.FunSpec -import io.kotest.core.test.TestCase -import io.kotest.matchers.Matcher -import io.kotest.matchers.MatcherResult -import org.intellij.lang.annotations.Language -import org.python.core.CompileMode -import org.python.core.CompilerFlags -import org.python.core.Py -import org.python.core.PyBaseException -import org.python.core.PyCode -import org.python.core.PyException -import org.python.core.PyObject -import org.python.core.PyStringMap -import org.python.core.PyType - -@Suppress("PyInterpreter") -abstract class JythonTest(init: (globals: PyStringMap) -> Unit) : FunSpec() { - protected var globals: PyStringMap = PyStringMap() - - init { - extension( - object : BeforeEachListener { - override suspend fun beforeEach(testCase: TestCase) { - globals.clear() - init(globals) - } - }, - ) - } - - protected inline fun eval(@Language("python") script: String): T { - exec("$RESULT = $script") - return globals[RESULT] - } - - protected fun exec(@Language("python") script: String?) { - val compiledCall = compile(script) - Py.runCode(compiledCall, null, globals) - } - - private fun compile(@Language("python") script: String?): PyCode { - return Py.compile_flags( - script, - "", - CompileMode.exec, - CompilerFlags(CompilerFlags.PyCF_SOURCE_IS_UTF8), - ) - } - - fun shouldThrowPyException(type: PyObject, case: () -> Unit) { - require(type is PyType) - require(type.isSubType(PyBaseException.TYPE)) - try { - case() - } catch (exception: PyException) { - PyExceptionTypeMatcher(exception).test(type) - return - } - fail("Expected a $type to be thrown, but nothing was thrown") - } - - companion object { - const val RESULT = "__RESULT" - - init { - Py.setSystemState(Py.defaultSystemState) - } - - inline operator fun PyStringMap.get(variable: String): T { - val value = this[Py.newStringOrUnicode(variable)] - return TypeUtilities.pyToJava(value) as T - } - - operator fun PyStringMap.set(key: String, value: Any?) { - return __setitem__(key, Py.java2py(value)) - } - - class PyExceptionTypeMatcher(val expected: PyException) : Matcher { - override fun test(value: PyObject): MatcherResult { - return MatcherResult( - expected.match(value), - failureMessageFn = { "Expected a $expected, but was $value" }, - negatedFailureMessageFn = { "Result should not be a $expected, but was $value" }, - ) - } - } - } -} diff --git a/common/bin/test/org/imdc/extensions/common/LogicalFunctionsTests.kt b/common/bin/test/org/imdc/extensions/common/LogicalFunctionsTests.kt deleted file mode 100644 index 6aba617..0000000 --- a/common/bin/test/org/imdc/extensions/common/LogicalFunctionsTests.kt +++ /dev/null @@ -1,49 +0,0 @@ -package org.imdc.extensions.common - -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe -import org.imdc.extensions.common.ExpressionTestHarness.Companion.withFunction -import org.imdc.extensions.common.expressions.LogicalPredicate.AllOfFunction -import org.imdc.extensions.common.expressions.LogicalPredicate.AnyOfFunction -import org.imdc.extensions.common.expressions.LogicalPredicate.NoneOfFunction - -class LogicalFunctionsTests : FunSpec() { - init { - context("AllOf") { - withFunction("allOf", AllOfFunction) { - test("No arguments") { - evaluate("allOf()") shouldBe true - } - test("Simple booleans") { - evaluate("allOf(true, true)") shouldBe true - evaluate("allOf(true, false)") shouldBe false - evaluate("allOf(false, false)") shouldBe false - } - } - } - context("AnyOf") { - withFunction("anyOf", AnyOfFunction) { - test("No arguments") { - evaluate("anyOf()") shouldBe false - } - test("Simple booleans") { - evaluate("anyOf(true, true)") shouldBe true - evaluate("anyOf(true, false)") shouldBe true - evaluate("anyOf(false, false)") shouldBe false - } - } - } - context("NoneOf") { - withFunction("noneOf", NoneOfFunction) { - test("No arguments") { - evaluate("noneOf()") shouldBe true - } - test("Simple booleans") { - evaluate("noneOf(true, true)") shouldBe false - evaluate("noneOf(true, false)") shouldBe false - evaluate("noneOf(false, false)") shouldBe true - } - } - } - } -} diff --git a/common/bin/test/org/imdc/extensions/common/UtilitiesExtensionsTests.kt b/common/bin/test/org/imdc/extensions/common/UtilitiesExtensionsTests.kt deleted file mode 100644 index a853fdd..0000000 --- a/common/bin/test/org/imdc/extensions/common/UtilitiesExtensionsTests.kt +++ /dev/null @@ -1,50 +0,0 @@ -package org.imdc.extensions.common - -import io.kotest.matchers.nulls.shouldBeNull -import io.kotest.matchers.shouldBe -import io.mockk.mockk -import org.python.core.Py -import org.python.core.PyDictionary -import org.python.core.PyList - -@Suppress("PyUnresolvedReferences", "PyInterpreter") -class UtilitiesExtensionsTests : JythonTest( - { globals -> - globals["utils"] = UtilitiesExtensions(mockk()) - }, -) { - init { - context("Deep copy tests") { - test("No input") { - shouldThrowPyException(Py.TypeError) { - eval("utils.deepCopy()") - } - } - test("Null input") { - eval("utils.deepCopy(None)").shouldBeNull() - } - test("String input") { - eval("utils.deepCopy('abc')") shouldBe "abc" - eval("utils.deepCopy(u'abc')") shouldBe "abc" - } - context("Simple conversions") { - test("List") { - eval("utils.deepCopy([1, 2, 3])") shouldBe listOf(1, 2, 3) - } - test("Tuple") { - eval("utils.deepCopy((1, 2, 3))") shouldBe listOf(1, 2, 3) - } - test("Set") { - eval("utils.deepCopy({1, 2, 3})") shouldBe listOf(1, 2, 3) - } - test("Dict") { - eval("utils.deepCopy({'a': 1, 'b': 2, 'c': 3})\n") shouldBe mapOf( - "a" to 1, - "b" to 2, - "c" to 3, - ) - } - } - } - } -} diff --git a/common/bin/test/org/imdc/extensions/common/sample.xls b/common/bin/test/org/imdc/extensions/common/sample.xls deleted file mode 100644 index 7ec3bda64796095198e906eae5bed8da70097a96..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 35328 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 B$cF#` diff --git a/common/bin/test/org/imdc/extensions/common/sample.xlsx b/common/bin/test/org/imdc/extensions/common/sample.xlsx deleted file mode 100644 index 15fd838ccc7e46c30f13085d89ba9154dbcfeec5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13410 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 - addPropertyBundle() - addPropertyBundle() - } - - PyDatasetBuilder.register() - } - - override fun shutdown() { - PyDatasetBuilder.unregister() - } - - override fun initializeScriptManager(manager: ScriptManager) { - manager.apply { - addScriptModule("system.dataset", DatasetExtensions, ExtensionDocProvider) - addScriptModule("system.util", UtilitiesExtensions(context), ExtensionDocProvider) - addScriptModule("system.project", DesignerProjectExtensions(context), ExtensionDocProvider) - } - } - - override fun configureFunctionFactory(factory: ExpressionFunctionManager) { - factory.apply { - addFunction( - IsAvailableFunction.NAME, - IsAvailableFunction.CATEGORY, - IsAvailableFunction(), - ) - registerLogicFunctions() - } - } -} diff --git a/designer/bin/main/org/imdc/extensions/designer/DesignerProjectExtensions.kt b/designer/bin/main/org/imdc/extensions/designer/DesignerProjectExtensions.kt deleted file mode 100644 index df22708..0000000 --- a/designer/bin/main/org/imdc/extensions/designer/DesignerProjectExtensions.kt +++ /dev/null @@ -1,38 +0,0 @@ -package org.imdc.extensions.designer - -import com.inductiveautomation.ignition.common.script.hints.ScriptFunction -import com.inductiveautomation.ignition.designer.IgnitionDesigner -import com.inductiveautomation.ignition.designer.model.DesignerContext -import com.inductiveautomation.ignition.designer.project.DesignableProject -import org.apache.commons.lang3.reflect.MethodUtils -import org.imdc.extensions.common.ProjectExtensions -import org.imdc.extensions.common.UnsafeExtension - -class DesignerProjectExtensions(private val context: DesignerContext) : ProjectExtensions { - @ScriptFunction(docBundlePrefix = "DesignerProjectExtensions") - @UnsafeExtension - override fun getProject(): DesignableProject { - return requireNotNull(context.project) - } - - @ScriptFunction(docBundlePrefix = "DesignerProjectExtensions") - @UnsafeExtension - fun save() { - MethodUtils.invokeMethod( - context.frame, - true, // forceAccess - "handleSave", - false, // saveAs - null, // newName - false, // commitOnly - false, // skipReopen - false, // showDialog - ) - } - - @ScriptFunction(docBundlePrefix = "DesignerProjectExtensions") - @UnsafeExtension - fun update() { - (context.frame as IgnitionDesigner).updateProject() - } -} diff --git a/designer/bin/main/org/imdc/extensions/designer/DesignerProjectExtensions.properties b/designer/bin/main/org/imdc/extensions/designer/DesignerProjectExtensions.properties deleted file mode 100644 index c5ac1cc..0000000 --- a/designer/bin/main/org/imdc/extensions/designer/DesignerProjectExtensions.properties +++ /dev/null @@ -1,6 +0,0 @@ -getProject.desc=Retrieves the current project. -getProject.returns=The current project as a DesignableProject instance. -update.desc=Pulls in external changes made to this project from the gateway. -update.returns=None -save.desc=Programmatically invokes the save action. -save.returns=None diff --git a/gateway/bin/main/org/imdc/extensions/gateway/GatewayHook.kt b/gateway/bin/main/org/imdc/extensions/gateway/GatewayHook.kt deleted file mode 100644 index 36cffab..0000000 --- a/gateway/bin/main/org/imdc/extensions/gateway/GatewayHook.kt +++ /dev/null @@ -1,65 +0,0 @@ -package org.imdc.extensions.gateway - -import com.inductiveautomation.ignition.common.BundleUtil -import com.inductiveautomation.ignition.common.expressions.ExpressionFunctionManager -import com.inductiveautomation.ignition.common.licensing.LicenseState -import com.inductiveautomation.ignition.common.script.ScriptManager -import com.inductiveautomation.ignition.gateway.model.AbstractGatewayModuleHook -import com.inductiveautomation.ignition.gateway.model.GatewayContext -import org.imdc.extensions.common.DatasetExtensions -import org.imdc.extensions.common.ExtensionDocProvider -import org.imdc.extensions.common.PyDatasetBuilder -import org.imdc.extensions.common.UtilitiesExtensions -import org.imdc.extensions.common.addPropertyBundle -import org.imdc.extensions.common.expressions.IsAvailableFunction -import org.imdc.extensions.common.expressions.LogicalPredicate.Companion.registerLogicFunctions - -@Suppress("unused") -class GatewayHook : AbstractGatewayModuleHook() { - private lateinit var context: GatewayContext - - override fun setup(context: GatewayContext) { - this.context = context - - BundleUtil.get().apply { - addPropertyBundle() - addPropertyBundle() - addPropertyBundle() - addPropertyBundle() - } - - PyDatasetBuilder.register() - - context.webResourceManager.addServlet(HistoryServlet.PATH, HistoryServlet::class.java) - } - - override fun startup(activationState: LicenseState) = Unit - - override fun shutdown() { - PyDatasetBuilder.unregister() - context.webResourceManager.removeServlet(HistoryServlet.PATH) - } - - override fun initializeScriptManager(manager: ScriptManager) { - manager.apply { - addScriptModule("system.dataset", DatasetExtensions, ExtensionDocProvider) - addScriptModule("system.util", UtilitiesExtensions(context), ExtensionDocProvider) - addScriptModule("system.project", GatewayProjectExtensions(context), ExtensionDocProvider) - addScriptModule("system.tag", GatewayTagExtensions(context), ExtensionDocProvider) - } - } - - override fun configureFunctionFactory(factory: ExpressionFunctionManager) { - factory.apply { - addFunction( - IsAvailableFunction.NAME, - IsAvailableFunction.CATEGORY, - IsAvailableFunction(), - ) - registerLogicFunctions() - } - } - - override fun isFreeModule() = true - override fun isMakerEditionCompatible() = true -} diff --git a/gateway/bin/main/org/imdc/extensions/gateway/GatewayProjectExtensions.kt b/gateway/bin/main/org/imdc/extensions/gateway/GatewayProjectExtensions.kt deleted file mode 100644 index 69950a2..0000000 --- a/gateway/bin/main/org/imdc/extensions/gateway/GatewayProjectExtensions.kt +++ /dev/null @@ -1,24 +0,0 @@ -package org.imdc.extensions.gateway - -import com.inductiveautomation.ignition.common.project.RuntimeProject -import com.inductiveautomation.ignition.common.script.ScriptContext -import com.inductiveautomation.ignition.common.script.hints.ScriptArg -import com.inductiveautomation.ignition.common.script.hints.ScriptFunction -import com.inductiveautomation.ignition.gateway.model.GatewayContext -import org.imdc.extensions.common.ProjectExtensions -import org.python.core.Py - -class GatewayProjectExtensions(private val context: GatewayContext) : ProjectExtensions { - @ScriptFunction(docBundlePrefix = "GatewayProjectExtensions") - override fun getProject(): RuntimeProject { - val defaultProject = ScriptContext.defaultProject() ?: throw Py.EnvironmentError("No context project populated") - return requireNotNull(getProject(defaultProject)) { "No such project $defaultProject" } - } - - @ScriptFunction(docBundlePrefix = "GatewayProjectExtensions") - fun getProject( - @ScriptArg("project", optional = true) project: String, - ): RuntimeProject? { - return context.projectManager.getProject(project).orElse(null) - } -} diff --git a/gateway/bin/main/org/imdc/extensions/gateway/GatewayProjectExtensions.properties b/gateway/bin/main/org/imdc/extensions/gateway/GatewayProjectExtensions.properties deleted file mode 100644 index 4be8f31..0000000 --- a/gateway/bin/main/org/imdc/extensions/gateway/GatewayProjectExtensions.properties +++ /dev/null @@ -1,3 +0,0 @@ -getProject.desc=Retrieves a project by name, or None. -getProject.param.project=The name of the project to retrieve. Defaults to the current project, if known. -getProject.returns=The specified project as a RuntimeProject instance. diff --git a/gateway/bin/main/org/imdc/extensions/gateway/GatewayTagExtensions.kt b/gateway/bin/main/org/imdc/extensions/gateway/GatewayTagExtensions.kt deleted file mode 100644 index d8b3509..0000000 --- a/gateway/bin/main/org/imdc/extensions/gateway/GatewayTagExtensions.kt +++ /dev/null @@ -1,18 +0,0 @@ -package org.imdc.extensions.gateway - -import com.inductiveautomation.ignition.common.tags.config.TagConfigurationModel -import com.inductiveautomation.ignition.common.tags.model.TagPath -import com.inductiveautomation.ignition.gateway.model.GatewayContext -import org.imdc.extensions.common.TagExtensions -import org.python.core.Py - -class GatewayTagExtensions(private val context: GatewayContext) : TagExtensions() { - override fun getConfigurationImpl(basePath: TagPath, recursive: Boolean): List { - val provider = ( - context.tagManager.getTagProvider(basePath.source) - ?: throw Py.ValueError("No such tag provider ${basePath.source}") - ) - - return provider.getTagConfigsAsync(listOf(basePath), recursive, true).get() - } -} diff --git a/gateway/bin/main/org/imdc/extensions/gateway/HistoryServlet.kt b/gateway/bin/main/org/imdc/extensions/gateway/HistoryServlet.kt deleted file mode 100644 index 725639f..0000000 --- a/gateway/bin/main/org/imdc/extensions/gateway/HistoryServlet.kt +++ /dev/null @@ -1,162 +0,0 @@ -package org.imdc.extensions.gateway - -import com.inductiveautomation.ignition.common.QualifiedPathUtils -import com.inductiveautomation.ignition.common.StreamingDatasetWriter -import com.inductiveautomation.ignition.common.gson.stream.JsonWriter -import com.inductiveautomation.ignition.common.model.values.QualityCode -import com.inductiveautomation.ignition.common.sqltags.history.AggregationMode -import com.inductiveautomation.ignition.common.sqltags.history.BasicTagHistoryQueryParams -import com.inductiveautomation.ignition.common.sqltags.history.ReturnFormat -import com.inductiveautomation.ignition.common.util.LoggerEx -import com.inductiveautomation.ignition.gateway.model.GatewayContext -import org.apache.http.entity.ContentType -import java.io.PrintWriter -import java.text.SimpleDateFormat -import java.time.Instant -import java.time.format.DateTimeFormatter -import java.time.temporal.ChronoUnit -import java.util.Date -import javax.servlet.http.HttpServlet -import javax.servlet.http.HttpServletRequest -import javax.servlet.http.HttpServletResponse - -class HistoryServlet : HttpServlet() { - private lateinit var context: GatewayContext - - override fun init() { - context = servletContext.getAttribute(GatewayContext.SERVLET_CONTEXT_KEY) as GatewayContext - } - - override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { - resp.contentType = ContentType.APPLICATION_JSON.toString() - resp.writer.use { writer -> - val historyQuery: BasicTagHistoryQueryParams = - try { - val paths = - req.getParameterValues("path") - ?: throw IllegalArgumentException("Must specify at least one path") - val startDate = - req.getParameter("startDate")?.toDate() ?: Date.from(Instant.now().minus(8, ChronoUnit.HOURS)) - val endDate = req.getParameter("endDate")?.toDate() ?: Date() - val returnSize = req.getParameter("returnSize")?.toInt() ?: -1 - val aggregationMode = - req.getParameter("aggregationMode")?.let(AggregationMode::valueOf) ?: AggregationMode.Average - val aliases = req.getParameter("aliases")?.split(',') - - BasicTagHistoryQueryParams( - paths.map(QualifiedPathUtils::toPathFromHistoricalString), - startDate, - endDate, - returnSize, - aggregationMode, - ReturnFormat.Wide, - aliases, - emptyList(), - ) - } catch (e: Exception) { - resp.status = HttpServletResponse.SC_BAD_REQUEST - e.printStackTrace(PrintWriter(writer)) - return - } - - try { - context.tagHistoryManager.queryHistory( - historyQuery, - StreamingJsonWriter( - JsonWriter(writer), - ), - ) - } catch (e: TrialExpiredException) { - resp.status = HttpServletResponse.SC_PAYMENT_REQUIRED - logger.error("Tag historian module reported trial expired", e) - } catch (e: Exception) { - resp.status = HttpServletResponse.SC_INTERNAL_SERVER_ERROR - logger.error("Unexpected exception writing JSON content to servlet", e) - } - } - } - - class TrialExpiredException : Exception() - - class StreamingJsonWriter(private val jsonWriter: JsonWriter) : StreamingDatasetWriter { - private lateinit var names: Array - private lateinit var types: Array> - - private val test = DateTimeFormatter.ISO_INSTANT - - override fun initialize( - columnNames: Array, - columnTypes: Array>, - hasQuality: Boolean, - expectedRows: Int, - ) { - this.names = columnNames - this.types = columnTypes - - jsonWriter.beginArray() - } - - override fun write(data: Array, quality: Array): Unit = jsonWriter.run { - if (quality.any { it.`is`(QualityCode.Bad_TrialExpired) }) { - throw TrialExpiredException() - } - - writeObject { - for (index in data.indices) { - name(names[index]) - when (val value = data[index]) { - is Number -> { - when (types[index]) { - Float::class.java, Double::class.java -> value(value.toDouble()) - else -> value(value.toLong()) - } - } - - is Date -> { - value(test.format(value.toInstant())) - } - - is String -> value(value) - is Boolean -> value(value) - - null -> nullValue() - } - } - } - } - - override fun finish() { - jsonWriter.endArray() - } - - override fun finishWithError(exception: java.lang.Exception) = throw exception - - private inline fun JsonWriter.writeObject(block: JsonWriter.() -> Unit) { - beginObject() - block() - endObject() - } - } - - companion object { - const val PATH = "history-extension" - - private val logger = LoggerEx.newBuilder().build(HistoryServlet::class.java) - - private val parsingStrategies = listOf<(String) -> Date>( - SimpleDateFormat.getDateTimeInstance()::parse, - SimpleDateFormat.getInstance()::parse, - { Date(it.toLong()) }, - ) - - private fun String.toDate(): Date? { - return parsingStrategies.firstNotNullOfOrNull { strategy -> - try { - strategy.invoke(this) - } catch (e: Exception) { - null - } - } - } - } -} From 78e52d0f2ba5705f5220143ed42cc8d3ea0fbcba Mon Sep 17 00:00:00 2001 From: Paul Griffith Date: Tue, 27 Jun 2023 14:49:50 -0700 Subject: [PATCH 06/14] Tweak changes + format --- .../extensions/common/DatasetExtensions.kt | 202 +++++++----------- .../org/imdc/extensions/common/Utilities.kt | 2 + .../common/DatasetExtensions.properties | 2 +- 3 files changed, 83 insertions(+), 123 deletions(-) diff --git a/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt b/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt index 8684099..f93735b 100644 --- a/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt +++ b/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt @@ -1,6 +1,7 @@ package org.imdc.extensions.common import com.inductiveautomation.ignition.common.Dataset +import com.inductiveautomation.ignition.common.PyUtilities import com.inductiveautomation.ignition.common.TypeUtilities import com.inductiveautomation.ignition.common.script.PyArgParser import com.inductiveautomation.ignition.common.script.builtin.KeywordArgs @@ -8,10 +9,8 @@ import com.inductiveautomation.ignition.common.script.hints.ScriptArg import com.inductiveautomation.ignition.common.script.hints.ScriptFunction import com.inductiveautomation.ignition.common.util.DatasetBuilder import com.inductiveautomation.ignition.common.xmlserialization.ClassNameResolver -import org.apache.poi.ss.usermodel.CellType.BOOLEAN -import org.apache.poi.ss.usermodel.CellType.FORMULA -import org.apache.poi.ss.usermodel.CellType.NUMERIC -import org.apache.poi.ss.usermodel.CellType.STRING +import org.apache.poi.ss.usermodel.Cell +import org.apache.poi.ss.usermodel.CellType import org.apache.poi.ss.usermodel.DateUtil import org.apache.poi.ss.usermodel.WorkbookFactory import org.python.core.Py @@ -21,15 +20,19 @@ import org.python.core.PyDictionary import org.python.core.PyFloat import org.python.core.PyFunction import org.python.core.PyInteger +import org.python.core.PyList import org.python.core.PyLong import org.python.core.PyObject import org.python.core.PyString +import org.python.core.PyStringMap import org.python.core.PyType import org.python.core.PyUnicode import java.io.File import java.math.BigDecimal import java.util.Date +import kotlin.jvm.optionals.getOrElse import kotlin.math.max +import kotlin.streams.asSequence object DatasetExtensions { @Suppress("unused") @@ -224,44 +227,37 @@ object DatasetExtensions { names = ["dataset", "filterNull"], types = [Dataset::class, Boolean::class], ) - fun toDict(args: Array, keywords: Array): PyDictionary { + fun toDict(args: Array, keywords: Array): PyStringMap { val parsedArgs = PyArgParser.parseArgs( args, keywords, - arrayOf( - "dataset", - "filterNull", - ), + arrayOf("dataset", "filterNull"), Array(2) { Any::class.java }, "toDict", ) val dataset = parsedArgs.requirePyObject("dataset").toJava() val filterNull = parsedArgs.getBoolean("filterNull").orElse(false) - val pyDict = PyDictionary() - for (column in 0 until dataset.columnCount) { - val rowArray = if (filterNull) { - (0 until dataset.rowCount) - .mapNotNull { row -> - dataset.getValueAt(row, column) - } - .toList() - } else { - (0 until dataset.rowCount) - .map { row -> - dataset.getValueAt(row, column) - } - .toList() - } - pyDict[dataset.columnNames[column]] = rowArray - } - return pyDict + return PyStringMap( + dataset.columnIndices.associate { col -> + dataset.getColumnName(col) to PyList( + buildList { + for (row in dataset.rowIndices) { + val value = dataset[row, col] + if (value != null || !filterNull) { + add(value) + } + } + }, + ) + }, + ) } @Suppress("unused") @ScriptFunction(docBundlePrefix = "DatasetExtensions") @KeywordArgs( - names = ["input", "headerRow", "sheetNumber", "firstRow", "lastRow", "firstColumn", "lastColumn", "stringColumns"], - types = [ByteArray::class, Int::class, Int::class, Int::class, Int::class, Int::class, Int::class, Array::class], + names = ["input", "headerRow", "sheetNumber", "firstRow", "lastRow", "firstColumn", "lastColumn", "typeOverrides"], + types = [ByteArray::class, Int::class, Int::class, Int::class, Int::class, Int::class, Int::class, PyDictionary::class], ) fun fromExcel(args: Array, keywords: Array): Dataset { val parsedArgs = PyArgParser.parseArgs( @@ -275,15 +271,20 @@ object DatasetExtensions { "lastRow", "firstColumn", "lastColumn", - "stringColumns" + "typeOverrides", ), Array(8) { Any::class.java }, "fromExcel", ) - //val tempstringColumn = parsedArgs.getInteger("stringColumn").orElse(0) - val stringColumns = parsedArgs.getPyObject("stringColumns").map { it.toJava>() }.orElse(arrayOf()) - //val stringColumn = tempstringColumn != 0 + val typeOverrides = parsedArgs.getPyObject("typeColumns") + .map(PyUtilities::streamEntries) + .map { stream -> + stream.asSequence().associate { (key, value) -> + key.asIndex() to value.asJavaClass() + } + }.getOrElse { emptyMap() } + when (val input = parsedArgs.requirePyObject("input").toJava()) { is String -> WorkbookFactory.create(File(input)) is ByteArray -> WorkbookFactory.create(input.inputStream().buffered()) @@ -306,7 +307,8 @@ object DatasetExtensions { } val columnRow = sheet.getRow(if (headerRow >= 0) headerRow else firstRow) - val firstColumn = parsedArgs.getInteger("firstColumn").orElseGet { columnRow.firstCellNum.toInt() } + val firstColumn = + parsedArgs.getInteger("firstColumn").orElseGet { columnRow.firstCellNum.toInt() } val lastColumn = parsedArgs.getInteger("lastColumn").map { it + 1 }.orElseGet { columnRow.lastCellNum.toInt() } if (firstColumn >= lastColumn) { @@ -335,110 +337,66 @@ object DatasetExtensions { } val row = sheet.getRow(i) - + val rowValues = Array(columnCount) { j -> - val cell = row.getCell(j + firstColumn) - val stringColumn = j in stringColumns - if(cell == null){ + val cell: Cell? = row.getCell(j + firstColumn) + val typeOverride = typeOverrides[j] + if (typeOverride != null) { if (!typesSet) { - if (stringColumn) { - columnTypes.add(String::class.java) - } - else{ - columnTypes.add(Any::class.java) - } + columnTypes.add(typeOverride) } - null - } - else{ - val cellType = cell.getCellType().toString() - - if(cellType == "NUMERIC") { - if (DateUtil.isCellDateFormatted(cell)) { - if (!typesSet) { - if(stringColumn){ - columnTypes.add(String::class.java) - } - else{ - columnTypes.add(Date::class.java) - } - - } - val tempCell = cell.dateCellValue - if(stringColumn){ - tempCell.toString() - } - else{ - tempCell - } - } else { - val numericCellValue = cell.numericCellValue - if (BigDecimal(numericCellValue).scale() == 0) { + when { + cell == null -> null + typeOverride.isAssignableFrom() -> cell.dateCellValue + typeOverride.isAssignableFrom() -> cell.booleanCellValue + typeOverride.isAssignableFrom() -> cell.stringCellValue + typeOverride.isAssignableFrom() -> cell.numericCellValue.toInt() + typeOverride.isAssignableFrom() -> cell.numericCellValue.toLong() + typeOverride.isAssignableFrom() -> cell.numericCellValue + else -> throw Py.TypeError("Unable to retrieve R${cell.row.rowNum + 1}C${cell.columnIndex + 1} as a ${typeOverride.simpleName}") + } + } else { + when (cell?.cellType) { + CellType.NUMERIC -> { + if (DateUtil.isCellDateFormatted(cell)) { if (!typesSet) { - if(stringColumn){ - columnTypes.add(String::class.java) - } - else{ - columnTypes.add(Int::class.javaObjectType) - } - } - if(stringColumn){ - numericCellValue.toInt().toString() + columnTypes.add(Date::class.java) } - else{ - numericCellValue.toInt() - } + cell.dateCellValue } else { - if (!typesSet) { - if(stringColumn){ - columnTypes.add(String::class.java) + val numericCellValue = cell.numericCellValue + if (BigDecimal(numericCellValue).scale() == 0) { + if (!typesSet) { + columnTypes.add(Int::class.javaObjectType) } - else{ + numericCellValue.toInt() + } else { + if (!typesSet) { columnTypes.add(Double::class.javaObjectType) - } - } - if(stringColumn){ - numericCellValue.toString() - } - else{ + } numericCellValue } } } - } - else if(cellType == "STRING"){ - if (!typesSet) { - columnTypes.add(String::class.java) + CellType.STRING -> { + if (!typesSet) { + columnTypes.add(String::class.java) + } + cell.stringCellValue } - cell.stringCellValue - } - else if(cellType == "BOOLEAN"){ - if (!typesSet) { - columnTypes.add(Boolean::class.javaObjectType) - } - if(stringColumn){ - cell.booleanCellValue.toString() - } - else{ + CellType.BOOLEAN -> { + if (!typesSet) { + columnTypes.add(Boolean::class.javaObjectType) + } cell.booleanCellValue - } - } + } - else{ - if (!typesSet) { - if (stringColumn) { - columnTypes.add(String::class.java) - } - else{ + else -> { + if (!typesSet) { columnTypes.add(Any::class.java) } - } - if (stringColumn) { - null.toString() - } - else{ null } } @@ -519,8 +477,6 @@ object DatasetExtensions { } } - private val classNameResolver = ClassNameResolver.createBasic() - @ScriptFunction(docBundlePrefix = "DatasetExtensions") @KeywordArgs( names = ["**columns"], @@ -539,7 +495,9 @@ object DatasetExtensions { return DatasetBuilder.newBuilder().colNames(colNames).colTypes(colTypes) } - fun PyObject.asJavaClass(): Class<*>? = when (this) { + private val classNameResolver = ClassNameResolver.createBasic() + + private fun PyObject.asJavaClass(): Class<*>? = when (this) { is PyBaseString -> classNameResolver.classForName(asString()) !is PyType -> throw ClassCastException() PyString.TYPE, PyUnicode.TYPE -> String::class.java diff --git a/common/src/main/kotlin/org/imdc/extensions/common/Utilities.kt b/common/src/main/kotlin/org/imdc/extensions/common/Utilities.kt index 16981f0..efee39c 100644 --- a/common/src/main/kotlin/org/imdc/extensions/common/Utilities.kt +++ b/common/src/main/kotlin/org/imdc/extensions/common/Utilities.kt @@ -53,3 +53,5 @@ inline fun BundleUtil.addPropertyBundle() { inline fun Method.getAnnotation(): T? { return getAnnotation(T::class.java) } + +inline fun Class<*>.isAssignableFrom(): Boolean = this.isAssignableFrom(T::class.java) diff --git a/common/src/main/resources/org/imdc/extensions/common/DatasetExtensions.properties b/common/src/main/resources/org/imdc/extensions/common/DatasetExtensions.properties index 2c0518a..5ac7dfe 100644 --- a/common/src/main/resources/org/imdc/extensions/common/DatasetExtensions.properties +++ b/common/src/main/resources/org/imdc/extensions/common/DatasetExtensions.properties @@ -28,7 +28,7 @@ fromExcel.param.firstRow=The first row (zero-indexed) in the Excel document to r fromExcel.param.lastRow=The last row (zero-indexed) in the Excel document to retrieve data from. If not supplied, the last non-empty row will be used. fromExcel.param.firstColumn=The first column (zero-indexed) in the Excel document to retrieve data from. If not supplied, the first non-empty column will be used. fromExcel.param.lastColumn=The last column (zero-indexed) in the Excel document to retrieve data from. If not supplied, the last non-empty column will be used. -fromExcel.param.stringColumns=List of columns to String. +fromExcel.param.typeOverrides=A dictionary of column index: column types, using the same semantics as system.dataset.builder for type specification. fromExcel.returns=A Dataset created from the Excel document. Types are assumed based on the first row of input data. equals.desc=Compares two datasets for structural equality. From d5a187548aac327ad4346bb2465faffc0ebf2bff Mon Sep 17 00:00:00 2001 From: Paul Griffith Date: Tue, 27 Jun 2023 14:55:37 -0700 Subject: [PATCH 07/14] Update function visibility --- .../main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt b/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt index f93735b..ffa46e5 100644 --- a/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt +++ b/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt @@ -497,7 +497,7 @@ object DatasetExtensions { private val classNameResolver = ClassNameResolver.createBasic() - private fun PyObject.asJavaClass(): Class<*>? = when (this) { + internal fun PyObject.asJavaClass(): Class<*>? = when (this) { is PyBaseString -> classNameResolver.classForName(asString()) !is PyType -> throw ClassCastException() PyString.TYPE, PyUnicode.TYPE -> String::class.java From fe5dc7c047b42337d58c87f9a1ecf09dfcfb9176 Mon Sep 17 00:00:00 2001 From: JosephTLockwood Date: Wed, 28 Jun 2023 12:45:01 -0400 Subject: [PATCH 08/14] Fix DatasetExtensions typeOverrides and add test case in DatasetExtensionsTests --- .../extensions/common/DatasetExtensions.kt | 2 +- .../common/DatasetExtensionsTests.kt | 41 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt b/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt index ffa46e5..2299dde 100644 --- a/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt +++ b/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt @@ -277,7 +277,7 @@ object DatasetExtensions { "fromExcel", ) - val typeOverrides = parsedArgs.getPyObject("typeColumns") + val typeOverrides = parsedArgs.getPyObject("typeOverrides") .map(PyUtilities::streamEntries) .map { stream -> stream.asSequence().associate { (key, value) -> diff --git a/common/src/test/kotlin/org/imdc/extensions/common/DatasetExtensionsTests.kt b/common/src/test/kotlin/org/imdc/extensions/common/DatasetExtensionsTests.kt index 2cda029..127fb0f 100644 --- a/common/src/test/kotlin/org/imdc/extensions/common/DatasetExtensionsTests.kt +++ b/common/src/test/kotlin/org/imdc/extensions/common/DatasetExtensionsTests.kt @@ -7,6 +7,7 @@ import io.kotest.assertions.withClue import io.kotest.matchers.shouldBe import org.imdc.extensions.common.DatasetExtensions.printDataset import org.python.core.Py +import org.python.core.PyDictionary import java.awt.Color import java.util.Date import kotlin.io.path.createTempFile @@ -33,6 +34,22 @@ class DatasetExtensionsTests : JythonTest( globals["xlsxBytes"] = excelSample globals["xlsxFile"] = tempXlsx.toString() + val excelSample2 = + DatasetExtensionsTests::class.java.getResourceAsStream("sample2.xlsx")!!.readAllBytes() + val tempXlsx2 = createTempFile(suffix = "xlsx").also { + it.writeBytes(excelSample2) + it.toFile().deleteOnExit() + } + globals["xlsxFile2"] = tempXlsx2.toString() + globals["xlsxBytes2"] = excelSample2 + val pyDictionary = PyDictionary() + + // Add key-value pairs to the dictionary + pyDictionary["Country"] = "String" + pyDictionary["Discount Band"] = "String" + + globals["pyDictionary"] = pyDictionary + val xlsSample = DatasetExtensionsTests::class.java.getResourceAsStream("sample.xls")!!.readAllBytes() val tempXls = createTempFile(suffix = "xls").also { @@ -239,6 +256,30 @@ class DatasetExtensionsTests : JythonTest( ) } } + test("With String OverRide") { + eval("utils.fromExcel(xlsxBytes2, typeOverrides=pyDictionary)").asClue { + it.rowCount shouldBe 99 + it.columnCount shouldBe 16 + it.columnNames shouldBe listOf( + "Segment", + "Country", + "Product", + "Discount Band", + "Units Sold", + "Manufacturing Price", + "Sale Price", + "Gross Sales", + "Discounts", + "Sales", + "COGS", + "Profit", + "Date", + "Month Number", + "Month Name", + "Year", + ) + } + } test("First row") { eval("utils.fromExcel(xlsxBytes, headerRow=0, firstRow=50)").asClue { it.rowCount shouldBe 50 From cd07728c04673406f16008ecaf745e0a96918768 Mon Sep 17 00:00:00 2001 From: JosephTLockwood Date: Wed, 28 Jun 2023 12:45:46 -0400 Subject: [PATCH 09/14] Fix DatasetExtensions typeOverrides and add test case in DatasetExtensionsTests --- .../org/imdc/extensions/common/sample2.xlsx | Bin 0 -> 18793 bytes local.properties | 8 ++++++++ 2 files changed, 8 insertions(+) create mode 100644 common/src/test/resources/org/imdc/extensions/common/sample2.xlsx create mode 100644 local.properties diff --git a/common/src/test/resources/org/imdc/extensions/common/sample2.xlsx b/common/src/test/resources/org/imdc/extensions/common/sample2.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..292d262149f3582970734b0d0fdb86bf1125995c GIT binary patch literal 18793 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 KC}$-9oc$k|e0Mnj literal 0 HcmV?d00001 diff --git a/local.properties b/local.properties new file mode 100644 index 0000000..d6deb09 --- /dev/null +++ b/local.properties @@ -0,0 +1,8 @@ +## This file must *NOT* be checked into Version Control Systems, +# as it contains information specific to your local configuration. +# +# Location of the SDK. This is only used by Gradle. +# For customization when using a Version Control System, please read the +# header note. +#Wed Jun 28 09:55:15 EDT 2023 +sdk.dir=C\:\\Users\\jlockwood\\AppData\\Local\\Android\\Sdk From 971314dc58318245d75ca5f8848f9c7f270c1230 Mon Sep 17 00:00:00 2001 From: JosephTLockwood Date: Wed, 28 Jun 2023 13:10:39 -0400 Subject: [PATCH 10/14] Change test case to PyStringMap --- .../org/imdc/extensions/common/DatasetExtensionsTests.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common/src/test/kotlin/org/imdc/extensions/common/DatasetExtensionsTests.kt b/common/src/test/kotlin/org/imdc/extensions/common/DatasetExtensionsTests.kt index 127fb0f..07bb766 100644 --- a/common/src/test/kotlin/org/imdc/extensions/common/DatasetExtensionsTests.kt +++ b/common/src/test/kotlin/org/imdc/extensions/common/DatasetExtensionsTests.kt @@ -8,6 +8,7 @@ import io.kotest.matchers.shouldBe import org.imdc.extensions.common.DatasetExtensions.printDataset import org.python.core.Py import org.python.core.PyDictionary +import org.python.core.PyStringMap import java.awt.Color import java.util.Date import kotlin.io.path.createTempFile @@ -42,7 +43,7 @@ class DatasetExtensionsTests : JythonTest( } globals["xlsxFile2"] = tempXlsx2.toString() globals["xlsxBytes2"] = excelSample2 - val pyDictionary = PyDictionary() + val pyDictionary = PyStringMap() // Add key-value pairs to the dictionary pyDictionary["Country"] = "String" From 388f74c1903c264aa525d4071df057a37ff46129 Mon Sep 17 00:00:00 2001 From: JosephTLockwood Date: Wed, 28 Jun 2023 13:20:33 -0400 Subject: [PATCH 11/14] Closer!!! Gets back table just returned wrong dataset --- .../kotlin/org/imdc/extensions/common/DatasetExtensions.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt b/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt index 2299dde..09cf98a 100644 --- a/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt +++ b/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt @@ -257,7 +257,7 @@ object DatasetExtensions { @ScriptFunction(docBundlePrefix = "DatasetExtensions") @KeywordArgs( names = ["input", "headerRow", "sheetNumber", "firstRow", "lastRow", "firstColumn", "lastColumn", "typeOverrides"], - types = [ByteArray::class, Int::class, Int::class, Int::class, Int::class, Int::class, Int::class, PyDictionary::class], + types = [ByteArray::class, Int::class, Int::class, Int::class, Int::class, Int::class, Int::class, PyStringMap::class], ) fun fromExcel(args: Array, keywords: Array): Dataset { val parsedArgs = PyArgParser.parseArgs( @@ -280,8 +280,9 @@ object DatasetExtensions { val typeOverrides = parsedArgs.getPyObject("typeOverrides") .map(PyUtilities::streamEntries) .map { stream -> - stream.asSequence().associate { (key, value) -> - key.asIndex() to value.asJavaClass() + var index = 0 + stream.asSequence().associate { (_, value) -> + index++ to value.asJavaClass() } }.getOrElse { emptyMap() } From 441b6cd98b1d94d136219e96b45ca69b7ec4df14 Mon Sep 17 00:00:00 2001 From: JosephTLockwood Date: Wed, 28 Jun 2023 13:21:18 -0400 Subject: [PATCH 12/14] Closer!!! Gets back dataset just returned wrong size dataset --- .../main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt | 1 - .../kotlin/org/imdc/extensions/common/DatasetExtensionsTests.kt | 1 - 2 files changed, 2 deletions(-) diff --git a/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt b/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt index 09cf98a..ffde1a9 100644 --- a/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt +++ b/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt @@ -16,7 +16,6 @@ import org.apache.poi.ss.usermodel.WorkbookFactory import org.python.core.Py import org.python.core.PyBaseString import org.python.core.PyBoolean -import org.python.core.PyDictionary import org.python.core.PyFloat import org.python.core.PyFunction import org.python.core.PyInteger diff --git a/common/src/test/kotlin/org/imdc/extensions/common/DatasetExtensionsTests.kt b/common/src/test/kotlin/org/imdc/extensions/common/DatasetExtensionsTests.kt index 07bb766..9f2a420 100644 --- a/common/src/test/kotlin/org/imdc/extensions/common/DatasetExtensionsTests.kt +++ b/common/src/test/kotlin/org/imdc/extensions/common/DatasetExtensionsTests.kt @@ -7,7 +7,6 @@ import io.kotest.assertions.withClue import io.kotest.matchers.shouldBe import org.imdc.extensions.common.DatasetExtensions.printDataset import org.python.core.Py -import org.python.core.PyDictionary import org.python.core.PyStringMap import java.awt.Color import java.util.Date From 9a111d2152f0a254c26fc78f656ffa05d3ee0006 Mon Sep 17 00:00:00 2001 From: JosephTLockwood Date: Wed, 28 Jun 2023 13:26:17 -0400 Subject: [PATCH 13/14] Working with test case. --- .../kotlin/org/imdc/extensions/common/DatasetExtensionsTests.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/test/kotlin/org/imdc/extensions/common/DatasetExtensionsTests.kt b/common/src/test/kotlin/org/imdc/extensions/common/DatasetExtensionsTests.kt index 9f2a420..0e51dbf 100644 --- a/common/src/test/kotlin/org/imdc/extensions/common/DatasetExtensionsTests.kt +++ b/common/src/test/kotlin/org/imdc/extensions/common/DatasetExtensionsTests.kt @@ -257,7 +257,7 @@ class DatasetExtensionsTests : JythonTest( } } test("With String OverRide") { - eval("utils.fromExcel(xlsxBytes2, typeOverrides=pyDictionary)").asClue { + eval("utils.fromExcel(xlsxBytes2, headerRow=0, typeOverrides=pyDictionary)").asClue { it.rowCount shouldBe 99 it.columnCount shouldBe 16 it.columnNames shouldBe listOf( From fc869c6c5c5d68dcb37e63d9e082caec81c46130 Mon Sep 17 00:00:00 2001 From: Paul Griffith Date: Wed, 28 Jun 2023 10:57:26 -0700 Subject: [PATCH 14/14] Some cleanups --- .gitignore | 2 + .../extensions/common/DatasetExtensions.kt | 98 +++++++++---------- .../common/DatasetExtensions.properties | 2 +- .../common/DatasetExtensionsTests.kt | 42 ++++---- .../org/imdc/extensions/common/JythonTest.kt | 4 +- local.properties | 8 -- 6 files changed, 76 insertions(+), 80 deletions(-) delete mode 100644 local.properties diff --git a/.gitignore b/.gitignore index 6dd5458..1a00f40 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .idea/ **/build/ .gradle/ + +local.properties diff --git a/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt b/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt index ffde1a9..5b31d60 100644 --- a/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt +++ b/common/src/main/kotlin/org/imdc/extensions/common/DatasetExtensions.kt @@ -279,9 +279,8 @@ object DatasetExtensions { val typeOverrides = parsedArgs.getPyObject("typeOverrides") .map(PyUtilities::streamEntries) .map { stream -> - var index = 0 - stream.asSequence().associate { (_, value) -> - index++ to value.asJavaClass() + stream.asSequence().associate { (pyKey, value) -> + pyKey.asIndex() to value.asJavaClass() } }.getOrElse { emptyMap() } @@ -338,69 +337,66 @@ object DatasetExtensions { val row = sheet.getRow(i) - val rowValues = Array(columnCount) { j -> + val rowValues = Array(columnCount) { j -> val cell: Cell? = row.getCell(j + firstColumn) - val typeOverride = typeOverrides[j] - if (typeOverride != null) { - if (!typesSet) { - columnTypes.add(typeOverride) - } - when { - cell == null -> null - typeOverride.isAssignableFrom() -> cell.dateCellValue - typeOverride.isAssignableFrom() -> cell.booleanCellValue - typeOverride.isAssignableFrom() -> cell.stringCellValue - typeOverride.isAssignableFrom() -> cell.numericCellValue.toInt() - typeOverride.isAssignableFrom() -> cell.numericCellValue.toLong() - typeOverride.isAssignableFrom() -> cell.numericCellValue - else -> throw Py.TypeError("Unable to retrieve R${cell.row.rowNum + 1}C${cell.columnIndex + 1} as a ${typeOverride.simpleName}") - } - } else { - when (cell?.cellType) { - CellType.NUMERIC -> { - if (DateUtil.isCellDateFormatted(cell)) { + + val actualValue: Any? = when (cell?.cellType) { + CellType.NUMERIC -> { + if (DateUtil.isCellDateFormatted(cell)) { + if (!typesSet) { + columnTypes.add(Date::class.java) + } + cell.dateCellValue + } else { + val numericCellValue = cell.numericCellValue + if (BigDecimal(numericCellValue).scale() == 0) { if (!typesSet) { - columnTypes.add(Date::class.java) + columnTypes.add(Int::class.javaObjectType) } - cell.dateCellValue + numericCellValue.toInt() } else { - val numericCellValue = cell.numericCellValue - if (BigDecimal(numericCellValue).scale() == 0) { - if (!typesSet) { - columnTypes.add(Int::class.javaObjectType) - } - numericCellValue.toInt() - } else { - if (!typesSet) { - columnTypes.add(Double::class.javaObjectType) - } - numericCellValue + if (!typesSet) { + columnTypes.add(Double::class.javaObjectType) } + numericCellValue } } + } - CellType.STRING -> { - if (!typesSet) { - columnTypes.add(String::class.java) - } - cell.stringCellValue + CellType.STRING -> { + if (!typesSet) { + columnTypes.add(String::class.java) } + cell.stringCellValue + } - CellType.BOOLEAN -> { - if (!typesSet) { - columnTypes.add(Boolean::class.javaObjectType) - } - cell.booleanCellValue + CellType.BOOLEAN -> { + if (!typesSet) { + columnTypes.add(Boolean::class.javaObjectType) } + cell.booleanCellValue + } - else -> { - if (!typesSet) { - columnTypes.add(Any::class.java) - } - null + else -> { + if (!typesSet) { + columnTypes.add(Any::class.java) } + null } } + val typeOverride = typeOverrides[j] + if (typeOverride != null) { + if (!typesSet) { + columnTypes[j] = typeOverride + } + try { + TypeUtilities.coerceGeneric(actualValue, typeOverride) + } catch (e: ClassCastException) { + throw Py.TypeError(e.message) + } + } else { + actualValue + } } if (!typesSet) { diff --git a/common/src/main/resources/org/imdc/extensions/common/DatasetExtensions.properties b/common/src/main/resources/org/imdc/extensions/common/DatasetExtensions.properties index 5ac7dfe..b7a5530 100644 --- a/common/src/main/resources/org/imdc/extensions/common/DatasetExtensions.properties +++ b/common/src/main/resources/org/imdc/extensions/common/DatasetExtensions.properties @@ -28,7 +28,7 @@ fromExcel.param.firstRow=The first row (zero-indexed) in the Excel document to r fromExcel.param.lastRow=The last row (zero-indexed) in the Excel document to retrieve data from. If not supplied, the last non-empty row will be used. fromExcel.param.firstColumn=The first column (zero-indexed) in the Excel document to retrieve data from. If not supplied, the first non-empty column will be used. fromExcel.param.lastColumn=The last column (zero-indexed) in the Excel document to retrieve data from. If not supplied, the last non-empty column will be used. -fromExcel.param.typeOverrides=A dictionary of column index: column types, using the same semantics as system.dataset.builder for type specification. +fromExcel.param.typeOverrides=A dictionary of column index: column types, using the same semantics as system.dataset.builder for types. fromExcel.returns=A Dataset created from the Excel document. Types are assumed based on the first row of input data. equals.desc=Compares two datasets for structural equality. diff --git a/common/src/test/kotlin/org/imdc/extensions/common/DatasetExtensionsTests.kt b/common/src/test/kotlin/org/imdc/extensions/common/DatasetExtensionsTests.kt index 0e51dbf..b7b2dce 100644 --- a/common/src/test/kotlin/org/imdc/extensions/common/DatasetExtensionsTests.kt +++ b/common/src/test/kotlin/org/imdc/extensions/common/DatasetExtensionsTests.kt @@ -4,14 +4,12 @@ import com.inductiveautomation.ignition.common.BasicDataset import com.inductiveautomation.ignition.common.Dataset import com.inductiveautomation.ignition.common.util.DatasetBuilder import io.kotest.assertions.withClue +import io.kotest.engine.spec.tempfile import io.kotest.matchers.shouldBe import org.imdc.extensions.common.DatasetExtensions.printDataset import org.python.core.Py -import org.python.core.PyStringMap import java.awt.Color import java.util.Date -import kotlin.io.path.createTempFile -import kotlin.io.path.writeBytes @Suppress("PyUnresolvedReferences", "PyInterpreter") class DatasetExtensionsTests : JythonTest( @@ -27,34 +25,24 @@ class DatasetExtensionsTests : JythonTest( val excelSample = DatasetExtensionsTests::class.java.getResourceAsStream("sample.xlsx")!!.readAllBytes() - val tempXlsx = createTempFile(suffix = "xlsx").also { + val tempXlsx = tempfile(suffix = "xlsx").also { it.writeBytes(excelSample) - it.toFile().deleteOnExit() } globals["xlsxBytes"] = excelSample globals["xlsxFile"] = tempXlsx.toString() val excelSample2 = DatasetExtensionsTests::class.java.getResourceAsStream("sample2.xlsx")!!.readAllBytes() - val tempXlsx2 = createTempFile(suffix = "xlsx").also { + val tempXlsx2 = tempfile(suffix = "xlsx").also { it.writeBytes(excelSample2) - it.toFile().deleteOnExit() } globals["xlsxFile2"] = tempXlsx2.toString() globals["xlsxBytes2"] = excelSample2 - val pyDictionary = PyStringMap() - - // Add key-value pairs to the dictionary - pyDictionary["Country"] = "String" - pyDictionary["Discount Band"] = "String" - - globals["pyDictionary"] = pyDictionary val xlsSample = DatasetExtensionsTests::class.java.getResourceAsStream("sample.xls")!!.readAllBytes() - val tempXls = createTempFile(suffix = "xls").also { + val tempXls = tempfile(suffix = "xls").also { it.writeBytes(xlsSample) - it.toFile().deleteOnExit() } globals["xlsBytes"] = xlsSample globals["xlsFile"] = tempXls.toString() @@ -256,8 +244,8 @@ class DatasetExtensionsTests : JythonTest( ) } } - test("With String OverRide") { - eval("utils.fromExcel(xlsxBytes2, headerRow=0, typeOverrides=pyDictionary)").asClue { + test("With String Override") { + eval("utils.fromExcel(xlsxBytes2, headerRow=0, typeOverrides={2: 'str', 4: 'str'})").asClue { it.rowCount shouldBe 99 it.columnCount shouldBe 16 it.columnNames shouldBe listOf( @@ -278,6 +266,24 @@ class DatasetExtensionsTests : JythonTest( "Month Name", "Year", ) + it.columnTypes shouldBe listOf( + String::class.java, + Any::class.java, + String::class.java, + String::class.java, + String::class.java, + Int::class.javaObjectType, + Int::class.javaObjectType, + Int::class.javaObjectType, + Int::class.javaObjectType, + Int::class.javaObjectType, + Int::class.javaObjectType, + Int::class.javaObjectType, + Date::class.java, + Int::class.javaObjectType, + String::class.java, + String::class.java, + ) } } test("First row") { diff --git a/common/src/test/kotlin/org/imdc/extensions/common/JythonTest.kt b/common/src/test/kotlin/org/imdc/extensions/common/JythonTest.kt index 9144e89..6ffc4ed 100644 --- a/common/src/test/kotlin/org/imdc/extensions/common/JythonTest.kt +++ b/common/src/test/kotlin/org/imdc/extensions/common/JythonTest.kt @@ -19,7 +19,7 @@ import org.python.core.PyStringMap import org.python.core.PyType @Suppress("PyInterpreter") -abstract class JythonTest(init: (globals: PyStringMap) -> Unit) : FunSpec() { +abstract class JythonTest(init: FunSpec.(globals: PyStringMap) -> Unit) : FunSpec() { protected var globals: PyStringMap = PyStringMap() init { @@ -27,7 +27,7 @@ abstract class JythonTest(init: (globals: PyStringMap) -> Unit) : FunSpec() { object : BeforeEachListener { override suspend fun beforeEach(testCase: TestCase) { globals.clear() - init(globals) + init(this@JythonTest, globals) } }, ) diff --git a/local.properties b/local.properties deleted file mode 100644 index d6deb09..0000000 --- a/local.properties +++ /dev/null @@ -1,8 +0,0 @@ -## This file must *NOT* be checked into Version Control Systems, -# as it contains information specific to your local configuration. -# -# Location of the SDK. This is only used by Gradle. -# For customization when using a Version Control System, please read the -# header note. -#Wed Jun 28 09:55:15 EDT 2023 -sdk.dir=C\:\\Users\\jlockwood\\AppData\\Local\\Android\\Sdk