cim-loader 0.1.0b0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2023, PNNL-CIM-Tools
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,78 @@
1
+ Metadata-Version: 2.1
2
+ Name: cim-loader
3
+ Version: 0.1.0b0
4
+ Summary: CIM Database Uploade & Downloader
5
+ Author: A. Anderson
6
+ Author-email: 19935503+AAndersn@users.noreply.github.com
7
+ Requires-Python: >=3.8.1,<4.0
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.9
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Provides-Extra: gridappsd
14
+ Requires-Dist: SPARQLWrapper (>=2.0.0,<3.0.0)
15
+ Requires-Dist: cim-graph (>=0.1.5a0,<0.2.0)
16
+ Requires-Dist: mysql-connector-python (>=8.0.0,<9.0.0)
17
+ Requires-Dist: neo4j (>=5.10.0,<6.0.0)
18
+ Requires-Dist: rdflib (>=7.0.0,<8.0.0)
19
+ Requires-Dist: rdflib-neo4j (>=0.0.1b6,<0.0.2)
20
+ Requires-Dist: requests (>=2.28.0,<3.0.0)
21
+ Description-Content-Type: text/markdown
22
+
23
+ # CIM-Loader
24
+ Automated scripts for
25
+ * uploading and downloading CIM files from various databases
26
+ * converting CIM files between common formats (XML, TTL, etc.)
27
+
28
+
29
+ ## Installation
30
+ PyPi version coming soon.
31
+
32
+ Clone the repo and run
33
+ `pip install -e CIM-Loader`
34
+
35
+
36
+ ## Usage
37
+
38
+ To use CIM-Loader for bulk upload and download, set the connection parameters with the correct url / host / port / username / password and then invoke the associated upload / download method
39
+
40
+ ```python
41
+ from cimloader.databases import ConnectionParameters
42
+ from cimloader.uploaders import BlazegraphUploader
43
+ params = ConnectionParameters(url = "http://localhost:8889/bigdata/namespace/kb/sparql")
44
+ loader = BlazegraphUploader(params)
45
+
46
+ loader.upload_from_file(filepath='./test_models', filename='ieee13_seto.xml')
47
+ ```
48
+
49
+
50
+ ## Databases Supported
51
+ Databases to be supported in first full release:
52
+ * Blazegraph
53
+ * Neo4J
54
+ * GraphDB
55
+ * MySQL
56
+
57
+ Support may be added in the future for:
58
+ * Apache Tinkerpop
59
+ * SQlite
60
+ * AVEVA PI Historian
61
+ * Others as requested
62
+
63
+
64
+ ## Attribution and Disclaimer
65
+
66
+ This software was created under a project sponsored by the U.S. Department of Energy’s Office of Electricity, an agency of the United States Government. Neither the United States Government nor the United States Department of Energy, nor Battelle, nor any of their employees, nor any jurisdiction or organization that has cooperated in the development of these materials, makes any warranty, express or implied, or assumes any legal liability or responsibility for the accuracy, completeness, or usefulness or any information, apparatus, product, software, or process disclosed, or represents that its use would not infringe privately owned rights.
67
+
68
+ Reference herein to any specific commercial product, process, or service by trade name, trademark, manufacturer, or otherwise does not necessarily constitute or imply its endorsement, recommendation, or favoring by the United States Government or any agency thereof, or Battelle Memorial Institute. The views and opinions of authors expressed herein do not necessarily state or reflect those of the United States Government or any agency thereof.
69
+
70
+ PACIFIC NORTHWEST NATIONAL LABORATORY
71
+ operated by
72
+ BATTELLE
73
+ for the
74
+ UNITED STATES DEPARTMENT OF ENERGY
75
+ under Contract DE-AC05-76RL01830
76
+
77
+
78
+
@@ -0,0 +1,55 @@
1
+ # CIM-Loader
2
+ Automated scripts for
3
+ * uploading and downloading CIM files from various databases
4
+ * converting CIM files between common formats (XML, TTL, etc.)
5
+
6
+
7
+ ## Installation
8
+ PyPi version coming soon.
9
+
10
+ Clone the repo and run
11
+ `pip install -e CIM-Loader`
12
+
13
+
14
+ ## Usage
15
+
16
+ To use CIM-Loader for bulk upload and download, set the connection parameters with the correct url / host / port / username / password and then invoke the associated upload / download method
17
+
18
+ ```python
19
+ from cimloader.databases import ConnectionParameters
20
+ from cimloader.uploaders import BlazegraphUploader
21
+ params = ConnectionParameters(url = "http://localhost:8889/bigdata/namespace/kb/sparql")
22
+ loader = BlazegraphUploader(params)
23
+
24
+ loader.upload_from_file(filepath='./test_models', filename='ieee13_seto.xml')
25
+ ```
26
+
27
+
28
+ ## Databases Supported
29
+ Databases to be supported in first full release:
30
+ * Blazegraph
31
+ * Neo4J
32
+ * GraphDB
33
+ * MySQL
34
+
35
+ Support may be added in the future for:
36
+ * Apache Tinkerpop
37
+ * SQlite
38
+ * AVEVA PI Historian
39
+ * Others as requested
40
+
41
+
42
+ ## Attribution and Disclaimer
43
+
44
+ This software was created under a project sponsored by the U.S. Department of Energy’s Office of Electricity, an agency of the United States Government. Neither the United States Government nor the United States Department of Energy, nor Battelle, nor any of their employees, nor any jurisdiction or organization that has cooperated in the development of these materials, makes any warranty, express or implied, or assumes any legal liability or responsibility for the accuracy, completeness, or usefulness or any information, apparatus, product, software, or process disclosed, or represents that its use would not infringe privately owned rights.
45
+
46
+ Reference herein to any specific commercial product, process, or service by trade name, trademark, manufacturer, or otherwise does not necessarily constitute or imply its endorsement, recommendation, or favoring by the United States Government or any agency thereof, or Battelle Memorial Institute. The views and opinions of authors expressed herein do not necessarily state or reflect those of the United States Government or any agency thereof.
47
+
48
+ PACIFIC NORTHWEST NATIONAL LABORATORY
49
+ operated by
50
+ BATTELLE
51
+ for the
52
+ UNITED STATES DEPARTMENT OF ENERGY
53
+ under Contract DE-AC05-76RL01830
54
+
55
+
@@ -0,0 +1,18 @@
1
+
2
+ # from cimloader.databases.blazegraph import Blazegraph
3
+ from cimloader.databases.mysql import MySQLConnection
4
+ from cimloader.databases import ConnectionInterface, ConnectionParameters, Parameter, QueryResponse
5
+
6
+ import os
7
+ import logging
8
+ import subprocess
9
+
10
+
11
+ _log = logging.getLogger(__name__)
12
+
13
+ class BlazegraphtoMySQL():
14
+ def __init__(self, naerm_params:ConnectionInterface, neo4j_params:ConnectionInterface,
15
+ tmp_dir:str, docker_container:str):
16
+ # self.NaermDownloader = NAERM(naerm_params)
17
+ # self.Neo4jConnection= Neo4j(neo4j_params)
18
+ pass
@@ -0,0 +1,80 @@
1
+ from cimloader.web_apis.naerm_api import NAERM
2
+ from cimloader.databases.neo4j import Neo4jConnection
3
+ from cimloader.converters.dss_to_cim import DSStoCIM
4
+ from cimloader.databases import ConnectionInterface, ConnectionParameters, Parameter, QueryResponse
5
+
6
+ import os
7
+ import logging
8
+ import subprocess
9
+
10
+
11
+ _log = logging.getLogger(__name__)
12
+
13
+ class NAERMtoNeo4j():
14
+ def __init__(self, naerm_params:ConnectionInterface, neo4j_params:ConnectionInterface,
15
+ tmp_dir:str, docker_container:str):
16
+ self.NaermDownloader = NAERM(naerm_params)
17
+ # self.Neo4jConnection= Neo4jConnection(neo4j_params)
18
+ self.dss_converter = DSStoCIM()
19
+ self.tmp_dir = tmp_dir
20
+ self.docker_container = docker_container
21
+
22
+ def upload_all_dss(self, limit):
23
+ # self.Neo4jConnection.connect()
24
+ case_uuid_list, case_uuid_names = self.NaermDownloader.download_all_dss(destination=self.tmp_dir, limit=limit)
25
+ for case_uuid in case_uuid_list:
26
+ self.dss_converter.convert_file(file_path=f"{self.tmp_dir}/{case_uuid}", feeder_mrid=case_uuid,
27
+ sub_name=case_uuid_names[case_uuid], sub_geo="SFO", geo="CA", uuids_file=f"{self.tmp_dir}/sfo_uuid.dat")
28
+ if self.docker_container is not None:
29
+ subprocess.call(["docker", "cp", f"{self.tmp_dir}/{case_uuid}/Master.xml", f"{self.docker_container}:import/{case_uuid}/Master.xml"])
30
+ self.Neo4jConnection.upload(filepath=f"/import/{case_uuid}", filename="Master.xml", format="RDF/XML")
31
+ _log.info(f"Successfully uploaded feeder id {case_uuid}")
32
+ # self.Neo4jConnection.disconnect()
33
+
34
+ def upload_case(self, case_uuid):
35
+ # self.Neo4jConnection.connect()
36
+ self.NaermDownloader.download_case(case_uuid=case_uuid, destination=self.tmp_dir)
37
+
38
+ self.dss_converter.convert_file(file_path=f"{self.tmp_dir}/{case_uuid}", feeder_mrid=case_uuid, sub_geo="SFO", geo="CA", uuids_file=f"{self.tmp_dir}/sfo_uuid.dat")
39
+ if self.docker_container is not None:
40
+ subprocess.call(["docker", "cp", f"{self.tmp_dir}/{case_uuid}/Master.xml", f"{self.docker_container}:import/{case_uuid}/Master.xml"])
41
+ self.Neo4jConnection.upload(filepath=f"/import/{case_uuid}", filename="Master.xml", format="RDF/XML")
42
+ _log.info(f"Successfully uploaded feeder id {case_uuid}")
43
+ # self.Neo4jConnection.disconnect()
44
+
45
+
46
+
47
+
48
+
49
+ def _main():
50
+
51
+ neo4j_params = ConnectionParameters(url = "neo4j://localhost:7687", database="neo4j", username="neo4j", password="neo4j",
52
+ cim_profile='cimhub_2023', namespace="http://iec.ch/TC57/CIM100#")
53
+
54
+ naerm_params = ConnectionParameters(url = "https://api.develop.naerm.team/data/bes/case_files")
55
+
56
+ tmp_dir = "/home/ande188/naerm"
57
+
58
+ sfo_uuid = """
59
+ Station=Station=1 {ad4e7f6e-b4eb-44cb-a75e-ac7f3e2e41c5}
60
+ GeoRgn=GeoRgn=1 {731690f4-13e0-4555-8084-3cc05d1000f8}
61
+ SubGeoRgn=SubGeoRgn=1 {277ebb75-f974-4de6-8a85-8526d0aa3351}"""
62
+
63
+ with open(f"{tmp_dir}/sfo_uuid.dat", "w") as fp:
64
+ fp.write(sfo_uuid)
65
+
66
+ docker_container = "gridappsd-docker-neo4j-apoc_1"
67
+
68
+ nn4j = NAERMtoNeo4j(naerm_params=naerm_params, neo4j_params=neo4j_params, tmp_dir=tmp_dir, docker_container=docker_container)
69
+
70
+ nn4j.upload_all_dss(limit=10)
71
+
72
+ # case_uuid = "accdf54c-c90c-4c41-9d7a-5e5d3d17a4e5"
73
+ case_uuid = "1b5adb54-dcc3-472b-a5fd-aaab635857e1"
74
+ nn4j.upload_case(case_uuid=case_uuid)
75
+
76
+
77
+
78
+
79
+ if __name__ == "__main__":
80
+ _main()
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any, List
5
+
6
+
7
+
8
+ @dataclass
9
+ class Parameter:
10
+ key: Any
11
+ value: Any
12
+
13
+
14
+ @dataclass
15
+ class ConnectionParameters:
16
+ url: str = field(default_factory=str)
17
+ host: str = field(default_factory=str)
18
+ port: str = field(default_factory=str)
19
+ filename: str = field(default_factory=str)
20
+ username: str = field(default_factory=str)
21
+ password: str = field(default_factory=str)
22
+ database: str = field(default_factory=str)
23
+ container: str = field(default_factory=str)
24
+ namespace: str = field(default="<http://iec.ch/TC57/CIM100#>")
25
+ cim_profile: str = field(default_factory=str)
26
+ iec61970_301: int = field(default=7)
27
+
28
+
29
+ @dataclass
30
+ class QueryResponse:
31
+ response: Any
32
+
33
+
34
+ @dataclass
35
+ class ConnectionInterface:
36
+ connection_params: ConnectionParameters
37
+
38
+ def connect(self):
39
+ raise RuntimeError("Must have implemented connect in inherited class")
40
+
41
+ def disconnect(self):
42
+ raise RuntimeError("Must have implemented disconnect in inherited class")
43
+
44
+ def execute(self, query: str) -> QueryResponse:
45
+ raise RuntimeError("Must have implemented query in the inherited class")
46
+
47
+ from cimloader.databases.blazegraph import BlazegraphConnection
48
+ from cimloader.databases.neo4j import Neo4jConnection
49
+ from cimloader.databases.mysql import MySQLConnection
@@ -0,0 +1,45 @@
1
+ import logging
2
+ import subprocess
3
+
4
+ from cimloader.databases import ConnectionInterface, QueryResponse
5
+ from SPARQLWrapper import JSON, POST, SPARQLWrapper
6
+
7
+ _log = logging.getLogger(__name__)
8
+
9
+ class BlazegraphConnection(ConnectionInterface):
10
+ def __init__(self, connection_params: ConnectionInterface) -> None:
11
+ self.cim_profile = connection_params.cim_profile
12
+ # self.cim = importlib.import_module('cimgraph.data_profile.' + self.cim_profile)
13
+ self.namespace = connection_params.namespace
14
+ self.iec61970_301 = connection_params.iec61970_301
15
+ self.url = connection_params.url
16
+ self.connection_params = connection_params
17
+ self.sparql_obj = None
18
+
19
+ def connect(self):
20
+ if not self.sparql_obj:
21
+ self.sparql_obj = SPARQLWrapper(self.connection_params.url)
22
+ self.sparql_obj.setReturnFormat(JSON)
23
+
24
+ def configure(self):
25
+ pass
26
+
27
+ def drop_all(self):
28
+ self.update('drop all')
29
+
30
+ def disconnect(self):
31
+ self.sparql_obj = None
32
+
33
+ def execute(self, query_message: str) -> QueryResponse:
34
+ self.connect()
35
+ self.sparql_obj.setQuery(query_message)
36
+ self.sparql_obj.setMethod(POST)
37
+ query_output = self.sparql_obj.query().convert()
38
+ return query_output
39
+
40
+ def update(self, query_message:str) -> QueryResponse:
41
+ self.connect()
42
+ self.sparql_obj.setQuery(query_message)
43
+ self.sparql_obj.setMethod(POST)
44
+ query_output = self.sparql_obj.query()
45
+ return query_output
@@ -0,0 +1,186 @@
1
+ import re
2
+ import mysql.connector
3
+ import logging
4
+ import importlib
5
+ import json
6
+ import enum
7
+ import time
8
+
9
+ from cimloader.databases import ConnectionInterface, ConnectionParameters, Parameter, QueryResponse
10
+ from cimgraph.data_profile.known_problem_classes import ClassesWithoutMRID
11
+ from cimgraph.models import GraphModel
12
+
13
+ _log = logging.getLogger(__name__)
14
+
15
+ class MySQLConnection(ConnectionInterface):
16
+ def __init__(self, connection_parameters: ConnectionParameters):
17
+ self.connection_parameters = connection_parameters
18
+ self.cim_profile = connection_parameters.cim_profile
19
+ self.cim = importlib.import_module('cimgraph.data_profile.' + self.cim_profile)
20
+ self.namespace = connection_parameters.namespace
21
+ self.host = connection_parameters.host
22
+ self.port = connection_parameters.port
23
+ self.username = connection_parameters.username
24
+ self.password = connection_parameters.password
25
+ self.database = connection_parameters.database
26
+ self.connection = None
27
+ self.cursor = None
28
+
29
+ def connect(self):
30
+ if not self.cursor:
31
+ if not self.database: # Set database name to CIM Profile name if not specified
32
+ self.database = self.cim_profile
33
+ try:
34
+ self.connection = mysql.connector.connect(host = self.host, user = self.username, password = self.password, database = self.database)
35
+ self.cursor = self.connection.cursor(buffered = True)
36
+ except:
37
+ _log.error('Could not connect to database')
38
+
39
+ def disconnect(self):
40
+ self.cursor = None
41
+
42
+ def execute(self, query_message: str) -> QueryResponse:
43
+ self.connect()
44
+ self.cursor.execute(query_message)
45
+ response = self.cursor.fetchall()
46
+ return response
47
+
48
+ def create_database(self, database:str = None, overwrite:bool = True):
49
+ if database is None:
50
+ if self.database is None:
51
+ database = self.cim_profile
52
+ else:
53
+ database = self.database
54
+ self.database = database
55
+ connection = mysql.connector.connect(host = self.host, user = self.username, password = self.password)
56
+ cursor = connection.cursor(buffered = True)
57
+ if overwrite:
58
+ # try:
59
+ cursor.execute(f"DROP DATABASE {database}")
60
+ # except:
61
+ # _log.error("Unable to connect to database")
62
+
63
+ # try:
64
+ cursor.execute(f"CREATE DATABASE {database}")
65
+ # except:
66
+ # _log.error("Unable to create new database")
67
+
68
+ def configure(self, overwrite:bool = True):
69
+ class_list = self.cim.__all__
70
+ classes_without_mrid = ClassesWithoutMRID()
71
+
72
+ for class_name in class_list:
73
+ cim_class = eval(f"self.cim.{class_name}")
74
+ # print(cim_class.__name__)
75
+
76
+ try:
77
+ self.connect()
78
+ self.cursor.execute(f"DROP TABLE {cim_class.__name__}")
79
+ except:
80
+ pass
81
+ # Get all attribute types from CIMantic Graphs dataclasses
82
+ try:
83
+ if type(cim_class) == enum.EnumMeta:
84
+ values = cim_class._member_names_
85
+ sql_query = f"CREATE TABLE {cim_class.__name__} (enumeration VARCHAR(255))"
86
+ self.cursor.execute(sql_query) # create table
87
+ for value in values:
88
+ sql_insert = f"""INSERT INTO {cim_class.__name__} (enumeration) VALUES ("{value}")"""
89
+ self.cursor.execute(sql_insert)
90
+ else:
91
+ # Check if is class and has class fields
92
+ fields = cim_class.__dataclass_fields__
93
+ sql_query = f"CREATE TABLE {cim_class.__name__} ("
94
+
95
+ sql_query = sql_query + "username VARCHAR(255), "
96
+ sql_query = sql_query + "timestamp INT, "
97
+
98
+ # Handling for problem classes that don't inherit from IdentifiedObject
99
+ if cim_class in classes_without_mrid.classes:
100
+ sql_query = sql_query + "_mRID VARCHAR(255), "
101
+ # Iterate through all attributes and add to table
102
+ for attr in list(fields.keys()):
103
+ attribute_name = fields[attr].type
104
+ # Determine type of attribute value and use correct column type in table
105
+ if "List" in attribute_name:
106
+ sql_query = sql_query + f"_{attr} JSON, "
107
+ elif "float" in attribute_name:
108
+ sql_query = sql_query + f"_{attr} FLOAT, "
109
+ elif 'Optional' in attribute_name: #check if attribute is association to a class object
110
+ if '\'' in attribute_name: #handling inconsistent '' marks in data profile
111
+ at_cls = re.match(r'Optional\[\'(.*)\']',attribute_name)
112
+ attribute_name = at_cls.group(1)
113
+ else:
114
+ at_cls = re.match(r'Optional\[(.*)]',attribute_name)
115
+ attribute_name = at_cls.group(1)
116
+ # If a CIM class, then use JSON-LD
117
+ if attribute_name in self.cim.__all__:
118
+ attribute_class = eval(f"self.cim.{attribute_name}")
119
+ if type(attribute_class) == enum.EnumMeta:
120
+ sql_query = sql_query + f"_{attr} FLOAT, "
121
+ else:
122
+ sql_query = sql_query + f"_{attr} JSON, "
123
+
124
+ else:
125
+ sql_query = sql_query + f"_{attr} VARCHAR(255), "
126
+ else:
127
+ sql_query = sql_query + f"_{attr} VARCHAR(255), "
128
+ sql_query = sql_query[:-2] # remove extra punctuation
129
+ sql_query = sql_query + ")"
130
+ # print(sql_query)
131
+ self.cursor.execute(sql_query) # create table
132
+ _log.info(f"Created table for class {class_name}")
133
+ except:
134
+ _log.warning(f"Unable to create table for class {class_name}")
135
+
136
+
137
+
138
+
139
+
140
+
141
+
142
+ def upload_from_file(self):
143
+ pass
144
+
145
+ def upload_from_url(self):
146
+ pass
147
+
148
+ def upload_from_rdflib(self, rdflib_graph):
149
+
150
+ pass
151
+
152
+ def upload_from_cimgraph(self, network:GraphModel):
153
+
154
+ class_list = list(network.graph.keys())
155
+ for cim_class in class_list:
156
+ # Get JSON-LD representation of model
157
+ table = network.cim_dump(cim_class)
158
+ fields = cim_class.__dataclass_fields__
159
+ # Insert each power system object in CIMantic Graphs model
160
+ for obj in table.values():
161
+ # Create SQL Query
162
+ sql_insert = f"INSERT INTO {cim_class.__name__} ("
163
+ sql_values = " VALUES ("
164
+ sql_params = [self.username, int(time.time())]
165
+
166
+ sql_insert = sql_insert + "username, timestamp, "
167
+ sql_values = sql_values + f"%s, %s, "
168
+ # Iterate through each attribute
169
+ for attr in list(obj.keys()):
170
+ sql_insert = sql_insert + f"_{attr}, "
171
+ sql_values = sql_values + f"%s, "
172
+
173
+ if "List" in fields[attr].type:
174
+ sql_params.append(json.dumps(obj[attr])) # JSON-LD List of RDF links
175
+ elif "float" in fields[attr].type:
176
+ sql_params.append(obj[attr]) # float
177
+ else:
178
+ sql_params.append(str(obj[attr])) # free text
179
+
180
+ sql_insert = sql_insert[:-2] # remove extra punctuation
181
+ sql_insert = sql_insert + ")"
182
+ sql_values = sql_values[:-2] # remove extra punctuation
183
+ sql_values = sql_values + ")"
184
+ sql_query = sql_insert + sql_values # append query message
185
+ sql_params = tuple(sql_params)
186
+ self.cursor.execute(sql_query, sql_params) # insert all CIM objects
@@ -0,0 +1,69 @@
1
+ from __future__ import annotations
2
+ import logging
3
+ import subprocess
4
+
5
+ from typing import Dict, List, Optional
6
+
7
+
8
+ from neo4j import GraphDatabase
9
+ from neo4j.exceptions import DriverError, Neo4jError
10
+
11
+ from cimloader.databases import ConnectionInterface, ConnectionParameters, Parameter, QueryResponse
12
+
13
+ import rdflib
14
+ # from rdflib import Graph, Namespace
15
+ from rdflib.namespace import RDF
16
+
17
+ _log = logging.getLogger(__name__)
18
+
19
+ class Neo4jConnection(ConnectionInterface):
20
+ def __init__(self, connection_parameters):
21
+ self.connection_parameters = connection_parameters
22
+ self.cim_profile = connection_parameters.cim_profile
23
+ self.namespace = connection_parameters.namespace
24
+ self.url = connection_parameters.url
25
+ self.username = connection_parameters.username
26
+ self.password = connection_parameters.password
27
+ self.database = connection_parameters.database
28
+ self.container = connection_parameters.container
29
+ self.driver = None
30
+
31
+ def connect(self):
32
+ if not self.driver:
33
+ self.driver = GraphDatabase.driver(self.url, auth=(self.username, self.password))
34
+ self.driver.verify_connectivity()
35
+
36
+ def disconnect(self):
37
+ self.driver.close()
38
+ self.driver = None
39
+
40
+ def execute(self, query_message: str) -> QueryResponse:
41
+ self.connect()
42
+
43
+ try:
44
+ records, summary, keys = self.driver.execute_query(query_message, database_=self.database )
45
+ return records, summary, keys
46
+ # Capture any errors along with the query and data for traceability
47
+ except (DriverError, Neo4jError) as exception:
48
+ _log.error("%s raised an error: \n%s", query_message, exception)
49
+
50
+ def configure(self):
51
+ if self.cim_profile is not None and self.namespace is not None:
52
+ # self.execute("CALL n10s.nsprefixes.add(\""+self.cim_profile+"\",\""+self.namespace+"\");")
53
+ self.execute("CREATE CONSTRAINT n10s_unique_uri FOR (r:Resource) REQUIRE r.uri IS UNIQUE;")
54
+
55
+ else:
56
+ _log.exception("CIM profile and namespace must be defined in ConnectionParameters")
57
+
58
+ graph_config = """call n10s.graphconfig.init({
59
+ handleMultival: "OVERWRITE",
60
+ handleVocabUris: "IGNORE",
61
+ keepCustomDataTypes: true,
62
+ handleRDFTypes: "LABELS"})"""
63
+ self.execute(graph_config)
64
+
65
+
66
+
67
+ def drop_all(self):
68
+ self.execute("MATCH (n) DETACH DELETE n")
69
+ self.execute("DROP CONSTRAINT n10s_unique_uri")
File without changes
File without changes
File without changes
File without changes
@@ -0,0 +1,66 @@
1
+ from cimloader.databases import ConnectionInterface, ConnectionParameters
2
+
3
+ import io
4
+ import zipfile
5
+ import os
6
+ import requests
7
+ import logging
8
+
9
+ _log = logging.getLogger(__name__)
10
+
11
+ class NAERM(ConnectionInterface):
12
+ def __init__(self, connection_parameters:ConnectionParameters):
13
+ self.connection_parameters = connection_parameters
14
+ self.url = connection_parameters.url
15
+
16
+ def connect(self):
17
+ pass
18
+
19
+ def disconnect(self):
20
+ pass
21
+
22
+ def download_case(self, case_uuid, destination):
23
+ casefile_url = self.url + "/uuid/" + case_uuid
24
+ case_info = requests.get(casefile_url).json()
25
+ file_url = case_info["file_url"]
26
+ file_path = f"{destination}/{case_uuid}"
27
+
28
+ r = requests.get(file_url)
29
+ z = zipfile.ZipFile(io.BytesIO(r.content))
30
+ z.extractall(file_path)
31
+
32
+
33
+
34
+ def download_all_dss(self, destination, limit:int = 10000):
35
+ counter = 0
36
+ case_list = requests.get(self.url).json()
37
+ case_uuid_list = []
38
+ case_uuid_names = {}
39
+ for case in case_list:
40
+ if "simulator_type" in case["case"]:
41
+ if case["case"]["simulator_type"] == "opendss":
42
+ counter = counter+1
43
+ case_uuid_list.append(case["bes_case_uuid"])
44
+ case_uuid_names[case["bes_case_uuid"]] = case["case"]["feeder_name"]
45
+
46
+ if counter == limit:
47
+ break
48
+
49
+ counter = 0
50
+ for case_uuid in case_uuid_list:
51
+ counter = counter+1
52
+ casefile_url = self.url + "/uuid/" + case_uuid
53
+ case_info = requests.get(casefile_url).json()
54
+ file_url = case_info["file_url"]
55
+ file_path = f"{destination}/{case_uuid}"
56
+
57
+ r = requests.get(file_url)
58
+ z = zipfile.ZipFile(io.BytesIO(r.content))
59
+ z.extractall(file_path)
60
+
61
+ print(f"Successfully downloaded {counter} case files")
62
+
63
+ return case_uuid_list, case_uuid_names
64
+
65
+
66
+
File without changes
@@ -0,0 +1,2 @@
1
+ from cimloader.uploaders.blazegraph import BlazegraphUploader
2
+ from cimloader.uploaders.neo4j import Neo4jUploader
@@ -0,0 +1,25 @@
1
+ import logging
2
+ import subprocess
3
+
4
+ from cimloader.databases import ConnectionInterface, QueryResponse
5
+ from cimloader.databases.blazegraph import BlazegraphConnection
6
+ from SPARQLWrapper import JSON, POST, SPARQLWrapper
7
+
8
+ _log = logging.getLogger(__name__)
9
+
10
+ class BlazegraphUploader(ConnectionInterface):
11
+ def __init__(self, connection_params: ConnectionInterface) -> None:
12
+ self.url = connection_params.url
13
+ self.connection_params = connection_params
14
+ self.connection = BlazegraphConnection(connection_params)
15
+ self.connection.connect()
16
+
17
+ def upload_from_file(self, filepath:str, filename:str) -> None:
18
+ subprocess.call(["curl", "-s", "-D-", "-H", "Content-Type: application/xml", "--upload-file", f"{filepath}/{filename}", "-X", "Post", self.url])
19
+
20
+ def upload_from_url(self):
21
+ pass
22
+
23
+ def upload_from_rdflib(self, rdflib_graph):
24
+
25
+ pass
File without changes
File without changes
@@ -0,0 +1,50 @@
1
+ import logging
2
+ import subprocess
3
+
4
+ from cimloader.databases import ConnectionInterface, QueryResponse
5
+ from cimloader.databases.neo4j import Neo4jConnection
6
+ from SPARQLWrapper import JSON, POST, SPARQLWrapper
7
+
8
+ _log = logging.getLogger(__name__)
9
+
10
+ class Neo4jUploader(ConnectionInterface):
11
+ def __init__(self, connection_params: ConnectionInterface) -> None:
12
+ self.url = connection_params.url
13
+ self.connection_params = connection_params
14
+ self.container = connection_params.container
15
+ self.connection = Neo4jConnection(connection_params)
16
+ self.connection.connect()
17
+
18
+
19
+ def upload_from_file(self, filename, filepath):
20
+ if '.xml' in filename or '.XML' in filename:
21
+ format = 'RDF/XML'
22
+ elif '.ttl' in filename:
23
+ #TODO
24
+ pass
25
+
26
+ if self.container:
27
+ subprocess.call(["docker", "cp", f"{filepath}/{filename}", f"{self.container}:/var/lib/neo4j/import/{filename}"])
28
+ records=self.connection.execute(f"""call n10s.rdf.import.fetch( "file:///var/lib/neo4j/import//{filename}", "{format}"); """)
29
+ else:
30
+ records=self.connection.execute(f"""call n10s.rdf.import.fetch( "file://{filepath}/{filename}", "{format}"); """)
31
+ return records
32
+
33
+ def upload_from_url(self, url):
34
+ if '.xml' in url or '.XML' in url:
35
+ format = 'RDF/XML'
36
+ elif '.ttl' in url:
37
+ pass
38
+ records=self.connection.execute(f'''call n10s.rdf.import.fetch("{url}", "{format}"); ''')
39
+ return records
40
+
41
+ def upload_from_rdflib(self, rdflib_graph):
42
+
43
+ pass
44
+
45
+ def upload_from_cimgraph(self):
46
+ pass
47
+
48
+ def upload_from_rdflib(self, rdflib_graph):
49
+
50
+ pass
@@ -0,0 +1,37 @@
1
+ [tool.poetry]
2
+ name = "cim-loader"
3
+ version = "0.1.0b0"
4
+ description = "CIM Database Uploade & Downloader"
5
+ authors = ["A. Anderson <19935503+AAndersn@users.noreply.github.com>"]
6
+ packages = [
7
+ { include = "cimloader" }
8
+ ]
9
+ readme = "README.md"
10
+
11
+ [tool.poetry.dependencies]
12
+ python = ">=3.8.1,<4.0"
13
+ SPARQLWrapper = "^2.0.0"
14
+ neo4j = "^5.10.0"
15
+ rdflib = "^7.0.0"
16
+ rdflib-neo4j = "^0.0.1b6"
17
+ #OpenDSSDirect.py = "^0.8.2"
18
+ requests = "^2.28.0"
19
+ mysql-connector-python = "^8.0.0"
20
+
21
+ cim-graph = "^0.1.5a0"
22
+ #git = "https://github.com/PNNL-CIM-Tools/CIM-Graph/tree/main", branch = "v_1_0_0a", optional = true }
23
+
24
+ [tool.poetry.extras]
25
+ gridappsd = ["gridappsd-python"]
26
+
27
+ [tool.poetry.group.dev.dependencies]
28
+ pre-commit = "^2.17.0"
29
+ xsdata = {extras = ["cli"], version = "^22.2"}
30
+ xsdata-plantuml = "^21.4"
31
+ graphviz = "^0.19.1"
32
+ ipykernel = "^6.25.1"
33
+
34
+
35
+ [build-system]
36
+ requires = ["poetry-core>=1.2.0"]
37
+ build-backend = "poetry.core.masonry.api"