diff --git a/.gitignore b/.gitignore index a6f14901f..87c2c4a52 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,6 @@ target/ **/.classpath **/.project **/.settings/ -**/pom.xml.versionsBackup \ No newline at end of file +**/pom.xml.versionsBackup + +*-dev.properties \ No newline at end of file diff --git a/vtl-prov/docs/configuration/provenance/README.md b/vtl-prov/docs/configuration/provenance/README.md new file mode 100644 index 000000000..fc27aead7 --- /dev/null +++ b/vtl-prov/docs/configuration/provenance/README.md @@ -0,0 +1,3 @@ +# Blueprint Trevas provenance + +Deployed [here](https://blueprint-trevas-provenance.lab.sspcloud.fr), in the `project-mekong` namespace. diff --git a/vtl-prov/docs/configuration/provenance/blueprint-class-metadata.ttl b/vtl-prov/docs/configuration/provenance/blueprint-class-metadata.ttl new file mode 100644 index 000000000..f135af243 --- /dev/null +++ b/vtl-prov/docs/configuration/provenance/blueprint-class-metadata.ttl @@ -0,0 +1,55 @@ +PREFIX rdfs: +PREFIX sh: +PREFIX schema: +PREFIX fluxShape: +PREFIX flux: +PREFIX fluxSchema: +PREFIX sdth: + +sdth:Program a rdfs:Class; + rdfs:label "Program" ; + rdfs:comment "SDTH Program" . + +flux:ProgramFluxClassInstance a fluxShape:ClassMetadataShape ; + sh:targetNode sdth:Program ; + rdfs:label "Program" ; + rdfs:comment "SDTH Program" ; + fluxSchema:faIcon "fas fa-screwdriver-wrench" ; + fluxSchema:colorIndex 1 ; + fluxSchema:searchPrio 1 . + +sdth:ProgramStep a rdfs:Class; + rdfs:label "Program step" ; + rdfs:comment "SDTH Program step" . + +flux:ProgramStepFluxClassInstance a fluxShape:ClassMetadataShape ; + sh:targetNode sdth:ProgramStep ; + rdfs:label "Program step" ; + rdfs:comment "SDTH Program step" ; + fluxSchema:faIcon "fas fa-calculator" ; + fluxSchema:colorIndex 2 ; + fluxSchema:searchPrio 2 . + +sdth:VariableInstance a rdfs:Class; + rdfs:label "Variable instance" ; + rdfs:comment "SDTH Variable instance" . + +flux:VariableInstanceFluxClassInstance a fluxShape:ClassMetadataShape ; + sh:targetNode sdth:VariableInstance ; + rdfs:label "Variable instance" ; + rdfs:comment "SDTH Variable instance" ; + fluxSchema:faIcon "fa-solid fa-subscript" ; + fluxSchema:colorIndex 3 ; + fluxSchema:searchPrio 3 . + +sdth:DataframeInstance a rdfs:Class; + rdfs:label "Dataframe instance" ; + rdfs:comment "SDTH Dataframe instance" . + +flux:DataframeInstanceFluxClassInstance a fluxShape:ClassMetadataShape ; + sh:targetNode sdth:DataframeInstance ; + rdfs:label "Dataframe instance" ; + rdfs:comment "SDTH Dataframe instance" ; + fluxSchema:faIcon "fas fa-table" ; + fluxSchema:colorIndex 4 ; + fluxSchema:searchPrio 4 . \ No newline at end of file diff --git a/vtl-prov/docs/configuration/provenance/blueprint-detail-metadata.ttl b/vtl-prov/docs/configuration/provenance/blueprint-detail-metadata.ttl new file mode 100644 index 000000000..4fc694752 --- /dev/null +++ b/vtl-prov/docs/configuration/provenance/blueprint-detail-metadata.ttl @@ -0,0 +1,23 @@ +PREFIX rdf: +PREFIX rdfs: +PREFIX sh: +PREFIX blueprintMetaShapes: +PREFIX blueprint: +PREFIX sdth: + +blueprintMetaShapes:LabelDetail a blueprintMetaShapes:ClassDetailShape ; + rdfs:label "Name" ; + sh:path rdfs:label ; + sh:order 0 ; + blueprint:showAs blueprintMetaShapes:Plain ; + sh:targetClass sdth:Program ; + sh:targetClass sdth:ProgramStep ; + sh:targetClass sdth:VariableInstance ; + sh:targetClass sdth:DataframeInstance . + +blueprintMetaShapes:SourceCodeDetail a blueprintMetaShapes:ClassDetailShape ; + rdfs:label "Source code" ; + sh:path sdth:hasSourceCode ; + sh:order 1 ; + blueprint:showAs blueprintMetaShapes:Plain ; + sh:targetClass sdth:ProgramStep . \ No newline at end of file diff --git a/vtl-prov/docs/configuration/provenance/blueprint-link-metadata.ttl b/vtl-prov/docs/configuration/provenance/blueprint-link-metadata.ttl new file mode 100644 index 000000000..2e9c79039 --- /dev/null +++ b/vtl-prov/docs/configuration/provenance/blueprint-link-metadata.ttl @@ -0,0 +1,51 @@ +PREFIX rdfs: +PREFIX sh: +PREFIX fluxShape: +PREFIX flux: +PREFIX fluxSchema: +PREFIX prov: +PREFIX sdth: + +flux:ProgramToProgramStep a sh:PropertyShape, fluxSchema:Link ; + sh:name "has program step" ; + sh:path sdth:hasProgramStep; + sh:class sdth:ProgramStep ; + sh:targetClass sdth:Program . + +flux:ProgramStepToDataframeInstanceConsumes a sh:PropertyShape, fluxSchema:Link ; + sh:name "consumes Dataframe" ; + sh:path sdth:consumesDataframe; + sh:class sdth:DataframeInstance ; + sh:targetClass sdth:ProgramStep . + +flux:ProgramStepToDataframeInstanceProduces a sh:PropertyShape, fluxSchema:Link ; + sh:name "produces Dataframe" ; + sh:path sdth:producesDataframe; + sh:class sdth:DataframeInstance ; + sh:targetClass sdth:ProgramStep . + +flux:ProgramStepToVariableInstance a sh:PropertyShape, fluxSchema:Link ; + sh:name "uses variable" ; + sh:path sdth:usesVariable; + sh:class sdth:ProgramStep ; + sh:targetClass sdth:VariableInstance . + +flux:DataframeInstanceToVariableInstance a sh:PropertyShape, fluxSchema:Link ; + sh:name "has variable instance" ; + sh:path sdth:hasVariableInstance; + sh:class sdth:VariableInstance ; + sh:targetClass sdth:DataframeInstance . + +flux:DataframeInstanceToDataframeInstance a sh:PropertyShape, fluxSchema:Link ; + sh:name "was derived from" ; + sh:path sdth:wasDerivedFrom; + sh:class sdth:DataframeInstance ; + sh:targetClass sdth:DataframeInstance . + +flux:VariableInstanceToVariableInstance a sh:PropertyShape, fluxSchema:Link ; + sh:name "was derived from" ; + sh:path sdth:wasDerivedFrom; + sh:class sdth:VariableInstance ; + sh:targetClass sdth:VariableInstance . + + diff --git a/vtl-prov/docs/configuration/provenance/kubernetes/blueprint/deployment.yml b/vtl-prov/docs/configuration/provenance/kubernetes/blueprint/deployment.yml new file mode 100644 index 000000000..66a5d32d0 --- /dev/null +++ b/vtl-prov/docs/configuration/provenance/kubernetes/blueprint/deployment.yml @@ -0,0 +1,27 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: blueprint-trevas-provenance +spec: + replicas: 1 + selector: + matchLabels: + app: blueprint-trevas-provenance + template: + metadata: + labels: + app: blueprint-trevas-provenance + spec: + containers: + - name: blueprint-trevas-provenance + image: ghcr.io/zazuko/blueprint:dev-main-20240626132259 + imagePullPolicy: IfNotPresent + env: + - name: ENDPOINT_URL + value: "https://trifid-trevas-provenance.lab.sspcloud.fr/query" + - name: SPARQL_CONSOLE_URL + value: "https://trifid-trevas-provenance.lab.sspcloud.fr/sparql/#query" + - name: GRAPH_EXPLORER_URL + value: "https://trifid-trevas-provenance.lab.sspcloud.fr/graph-explorer/?resource" + - name: FULL_TEXT_SEARCH_DIALECT + value: "fuseki" diff --git a/vtl-prov/docs/configuration/provenance/kubernetes/blueprint/ingress.yml b/vtl-prov/docs/configuration/provenance/kubernetes/blueprint/ingress.yml new file mode 100644 index 000000000..504e07752 --- /dev/null +++ b/vtl-prov/docs/configuration/provenance/kubernetes/blueprint/ingress.yml @@ -0,0 +1,21 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: blueprint-trevas-provenance + annotations: + spec.ingressClassName: nginx +spec: + tls: + - hosts: + - blueprint-trevas-provenance.lab.sspcloud.fr + rules: + - host: blueprint-trevas-provenance.lab.sspcloud.fr + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: blueprint-trevas-provenance + port: + number: 80 diff --git a/vtl-prov/docs/configuration/provenance/kubernetes/blueprint/service.yml b/vtl-prov/docs/configuration/provenance/kubernetes/blueprint/service.yml new file mode 100644 index 000000000..ecdb6499d --- /dev/null +++ b/vtl-prov/docs/configuration/provenance/kubernetes/blueprint/service.yml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Service +metadata: + name: blueprint-trevas-provenance +spec: + ports: + - name: http + targetPort: 80 + port: 80 + selector: + app: blueprint-trevas-provenance diff --git a/vtl-prov/docs/configuration/provenance/kubernetes/trifid/deployment.yml b/vtl-prov/docs/configuration/provenance/kubernetes/trifid/deployment.yml new file mode 100644 index 000000000..96f24a111 --- /dev/null +++ b/vtl-prov/docs/configuration/provenance/kubernetes/trifid/deployment.yml @@ -0,0 +1,27 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: trifid-trevas-provenance +spec: + replicas: 1 + selector: + matchLabels: + app: trifid-trevas-provenance + template: + metadata: + labels: + app: trifid-trevas-provenance + spec: + containers: + - name: trifid + image: ghcr.io/zazuko/trifid:v5 + imagePullPolicy: IfNotPresent + env: + - name: SPARQL_ENDPOINT_URL + value: "https://projet-mekong-882672.user.lab.sspcloud.fr/trevas-provenance/query" + - name: DATASET_BASE_URL + value: "https://rdf.insee.fr/" + - name: SPARQL_USER + value: "admin" + - name: SPARQL_PASSWORD + value: "constances" diff --git a/vtl-prov/docs/configuration/provenance/kubernetes/trifid/ingress.yml b/vtl-prov/docs/configuration/provenance/kubernetes/trifid/ingress.yml new file mode 100644 index 000000000..0000a9ecb --- /dev/null +++ b/vtl-prov/docs/configuration/provenance/kubernetes/trifid/ingress.yml @@ -0,0 +1,21 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: trifid-trevas-provenance + annotations: + spec.ingressClassName: nginx +spec: + tls: + - hosts: + - trifid-trevas-provenance.lab.sspcloud.fr + rules: + - host: trifid-trevas-provenance.lab.sspcloud.fr + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: trifid-trevas-provenance + port: + number: 8080 diff --git a/vtl-prov/docs/configuration/provenance/kubernetes/trifid/service.yml b/vtl-prov/docs/configuration/provenance/kubernetes/trifid/service.yml new file mode 100644 index 000000000..5e76612df --- /dev/null +++ b/vtl-prov/docs/configuration/provenance/kubernetes/trifid/service.yml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Service +metadata: + name: trifid-trevas-provenance +spec: + ports: + - name: http + targetPort: 8080 + port: 8080 + selector: + app: trifid-trevas-provenance diff --git a/vtl-prov/docs/model-v1.md b/vtl-prov/docs/model-v1.md index 657fe20b2..a17217c17 100644 --- a/vtl-prov/docs/model-v1.md +++ b/vtl-prov/docs/model-v1.md @@ -49,10 +49,14 @@ classDiagram rdfs:label } class ProgramStep { + rdfs:label } class VariableInstance { + rdfs:label + sdth:hasName } class DataframeInstance { + rdfs:label sdth:hasName } @@ -87,15 +91,17 @@ ds_res <- ds_mul[filter mod(var1, 2) = 0][calc var_sum := var1 + var2]; #### Model target ```ttl -@PREFIX org: -@PREFIX prov: -@PREFIX sdth: +PREFIX org: +PREFIX prov: +PREFIX sdth: +PREFIX rdfs: +PREFIX rdf: # --- Program and steps a sdth:Program ; a prov:Agent ; # Agent? Or an activity rdfs:label "My program 1"@en, "Mon programme 1"@fr ; - sdth:hasProgramStep , , ; + sdth:hasProgramStep , , . a sdth:ProgramStep ; sdth:hasSourceCode "ds_sum := ds1 + ds2;" ; @@ -115,44 +121,40 @@ ds_res <- ds_mul[filter mod(var1, 2) = 0][calc var_sum := var1 + var2]; sdth:producesDataframe ; sdth:usesVariable , . # there i think it's ok -# --- Variables +# --- Variables # i think here it's not instances but names we refer to... - a sdth:VariableInstance . - a sdth:VariableInstance . - a sdth:VariableInstance . - a sdth:VariableInstance . + a sdth:VariableInstance ; + sdth:hasName "id1" . + a sdth:VariableInstance ; + sdth:hasName "var1" . + a sdth:VariableInstance ; + sdth:hasName "var2" . + a sdth:VariableInstance ; + sdth:hasName "var_sum" . # --- Data frames a sdth:DataframeInstance ; sdth:hasName "ds1" ; sdth:hasVariableInstance ; sdth:hasVariableInstance . - + a sdth:DataframeInstance ; sdth:hasName "ds2" ; sdth:hasVariableInstance ; sdth:hasVariableInstance . - + a sdth:DataframeInstance ; sdth:hasName "ds_sum" ; sdth:wasDerivedFrom , ; - sdth:hasVariableInstance ; - sdth:hasVariableInstance ; - sdth:hasVariableInstance . + sdth:hasVariableInstance , , . a sdth:DataframeInstance ; sdth:hasName "ds_mul" ; - sdth:wasDerivedFrom . - sdth:hasVariableInstance ; - sdth:hasVariableInstance ; - sdth:hasVariableInstance . + sdth:wasDerivedFrom ; + sdth:hasVariableInstance , , . a sdth:DataframeInstance ; sdth:hasName "ds_res" ; sdth:wasDerivedFrom ; - sdth:hasVariableInstance ; - sdth:hasVariableInstance ; - sdth:hasVariableInstance ; - sdth:hasVariableInstance . - + sdth:hasVariableInstance , , , . ``` \ No newline at end of file diff --git a/vtl-prov/pom.xml b/vtl-prov/pom.xml index 83585f559..98ea5a06f 100644 --- a/vtl-prov/pom.xml +++ b/vtl-prov/pom.xml @@ -28,6 +28,13 @@ vtl-parser 1.6.0-SNAPSHOT + + + org.apache.jena + apache-jena-libs + pom + 5.0.0 + \ No newline at end of file diff --git a/vtl-prov/src/main/java/fr/insee/vtl/prov/RDFUtils.java b/vtl-prov/src/main/java/fr/insee/vtl/prov/RDFUtils.java new file mode 100644 index 000000000..d95f96aaf --- /dev/null +++ b/vtl-prov/src/main/java/fr/insee/vtl/prov/RDFUtils.java @@ -0,0 +1,32 @@ +package fr.insee.vtl.prov; + +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdfconnection.RDFConnection; + +import java.io.FileNotFoundException; +import java.io.FileOutputStream; + +public class RDFUtils { + + private static final String SDTH_BASE_URI = "http://rdf-vocabulary.ddialliance.org/sdth"; + + public static Model initModel(String baseFilePath) { + Model model = ModelFactory.createDefaultModel(); + model.read(baseFilePath); + return model; + } + + public static void loadModelWithCredentials(Model model, + String sparlEndpoint, + String sparlEndpointUser, + String sparlEndpointPassword) { + if (!sparlEndpoint.isEmpty()) { + RDFConnection connect = RDFConnection + .connectPW(sparlEndpoint, sparlEndpointUser, sparlEndpointPassword); + connect.fetchDataset(); + connect.load(model); + connect.close(); + } + } +} diff --git a/vtl-prov/src/main/java/fr/insee/vtl/prov/utils/PropertiesLoader.java b/vtl-prov/src/main/java/fr/insee/vtl/prov/utils/PropertiesLoader.java new file mode 100644 index 000000000..c6157d98a --- /dev/null +++ b/vtl-prov/src/main/java/fr/insee/vtl/prov/utils/PropertiesLoader.java @@ -0,0 +1,22 @@ +package fr.insee.vtl.prov.utils; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +public class PropertiesLoader { + + public static Properties loadProperties() throws IOException { + Properties configuration = new Properties(); + InputStream inputStream = PropertiesLoader.class + .getClassLoader() + .getResourceAsStream("trevas.properties"); + if (null != inputStream) configuration.load(inputStream); + InputStream inputStreamDev = PropertiesLoader.class + .getClassLoader() + .getResourceAsStream("trevas-dev.properties"); + if (null != inputStreamDev) configuration.load(inputStreamDev); + inputStream.close(); + return configuration; + } +} diff --git a/vtl-prov/src/main/resources/trevas.properties b/vtl-prov/src/main/resources/trevas.properties new file mode 100644 index 000000000..9d68eb4db --- /dev/null +++ b/vtl-prov/src/main/resources/trevas.properties @@ -0,0 +1,4 @@ +sparql-endpoint-url= +trevas-provenance-repository-path=provenance +sparql-endpoint-user= +sparql-endpoint-password= \ No newline at end of file diff --git a/vtl-prov/src/test/java/fr/insee/vtl/prov/RDFTest.java b/vtl-prov/src/test/java/fr/insee/vtl/prov/RDFTest.java new file mode 100644 index 000000000..834df9991 --- /dev/null +++ b/vtl-prov/src/test/java/fr/insee/vtl/prov/RDFTest.java @@ -0,0 +1,50 @@ +package fr.insee.vtl.prov; + +import fr.insee.vtl.prov.utils.PropertiesLoader; +import org.apache.jena.rdf.model.Model; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.Properties; + +import static org.assertj.core.api.Assertions.assertThat; + +public class RDFTest { + + static Properties conf; + + static { + try { + conf = PropertiesLoader.loadProperties(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + static String sparqlEndpoint = conf.getProperty("sparql-endpoint-url") + conf.getProperty("trevas-provenance-repository-path"); + static String sparqlEndpointUser = conf.getProperty("sparql-endpoint-user"); + static String sparlqEndpointPassword = conf.getProperty("sparql-endpoint-password"); + + String BLUEPRINT_CLASS_PATH = "docs/configuration/provenance/blueprint-class-metadata.ttl"; + + String BLUEPRINT_DETAILS_PATH = "docs/configuration/provenance/blueprint-detail-metadata.ttl"; + + String BLUEPRINT_LINK_PATH = "docs/configuration/provenance/blueprint-link-metadata.ttl"; + + + @Test + public void loadRDF() { + // Blueprint configuration + Model modelClass = RDFUtils.initModel(BLUEPRINT_CLASS_PATH); + Model modelDetail = RDFUtils.initModel(BLUEPRINT_DETAILS_PATH); + Model modelLink = RDFUtils.initModel(BLUEPRINT_LINK_PATH); + // Data + Model modelProv = RDFUtils.initModel("src/test/resources/temp-prov.ttl"); + // Merge models + Model model = modelClass.add(modelDetail).add(modelLink).add(modelProv); + // Load model + RDFUtils.loadModelWithCredentials(model, sparqlEndpoint, sparqlEndpointUser, sparlqEndpointPassword); + assertThat(true).isTrue(); + } + +} diff --git a/vtl-prov/src/test/java/fr/insee/vtl/prov/VariableGraphListenerTest.java b/vtl-prov/src/test/java/fr/insee/vtl/prov/VariableGraphListenerTest.java index e0ac42500..0851727d5 100644 --- a/vtl-prov/src/test/java/fr/insee/vtl/prov/VariableGraphListenerTest.java +++ b/vtl-prov/src/test/java/fr/insee/vtl/prov/VariableGraphListenerTest.java @@ -9,6 +9,7 @@ import org.jgrapht.Graph; import org.jgrapht.graph.DefaultDirectedGraph; import org.jgrapht.graph.DefaultEdge; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.util.Set; @@ -17,6 +18,17 @@ public class VariableGraphListenerTest { + @Test + public void simpleTest() { + String script = "ds_sum := ds1 + ds2;\n" + + "ds_mul := ds_sum * 3; \n" + + "ds_res <- ds_mul[filter mod(var1, 2) = 0][calc var_sum := var1 + var2];"; + + VariableGraphListener provenanceListener = parseAndListen(script); + printTrees(provenanceListener); + assertThat(true).isTrue(); + } + @Test void testComplexGraph() { String expr = "" + @@ -181,4 +193,5 @@ private static VariableGraphListener parseAndListen(String expr) { printTrees(provenanceListener); return provenanceListener; } + } \ No newline at end of file diff --git a/vtl-prov/src/test/resources/temp-prov.ttl b/vtl-prov/src/test/resources/temp-prov.ttl new file mode 100644 index 000000000..63c460eed --- /dev/null +++ b/vtl-prov/src/test/resources/temp-prov.ttl @@ -0,0 +1,89 @@ +PREFIX org: +PREFIX prov: +PREFIX sdth: +PREFIX rdfs: +PREFIX rdf: + +# --- Program and steps + a sdth:Program ; + a prov:Agent ; # Agent? Or an activity + rdfs:label "My program 1"@en, "Mon programme 1"@fr ; + sdth:hasProgramStep , + , + . + + a sdth:ProgramStep ; + rdfs:label "Program step 1"@en, "Étape 1"@fr ; + sdth:hasSourceCode "ds_sum := ds1 + ds2;" ; + sdth:consumesDataframe , + ; + sdth:producesDataframe ; + sdth:usesVariable , + . # Do we need / have to declare it? + + a sdth:ProgramStep ; + rdfs:label "Program step 2"@en, "Étape 2"@fr ; + sdth:hasSourceCode "ds_mul := ds_sum * 3;" ; + sdth:consumesDataframe ; + sdth:producesDataframe ; + sdth:usesVariable , + . # Do we need / have to declare it? + + a sdth:ProgramStep ; + rdfs:label "Program step 3"@en, "Étape 3"@fr ; + sdth:hasSourceCode "ds_res <- ds_mul[filter mod(var1, 2) = 0][calc var_sum := var1 + var2];" ; + sdth:consumesDataframe ; + sdth:producesDataframe ; + sdth:usesVariable , + . # there i think it's ok + +# --- Variables +# i think here it's not instances but names we refer to... + a sdth:VariableInstance ; + rdfs:label "id1" . + a sdth:VariableInstance ; + rdfs:label "var1" . + a sdth:VariableInstance ; + rdfs:label "var2" . + a sdth:VariableInstance ; + rdfs:label "var_sum" . + +# --- Data frames + a sdth:DataframeInstance ; + rdfs:label "ds1" ; + sdth:hasName "ds1" ; + sdth:hasVariableInstance , + , + . + + a sdth:DataframeInstance ; + rdfs:label "ds2" ; + sdth:hasName "ds2" ; + sdth:hasVariableInstance , + , + . + + a sdth:DataframeInstance ; + rdfs:label "ds_sum" ; + sdth:hasName "ds_sum" ; + sdth:wasDerivedFrom , + ; + sdth:hasVariableInstance , + , + . + + a sdth:DataframeInstance ; + rdfs:label "ds_mul" ; + sdth:hasName "ds_mul" ; + sdth:wasDerivedFrom ; + sdth:hasVariableInstance , + , + . + + a sdth:DataframeInstance ; + rdfs:label "ds_res" ; + sdth:wasDerivedFrom ; + sdth:hasVariableInstance , + , + , + . \ No newline at end of file