aemo-mdff-reader 2.0.2__py3-none-any.whl

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.
File without changes
@@ -0,0 +1,107 @@
1
+ """Backward-compatible NEMReader facade.
2
+
3
+ The original :class:`NEMReader` class built an in-memory tree of records
4
+ and walked it twice (once to build, once to flatten). The new
5
+ implementation streams through :mod:`aemo_mdff_reader.parser` and only
6
+ materialises results when the user asks for them — typically to a
7
+ DataFrame or CSV.
8
+
9
+ The legacy method names are preserved so existing code continues to
10
+ work; new code should prefer :func:`aemo_mdff_reader.parser.parse` directly.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import Any, Iterable, Iterator, List, Optional, Sequence
16
+
17
+ from .parser import Columns, parse, to_columns, to_dataframe, write_csv
18
+ from .types import IntervalReading
19
+
20
+ # Kept for backward compatibility with the old INTERVAL_DATA_OUTPUT_HEADERS export.
21
+ INTERVAL_DATA_OUTPUT_HEADERS = [
22
+ "NMI",
23
+ "MeterSerial",
24
+ "Register",
25
+ "Date",
26
+ "Interval",
27
+ "IntervalLength",
28
+ "UOM",
29
+ "IntervalValue",
30
+ "Quality",
31
+ "UpdateDatetime",
32
+ ]
33
+
34
+
35
+ class NEMReader:
36
+ """Reads NEM12 / NEM13 files using the AEMO MDFF specification (v2.6).
37
+
38
+ See :mod:`aemo_mdff_reader.spec` for the spec version, effective date,
39
+ and the allowed-value enumerations referenced in the spec.
40
+ """
41
+
42
+ INTERVAL_DATA_OUTPUT_HEADERS = INTERVAL_DATA_OUTPUT_HEADERS
43
+
44
+ def __init__(self) -> None:
45
+ self._readings: Optional[List[IntervalReading]] = None
46
+ self._source_filename: Optional[str] = None
47
+
48
+ @property
49
+ def filename(self) -> Optional[str]:
50
+ return self._source_filename
51
+
52
+ @property
53
+ def readings(self) -> List[IntervalReading]:
54
+ if self._readings is None:
55
+ raise RuntimeError("Call read_from_file() or read_from_array() first.")
56
+ return self._readings
57
+
58
+ def read_from_array(self, array: Iterable[Sequence[str]]) -> None:
59
+ """Reads from an iterable of pre-split rows."""
60
+ self._readings = list(parse(array))
61
+
62
+ def read_from_file(self, filename: str) -> None:
63
+ """Read a NEM12 CSV file from disk and buffer every interval.
64
+
65
+ Memory note: this materialises the full reading list, so peak
66
+ usage scales with the file. For files larger than a few hundred
67
+ MiB use :func:`aemo_mdff_reader.parse` (streaming, O(1) memory) or
68
+ :func:`aemo_mdff_reader.iter_dataframes` (bounded, O(chunk_size))
69
+ instead.
70
+ """
71
+ self._source_filename = filename
72
+ self._readings = list(parse(filename))
73
+
74
+ def __iter__(self) -> Iterator[IntervalReading]:
75
+ """Iterate over the buffered readings.
76
+
77
+ Lets callers write ``for r in reader: ...`` after
78
+ :meth:`read_from_file` or :meth:`read_from_array` instead of
79
+ going through ``reader.readings``.
80
+ """
81
+ return iter(self.readings)
82
+
83
+ def __len__(self) -> int:
84
+ """Number of buffered readings (after ``read_from_file``)."""
85
+ return len(self.readings)
86
+
87
+ def to_dataframe(self) -> Any:
88
+ """Materialise readings into a pandas DataFrame.
89
+
90
+ Requires the ``pandas`` extra. When the buffered file path is
91
+ known, this re-parses via the columnar fast path for a 2–4×
92
+ speedup over iterating the in-memory readings list.
93
+ """
94
+ if self._source_filename is not None:
95
+ return to_dataframe(self._source_filename)
96
+ return to_dataframe(self.readings)
97
+
98
+ def to_csv(self, filename: str) -> int:
99
+ """Write readings as a flat CSV file. Returns the row count."""
100
+ return write_csv(self.readings, filename)
101
+
102
+ def to_columns(self) -> Columns:
103
+ """Return readings as a dict of column lists (no pandas required)."""
104
+ return to_columns(self.readings)
105
+
106
+
107
+ __all__ = ["INTERVAL_DATA_OUTPUT_HEADERS", "NEMReader"]
@@ -0,0 +1,242 @@
1
+ """Constants from the AEMO MDFF specification (NEM12 / NEM13).
2
+
3
+ This module mirrors the enumerations and allowed-value lists in the
4
+ AEMO Meter Data File Format Specification — NEM12 & NEM13, v2.6,
5
+ effective 29 September 2024. The spec is the source of truth; this
6
+ module is a convenience surface for callers who want to validate
7
+ parsed records against it.
8
+
9
+ The parser itself does not enforce these enumerations strictly — it
10
+ accepts any well-formed CSV — so downstream code is free to apply as
11
+ much or as little validation as it needs.
12
+
13
+ Reference (always points at the current AEMO publication):
14
+ https://www.aemo.com.au/energy-systems/electricity/national-electricity-market-nem/market-operations/retail-and-metering/metering-procedures-guidelines-and-processes
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from typing import Dict, FrozenSet
20
+
21
+ SPEC_VERSION = "2.6"
22
+ SPEC_EFFECTIVE_DATE = "2024-09-29"
23
+ SPEC_DOCUMENT_TITLE = "Meter Data File Format Specification NEM12 & NEM13"
24
+ SPEC_URL = (
25
+ "https://www.aemo.com.au/energy-systems/electricity/"
26
+ "national-electricity-market-nem/market-operations/retail-and-metering/"
27
+ "metering-procedures-guidelines-and-processes"
28
+ )
29
+
30
+
31
+ # -- Section 4.3, NMI data details (200) ------------------------------------
32
+
33
+ #: IntervalLength values permitted by the spec, in minutes.
34
+ #: The parser also accepts other divisors of 1440 (e.g. 1, 60) for tolerance
35
+ #: with non-conforming files; pass through this constant if you want to
36
+ #: reject anything outside the spec.
37
+ ALLOWED_INTERVAL_LENGTHS: FrozenSet[int] = frozenset({5, 15, 30})
38
+
39
+
40
+ # -- Section 5.3, Accumulation meter data (250) -----------------------------
41
+
42
+ #: DirectionIndicator allowed values per the spec.
43
+ DIRECTION_INDICATORS: Dict[str, str] = {
44
+ "I": "Import (energy flows from the connection point to the grid)",
45
+ "E": "Export (energy flows from the grid to the connection point)",
46
+ }
47
+
48
+
49
+ # -- Appendix A, Transaction code flags (500 / 550) -------------------------
50
+
51
+ TRANSACTION_CODES: Dict[str, str] = {
52
+ "A": "Alteration",
53
+ "C": "Meter Reconfiguration",
54
+ "G": "Re-energisation",
55
+ "D": "De-energisation",
56
+ "E": "Estimate",
57
+ "N": "Normal Read",
58
+ "O": "Other (use when the original TransCode is unavailable, e.g. Historical Data)",
59
+ "S": "Special Read",
60
+ "R": "Removal of meter",
61
+ }
62
+
63
+
64
+ # -- Appendix B, allowed UOM values -----------------------------------------
65
+
66
+ #: Units of measure allowed in the UOM field. Comparison is case-insensitive
67
+ #: per the spec, so callers should normalise before lookup.
68
+ UOM_VALUES: FrozenSet[str] = frozenset(
69
+ {
70
+ "MWh",
71
+ "kWh",
72
+ "Wh",
73
+ "MVArh",
74
+ "kVArh",
75
+ "VArh",
76
+ "MVAr",
77
+ "kVAr",
78
+ "VAr",
79
+ "MW",
80
+ "kW",
81
+ "W",
82
+ "MVAh",
83
+ "kVAh",
84
+ "VAh",
85
+ "MVA",
86
+ "kVA",
87
+ "VA",
88
+ "kV",
89
+ "V",
90
+ "kA",
91
+ "A",
92
+ "pf",
93
+ }
94
+ )
95
+
96
+
97
+ # -- Appendix C, Quality flags ----------------------------------------------
98
+
99
+ QUALITY_FLAGS: Dict[str, str] = {
100
+ "A": "Actual Metering Data (no method flag required)",
101
+ "E": "Forward estimated data (method flag required, no reason code)",
102
+ "F": "Final substituted data (method flag and reason code required)",
103
+ "S": "Substituted data (method flag and reason code required)",
104
+ "V": (
105
+ "Variable data — only valid as the QualityMethod of a 300 record "
106
+ "to indicate that 400 rows carry per-interval flags. Not valid in "
107
+ "NEM13 files."
108
+ ),
109
+ }
110
+
111
+
112
+ # -- Appendix E, current reason codes ---------------------------------------
113
+
114
+ REASON_CODES: Dict[int, str] = {
115
+ 0: "Free text description",
116
+ 1: "Meter/equipment changed",
117
+ 2: "Extreme weather conditions",
118
+ 3: "Quarantined premises",
119
+ 5: "Blank screen",
120
+ 6: "De-energised premises",
121
+ 7: "Unable to locate meter",
122
+ 8: "Vacant premises",
123
+ 9: "Under investigation",
124
+ 10: "Lock damaged unable to open",
125
+ 11: "In wrong route",
126
+ 12: "Locked premises",
127
+ 13: "Locked gate",
128
+ 14: "Locked meter box",
129
+ 15: "Overgrown vegetation",
130
+ 17: "Unsafe equipment/location",
131
+ 18: "Read less than previous",
132
+ 20: "Damaged equipment/panel",
133
+ 21: "Main switch off",
134
+ 22: "Meter/equipment seals missing",
135
+ 23: "Reader error",
136
+ 24: "Substituted/replaced data (data correction)",
137
+ 25: "Unable to locate premises",
138
+ 26: "Negative consumption (generation)",
139
+ 27: "RoLR",
140
+ 28: "CT/VT fault",
141
+ 29: "Relay faulty/damaged",
142
+ 31: "Not all meters read",
143
+ 32: "Re-energised without readings",
144
+ 33: "De-energised without readings",
145
+ 34: "Meter not in handheld",
146
+ 35: "Timeswitch faulty/reset required",
147
+ 36: "Meter high/ladder required",
148
+ 37: "Meter under churn",
149
+ 38: "Unmarried lock",
150
+ 39: "Reverse energy observed",
151
+ 40: "Unrestrained livestock",
152
+ 41: "Faulty Meter display/dials",
153
+ 42: "Channel added/removed",
154
+ 43: "Power outage",
155
+ 44: "Meter testing",
156
+ 45: "Readings failed to validate",
157
+ 47: "Refused access",
158
+ 48: "Dog on premises",
159
+ 51: "Installation demolished",
160
+ 52: "Access — blocked",
161
+ 53: "Pests in meter box",
162
+ 54: "Meter box damaged/faulty",
163
+ 55: "Dials obscured",
164
+ 60: "Illegal connection",
165
+ 61: "Equipment tampered",
166
+ 62: "NSRD window expired",
167
+ 64: "Key required",
168
+ 65: "Wrong key provided",
169
+ 67: "Transfer",
170
+ 68: "Zero consumption",
171
+ 69: "Reading exceeds Substitute",
172
+ 71: "Probe read error",
173
+ 72: "Re-calculated based on Actual Metering Data",
174
+ 73: "Low consumption",
175
+ 74: "High consumption",
176
+ 75: "Customer read",
177
+ 76: "Communications fault",
178
+ 77: "Estimation Forecast",
179
+ 78: "Null Data",
180
+ 79: "Power Outage Alarm",
181
+ 80: "Short Interval Alarm",
182
+ 81: "Long Interval Alarm",
183
+ 87: "Reset occurred",
184
+ 89: "Time reset occurred",
185
+ # Added in v2.6 (ICF_054 substitution type review, July 2023 REMP):
186
+ 100: "Incorrect Meter Multiplier",
187
+ 101: "Temporarily Connection Point unmetered",
188
+ 102: "Customer By-Pass",
189
+ 103: "Network By-Pass",
190
+ 104: "Transposed Channel",
191
+ 105: "Transposed Channel — UoM Correction",
192
+ 106: "Transposed Channel — Reverse Polarity",
193
+ 107: "Transposed Meter",
194
+ 108: "Network by-pass extreme weather",
195
+ 109: "Defined load method",
196
+ }
197
+
198
+
199
+ # -- Appendix F, obsolete reason codes (Historical Data only) ---------------
200
+
201
+ OBSOLETE_REASON_CODES: Dict[int, str] = {
202
+ 4: "Dangerous dog",
203
+ 16: "Noxious weeds at premises preventing access",
204
+ 19: "Consumer wanted",
205
+ 30: "Meter stop switch on",
206
+ 46: "Extreme weather/hot",
207
+ 49: "Wet paint",
208
+ 50: "Wrong tariff",
209
+ 58: "Meter ok — supply failure",
210
+ 70: "Probe reports tampering",
211
+ 82: "CRC error",
212
+ 83: "RAM checksum error",
213
+ 84: "ROM checksum error",
214
+ 85: "Data missing alarm",
215
+ 86: "Clock error alarm",
216
+ 88: "Watchdog timeout alarm",
217
+ 90: "Test mode",
218
+ 91: "Load control",
219
+ 92: "Added interval (data correction)",
220
+ 93: "Replaced interval (data correction)",
221
+ 94: "Estimated interval (data correction)",
222
+ 95: "Pulse overflow alarm",
223
+ 96: "Data out of limits",
224
+ 97: "Excluded data",
225
+ 98: "Parity error",
226
+ 99: "Energy type (register changed)",
227
+ }
228
+
229
+
230
+ __all__ = [
231
+ "ALLOWED_INTERVAL_LENGTHS",
232
+ "DIRECTION_INDICATORS",
233
+ "OBSOLETE_REASON_CODES",
234
+ "QUALITY_FLAGS",
235
+ "REASON_CODES",
236
+ "SPEC_DOCUMENT_TITLE",
237
+ "SPEC_EFFECTIVE_DATE",
238
+ "SPEC_URL",
239
+ "SPEC_VERSION",
240
+ "TRANSACTION_CODES",
241
+ "UOM_VALUES",
242
+ ]
@@ -0,0 +1,22 @@
1
+ """Optional SQL persistence layer.
2
+
3
+ Importing this subpackage does NOT eagerly import :mod:`pymysql`.
4
+ The dependency is only required when you actually instantiate
5
+ :class:`Storer`. Install with ``pip install aemo-mdff-reader[mysql]``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from .query import COLUMNS, QueryBuilder
11
+
12
+ __all__ = ["COLUMNS", "DBCredentials", "QueryBuilder", "Storer"]
13
+
14
+
15
+ def __getattr__(name):
16
+ # Lazy import so that ``import aemo_mdff_reader.sql`` does not require pymysql
17
+ # to be installed unless the caller actually uses the Storer.
18
+ if name in ("Storer", "DBCredentials"):
19
+ from .store import DBCredentials, Storer
20
+
21
+ return {"Storer": Storer, "DBCredentials": DBCredentials}[name]
22
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,102 @@
1
+ """Contains classes and functions used for building queries in SQL"""
2
+ # import statements
3
+
4
+ IMPORT_TABLE = "imports"
5
+ NMI_TABLE = "nmi"
6
+ CHANNEL_TABLE = "channel"
7
+ INTERVAL_TYPE_TABLE = "interval_type"
8
+ INTERVAL_TABLE = "interval"
9
+ INTERVAL_DATA_TABLE = "interval_data"
10
+ INTERVAL_EVENT_TABLE = "interval_event"
11
+ INTERVAL_B2B_TABLE = "interval_b2b"
12
+ ACCUMULATION_DATA_TABLE = "accumulation_data"
13
+ ACCUMULATION_B2B_TABLE = "accumulation_b2b"
14
+
15
+ COLUMNS = {
16
+ IMPORT_TABLE: ["filename", "VersionHeader", "DateTime", "FromParticipant", "ToParticipant"],
17
+ NMI_TABLE: ["NMI", "NMIConfiguration"],
18
+ CHANNEL_TABLE: [
19
+ "NMI",
20
+ "RegisterID",
21
+ "NMISuffix",
22
+ "MDMDataStreamIdentifier",
23
+ "MeterSerialNumber",
24
+ ],
25
+ INTERVAL_TYPE_TABLE: [
26
+ "UOM",
27
+ "IntervalLength",
28
+ "NextScheduledReadDate",
29
+ "channelID",
30
+ "importID",
31
+ ],
32
+ INTERVAL_TABLE: [
33
+ "IntervalDate",
34
+ "QualityMethod",
35
+ "ReasonCode",
36
+ "ReasonDescription",
37
+ "UpdateDateTime",
38
+ "MSATSLoadDateTime",
39
+ "typeID",
40
+ ],
41
+ INTERVAL_DATA_TABLE: ["valueID", "value", "intervalID"],
42
+ INTERVAL_EVENT_TABLE: [
43
+ "StartInterval",
44
+ "EndInterval",
45
+ "QualityMethod",
46
+ "ReasonCode",
47
+ "ReasonDescription",
48
+ "intervalID",
49
+ ],
50
+ INTERVAL_B2B_TABLE: ["TransCode", "RetServiceOrder", "ReadDateTime", "IndexRead", "eventID"],
51
+ ACCUMULATION_DATA_TABLE: [],
52
+ ACCUMULATION_B2B_TABLE: [],
53
+ }
54
+
55
+
56
+ class QueryBuilder:
57
+ """
58
+ Builds a query from given information.
59
+ """
60
+
61
+ COLUMN_GEN = 'NULLIF("%s", "")'
62
+ NUM_WRAPPER = "CAST(%s AS %s)"
63
+ DECIMAL_TYPE_1 = "DECIMAL(18, 3)"
64
+ DECIMAL_TYPE_2 = "DECIMAL(22, 5)"
65
+ INT_TYPE = "INT"
66
+ INSERT_QUERY = """INSERT INTO %s
67
+ (%s)
68
+ """
69
+ VALUES_QUERY = """VALUES (%s)"""
70
+
71
+ @staticmethod
72
+ def insert_query(table, data):
73
+ """
74
+ Insert query builder
75
+ """
76
+ query = "INSERT IGNORE INTO `%s`\n" % table
77
+ query += "(" + ",".join(["`" + item + "`" for item in COLUMNS[table]]) + ")" + "\n"
78
+ query += "VALUES "
79
+ for instance in data:
80
+ list_value = [
81
+ (
82
+ QueryBuilder.COLUMN_GEN % value
83
+ if "@" not in value and "STR" not in value
84
+ else value
85
+ )
86
+ for value in instance
87
+ ]
88
+ query += "("
89
+ for value in list_value:
90
+ query += value + ", "
91
+ query = query[:-2] + "),\n"
92
+ query = query[:-2] + ";"
93
+ return query
94
+
95
+ @staticmethod
96
+ def get_id_query():
97
+ """
98
+ Most recent ID retriever
99
+ """
100
+ query = "LAST_INSERT_ID();"
101
+
102
+ return query
@@ -0,0 +1,178 @@
1
+ """Storage for NEM12-13 within SQL Schema as specified."""
2
+
3
+ # import statements
4
+
5
+ import pymysql.cursors
6
+
7
+ from .query import *
8
+
9
+ # Load queries once session is imported
10
+ QUERY_FOLDER = "queries"
11
+ QUERIES = {}
12
+
13
+
14
+ class Storer:
15
+ """
16
+ Stores Reader into the DB
17
+ """
18
+
19
+ def __init__(self, credentials):
20
+ if credentials is not None:
21
+ self.__connection = self.connect(**credentials)
22
+
23
+ @staticmethod
24
+ def connect(host, port, user, password, schema, directory):
25
+ """
26
+ Connects to a MySQL database with the
27
+ given credentials.
28
+
29
+ Args:
30
+ db_credentials (DBCredentials): The credentials
31
+ of the DB to connect to
32
+ Returns:
33
+ pymysql.connect. A connection to the DB
34
+ """
35
+ # assert isinstance(db_credentials, DBCredentials)
36
+ # connect to server
37
+ connection = pymysql.connect(
38
+ host=host,
39
+ user=user,
40
+ password=password,
41
+ db=schema,
42
+ charset="utf8mb4",
43
+ autocommit=True,
44
+ cursorclass=pymysql.cursors.Cursor, # TODO: check if suitable cursor
45
+ )
46
+ return connection
47
+
48
+ def load_reader(self, reader):
49
+ """
50
+ loads reader
51
+ """
52
+ print(reader.filename)
53
+ with self.__connection.cursor() as cursor:
54
+ # first store header and filename
55
+ data = [
56
+ [
57
+ reader.filename.replace("\\", "/"),
58
+ reader.data.VersionHeader,
59
+ f'STR_TO_DATE("{reader.data.DateTime}", "%Y%m%d%k%i%s")',
60
+ reader.data.FromParticipant,
61
+ reader.data.ToParticipant,
62
+ ]
63
+ ]
64
+ query = QueryBuilder.insert_query(IMPORT_TABLE, data)
65
+ resultset = cursor.execute(query)
66
+ if resultset == 0:
67
+ return None
68
+ cursor.execute("SELECT importID FROM imports WHERE filename LIKE %s", (data[0][0]))
69
+ importID = str(cursor.fetchone()[0])
70
+ print(importID)
71
+ channelID = None
72
+ typeID = None
73
+ intervalID = None
74
+ # now store nmi, channels and interval type
75
+ data_nmi = []
76
+ data_channel = []
77
+ data_interval_type = []
78
+ data_intervals = []
79
+ for subrecord in reader.data.subrecords:
80
+ if isinstance(subrecord, list):
81
+ continue
82
+ if subrecord.RecordIndicator == "200":
83
+ data_interval = []
84
+ data_nmi.append([subrecord.NMI, subrecord.NMIConfiguration])
85
+ data_channel.append(
86
+ [
87
+ subrecord.NMI,
88
+ subrecord.RegisterID,
89
+ subrecord.NMISuffix,
90
+ subrecord.MDMDataStreamIdentifier,
91
+ subrecord.MeterSerialNumber,
92
+ ]
93
+ )
94
+ data_interval_type.append(
95
+ [
96
+ subrecord.UOM,
97
+ subrecord.IntervalLength,
98
+ f'STR_TO_DATE("{subrecord.NextScheduledReadDate}", "%Y%m%d%k%i%s")',
99
+ channelID,
100
+ importID,
101
+ ]
102
+ )
103
+ # add each interval
104
+ for subsubrecord in subrecord.subrecords:
105
+ if subsubrecord.RecordIndicator == "300":
106
+ interval_data = [
107
+ (
108
+ key[0].replace("IntervalValue", ""),
109
+ subsubrecord.__getattr__(key[0]),
110
+ intervalID,
111
+ )
112
+ for key in subsubrecord.intervals
113
+ ]
114
+ data_interval.append(
115
+ [
116
+ [
117
+ f'STR_TO_DATE("{subsubrecord.IntervalDate}", "%Y%m%d")',
118
+ subsubrecord.QualityMethod,
119
+ subsubrecord.ReasonCode,
120
+ subsubrecord.ReasonDescription,
121
+ f'STR_TO_DATE("{subsubrecord.UpdateDateTime}", "%Y%m%d%k%i%s")',
122
+ f'STR_TO_DATE("{subsubrecord.MSATSLoadDateTime}", "%Y%m%d%k%i%s")',
123
+ typeID,
124
+ ],
125
+ interval_data,
126
+ ]
127
+ )
128
+
129
+ data_intervals.append(data_interval)
130
+ query = QueryBuilder.insert_query(NMI_TABLE, data_nmi)
131
+ resultset = cursor.execute(query)
132
+
133
+ for row in range(len(data_channel)):
134
+ query = QueryBuilder.insert_query(CHANNEL_TABLE, [data_channel[row]])
135
+ resultset = cursor.execute(query)
136
+ query = "SELECT channelID FROM `nem`.`channel` WHERE `NMI` = %s AND `NMISuffix` = %s AND `MeterSerialNumber` = %s"
137
+ cursor.execute(
138
+ query, (data_channel[row][0], data_channel[row][2], data_channel[row][4])
139
+ )
140
+ channelID = str(cursor.fetchone()[0])
141
+ data_interval_type[row][-2] = channelID
142
+ cursor.execute(
143
+ QueryBuilder.insert_query(INTERVAL_TYPE_TABLE, [data_interval_type[row]])
144
+ )
145
+ cursor.execute(
146
+ """SELECT typeID FROM interval_type WHERE channelID = %s AND importID = %s""",
147
+ (channelID, importID),
148
+ )
149
+ typeID = str(cursor.fetchone()[0])
150
+ for interval_row in data_intervals[row]:
151
+ interval_row[0][-1] = typeID
152
+ query = QueryBuilder.insert_query(INTERVAL_TABLE, [interval_row[0]])
153
+ cursor.execute(query)
154
+ cursor.execute(
155
+ "SELECT intervalID FROM `interval` WHERE typeID = %s AND IntervalDate = %s"
156
+ % (typeID, interval_row[0][0])
157
+ )
158
+ intervalID = str(cursor.fetchone()[0])
159
+ interval_row[1] = [(val[0], val[1], intervalID) for val in interval_row[1]]
160
+ query = QueryBuilder.insert_query(INTERVAL_DATA_TABLE, list(interval_row[1]))
161
+ cursor.execute(query)
162
+
163
+ def close(self):
164
+ """
165
+ Closes the current connection
166
+ """
167
+ self.__connection.close()
168
+
169
+
170
+ class DBCredentials:
171
+ """Credentials for SQL Database"""
172
+
173
+ def __init__(self, host, port, schema, username, password):
174
+ self.host = host
175
+ self.port = port
176
+ self.schema = schema
177
+ self.username = username
178
+ self.password = password