diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index c88dd30..0371308 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -12,3 +12,8 @@ jobs: cache: 'gradle' - name: Build run: ./gradlew build + - name: Upload Unsigned Module + uses: actions/upload-artifact@v3 + with: + name: ignition-extensions-unsigned + path: build/Ignition-Extensions.unsigned.modl diff --git a/LICENSE.html b/LICENSE.html deleted file mode 100644 index 04e615a..0000000 --- a/LICENSE.html +++ /dev/null @@ -1,15 +0,0 @@ -
-

MIT License

-

Copyright (c) 2023 Ignition Module Development Community

-

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated - documentation files (the "Software"), to deal in the Software without restriction, including without limitation - the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and - to permit persons to whom the Software is furnished to do so, subject to the following conditions:

-

The above copyright notice and this permission notice shall be included in all copies or substantial portions of - the Software.

-

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO - THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF - CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - DEALINGS IN THE SOFTWARE.

-
diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..b621290 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,13 @@ +MIT License +Copyright (c) 2023 Ignition Module Development Community +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +to permit persons to whom the Software is furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all copies or substantial portions of +the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/build.gradle.kts b/build.gradle.kts index 692944a..f98efa4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -22,7 +22,7 @@ ignitionModule { id.set("org.imdc.extensions.IgnitionExtensions") moduleVersion.set("${project.version}") moduleDescription.set("Useful but niche extensions to Ignition for power users") - license.set("LICENSE.html") + license.set("LICENSE.txt") requiredIgnitionVersion.set(libs.versions.ignition) projectScopes.putAll( diff --git a/common/src/main/kotlin/org/imdc/extensions/common/TagExtensions.kt b/common/src/main/kotlin/org/imdc/extensions/common/TagExtensions.kt new file mode 100644 index 0000000..589e9be --- /dev/null +++ b/common/src/main/kotlin/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/src/main/resources/org/imdc/extensions/common/TagExtensions.properties b/common/src/main/resources/org/imdc/extensions/common/TagExtensions.properties new file mode 100644 index 0000000..45f8560 --- /dev/null +++ b/common/src/main/resources/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/gateway/src/main/kotlin/org/imdc/extensions/gateway/GatewayHook.kt b/gateway/src/main/kotlin/org/imdc/extensions/gateway/GatewayHook.kt index 31375e7..36cffab 100644 --- a/gateway/src/main/kotlin/org/imdc/extensions/gateway/GatewayHook.kt +++ b/gateway/src/main/kotlin/org/imdc/extensions/gateway/GatewayHook.kt @@ -25,6 +25,7 @@ class GatewayHook : AbstractGatewayModuleHook() { addPropertyBundle() addPropertyBundle() addPropertyBundle() + addPropertyBundle() } PyDatasetBuilder.register() @@ -44,6 +45,7 @@ class GatewayHook : AbstractGatewayModuleHook() { addScriptModule("system.dataset", DatasetExtensions, ExtensionDocProvider) addScriptModule("system.util", UtilitiesExtensions(context), ExtensionDocProvider) addScriptModule("system.project", GatewayProjectExtensions(context), ExtensionDocProvider) + addScriptModule("system.tag", GatewayTagExtensions(context), ExtensionDocProvider) } } diff --git a/gateway/src/main/kotlin/org/imdc/extensions/gateway/GatewayTagExtensions.kt b/gateway/src/main/kotlin/org/imdc/extensions/gateway/GatewayTagExtensions.kt new file mode 100644 index 0000000..d8b3509 --- /dev/null +++ b/gateway/src/main/kotlin/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() + } +}