sql-testing-tools 0.1.0__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.
@@ -0,0 +1,170 @@
1
+ import sqlite3
2
+ import os
3
+ from importlib import resources
4
+
5
+
6
+ __db=""
7
+
8
+ def setDBName(name: str):
9
+ global __db
10
+ print("Setting DB to: " + name)
11
+ with resources.path('sql_testing_tools', name) as db_path:
12
+ __db = str(db_path)
13
+ print(__db)
14
+
15
+
16
+ setDBName("bayern.db")
17
+
18
+ def run(sql: str):
19
+ con = sqlite3.connect(__db)
20
+ cur = con.cursor()
21
+ cur.execute(sql)
22
+ con.commit()
23
+ return cur
24
+
25
+ def runFromFile(path: str):
26
+ sql = getSQLFromFile(path)
27
+ if not sql:
28
+ raise Exception("\nSQL-Datei ist leer. Aufgabe wurde noch nicht bearbeitet.")
29
+
30
+
31
+ if sql.lower().find("drop") != -1:
32
+ raise Exception("Guter Versuch, aber die Datenbank wird bei jedem Upload zurückgesetzt ;)")
33
+
34
+ try:
35
+ res = run(sql)
36
+ except Exception as e:
37
+ raise Exception(f"\n\nSyntax-Fehler in der SQL-Abfrage:\n{str(e)}")
38
+
39
+ headers = [h[0] for h in res.description] if res.description else []
40
+ rows = res.fetchall() if res else []
41
+
42
+ return headers, rows
43
+
44
+
45
+ def runAndGetStringTable_fromFile(path: str, count: int = 5, maxLineLength: int = 85):
46
+ try:
47
+ headers, rows = runFromFile(path)
48
+ resultCount = len(rows)
49
+ rows = rows[:count]
50
+ s = ""
51
+
52
+ matrix = [] #[[]*(len(rows)+1)]*len(headers)
53
+
54
+ for col in range(len(headers)):
55
+ matrix.append([])
56
+ matrix[-1].append(headers[col])
57
+
58
+ if rows is not None:
59
+ for col in range(len(matrix)):
60
+ for row in range(0, len(rows)):
61
+ if len(rows[row]) > col:
62
+ matrix[col].append(rows[row][col])
63
+
64
+ for col in range(len(matrix)):
65
+ maxLength = max([len(str(x)) for x in matrix[col]])
66
+
67
+ for row in range(0, len(matrix[col])):
68
+ matrix[col][row] = str(matrix[col][row]).ljust(maxLength)
69
+
70
+ normalizedRows = []
71
+ spacing = " "
72
+ spacingLength = len(spacing) * (len(matrix[0]) - 1)
73
+
74
+ lineLength = sum([len(x[0]) for x in matrix]) + spacingLength
75
+
76
+
77
+
78
+ if lineLength > maxLineLength:
79
+ valLength = lineLength - spacingLength
80
+ maxColLength = int(maxLineLength/len(matrix))
81
+
82
+ for col in range(len(matrix)):
83
+ for row in range(0, len(matrix[col])):
84
+ cutContent = len(matrix[col][row].strip()) > maxColLength
85
+ if cutContent:
86
+ matrix[col][row] = matrix[col][row][:maxColLength-2] + ".."
87
+ else:
88
+ matrix[col][row] = matrix[col][row][:maxColLength]
89
+
90
+
91
+ for row in range(min(count+1, len(matrix[0]))):
92
+ normalizedRows.append(spacing.join([str(matrix[col][row]) for col in range(len(matrix))]))
93
+ normalizedRows.insert(1, len(normalizedRows[0])*"-")
94
+
95
+ s = "\n".join(normalizedRows[:count+2])
96
+ s += "\n" + normalizedRows[1]
97
+
98
+ if (resultCount > count):
99
+ s += "\n... " + str(resultCount - count) + " weitere Zeilen"
100
+ else:
101
+ s += "\n... keine weiteren Zeilen"
102
+
103
+ #longestLineInS = 85 # max([len(x.replace("\t", " ")) for x in s.split("\n")])
104
+
105
+ #s = "-" * longestLineInS + "\n" + s + "\n" + "-" * longestLineInS
106
+ s = "\n\nDiese Meldung sagt nichts über die Korrektheit der Abgabe aus!\nDie ersten " + str(
107
+ count) + " Zeilen des Ergebnisses der SQL-Abfrage:\n\n" + s
108
+
109
+ return s.replace("None", "NULL")
110
+ except Exception as ex:
111
+ raise ex
112
+
113
+ def getSQLFromFile(path: str):
114
+ try:
115
+ with open(path, "r") as f:
116
+ s = f.read()
117
+ if not s:
118
+ raise Exception("\nSQL-Datei ist leer. Aufgabe wurde noch nicht bearbeitet.")
119
+ return s
120
+ except FileNotFoundError:
121
+ raise Exception(f"\nSQL-Datei nicht gefunden! Überprüfe, dass der Name korrekt ist ({path.split('/')[-1]}) und die Datei nicht gelöscht oder in einen Unterordner verschoben wurde.")
122
+
123
+
124
+
125
+ def getWorkingDir():
126
+ return os.getcwd()
127
+
128
+
129
+ def getWorkingDirFiles():
130
+ return os.listdir()
131
+
132
+ def mapDatabaseTypes(t):
133
+ # Map SQLite types to Python types
134
+ type_mapping = {
135
+ "INTEGER": "int",
136
+ "TEXT": "str",
137
+ "VARCHAR": "str",
138
+ "REAL": "float",
139
+ "BLOB": "bytes",
140
+ "NUMERIC": "float",
141
+ "DOUBLE": "float"
142
+ }
143
+ t = t.upper()
144
+ t = type_mapping.get(t, t)
145
+
146
+ if "INT" in t:
147
+ t = "int"
148
+ elif "VARCHAR" in t:
149
+ t = "str"
150
+ elif "CHAR" in t:
151
+ t = "str"
152
+
153
+ return t
154
+
155
+
156
+
157
+
158
+ def getTableDict():
159
+ res = run("SELECT name FROM sqlite_master WHERE type='table';")
160
+ tables = res.fetchall()
161
+
162
+ table_dict = {}
163
+ for table in tables:
164
+ res = run(f"PRAGMA table_info({table[0]})")
165
+ r = res.fetchall()
166
+ if table[0] == "sqlite_sequence":
167
+ continue
168
+ table_dict[table[0]] = [[c[1], mapDatabaseTypes(c[2])] for c in r]
169
+
170
+ return table_dict
@@ -0,0 +1,410 @@
1
+ import os
2
+ from . import BaseAccess as Ba
3
+
4
+
5
+ try:
6
+ import sqlparse
7
+ except ImportError:
8
+ print("Trying to Install required module: sqlparse\n")
9
+ os.system('pip install sqlparse')
10
+ import sqlparse
11
+
12
+
13
+ try:
14
+ import requests
15
+ except ImportError:
16
+ print("Trying to Install required module: requests\n")
17
+ os.system('pip install requests')
18
+ import requests
19
+
20
+ from sqlparse.sql import Identifier, IdentifierList, Where, Comparison, Function, Parenthesis
21
+ from sqlparse.tokens import Keyword, DML, Name, Wildcard
22
+
23
+
24
+ def normalizeSQLQuery(query, baseDict):
25
+ try:
26
+ query = query.replace("\"", "'")
27
+ parsed = sqlparse.parse(query)[0]
28
+ parsed.tokens = [token for token in parsed.tokens if not token.is_whitespace]
29
+ except Exception as e:
30
+ raise Exception(f"\nSyntax-Fehler in der SQL-Abfrage.")
31
+
32
+ formatted_query = []
33
+ alias_map = {}
34
+
35
+ def process_identifier(identifier, alias_map, baseDict: dict):
36
+ if isinstance(identifier, Identifier):
37
+ if identifier.get_real_name() and identifier.get_parent_name() and identifier.get_parent_name().lower() in alias_map.keys():
38
+ return f"{alias_map[identifier.get_parent_name().lower()].lower()}.{identifier.get_real_name().lower()}"
39
+ elif identifier.get_real_name():
40
+ tables = findTableForColumn(baseDict, identifier.get_real_name(), alias_map.keys())
41
+ if len(tables) == 1:
42
+ alias_map[tables[0].lower()] = tables[0]
43
+ return f"{tables[0].lower()}.{identifier.get_real_name().lower()}"
44
+ else:
45
+ return f"{identifier.get_real_name().lower()}"
46
+ return str(identifier)
47
+
48
+ def process_select(select, alias_map, insideFunction=False):
49
+ select_tokens = []
50
+ for token in select.tokens:
51
+ if isinstance(token, IdentifierList):
52
+ for identifier in token.get_identifiers():
53
+ select_tokens.append(f"{process_identifier(identifier, alias_map, baseDict).lower()} as {identifier.get_real_name().lower()}")
54
+ elif isinstance(token, Identifier):
55
+ if insideFunction:
56
+ select_tokens.append(process_identifier(token, alias_map, baseDict).lower())
57
+ else:
58
+ select_tokens.append(f"{process_identifier(token, alias_map, baseDict).lower()} as {token.get_real_name().lower()}")
59
+ elif isinstance(token, Function):
60
+ #select_tokens.append(f"{token.get_name().lower()}({process_identifier(token.get_parameters(), alias_map, baseDict).lower()})")
61
+ for par in token.tokens:
62
+ if isinstance(par, Identifier):
63
+ select_tokens.append(par.get_name().lower()+"(")
64
+ if isinstance(par, Parenthesis):
65
+ select_tokens[-1] += process_select(par, alias_map, True)+") as funcResult"
66
+ elif token.ttype is Wildcard:
67
+ select_tokens.append("*")
68
+ else:
69
+ continue
70
+ select_tokens.sort()
71
+ return ",".join(select_tokens)
72
+
73
+ def process_from(from_, alias_map):
74
+ from_tokens = []
75
+ if hasattr(from_, 'tokens'):
76
+ for token in from_.tokens:
77
+ if isinstance(token, IdentifierList):
78
+ for identifier in token.get_identifiers():
79
+ alias_map[identifier.get_real_name().lower()] = identifier.get_alias() or identifier.get_real_name()
80
+ from_tokens.append(f"{identifier.get_real_name().lower()} {alias_map[identifier.get_real_name().lower()].lower()}")
81
+ elif isinstance(token, Identifier):
82
+ alias_map[token.get_real_name().lower()] = token.get_alias() or token.get_real_name()
83
+ from_tokens.append(f"{token.get_real_name().lower()} {alias_map[token.get_real_name().lower()].lower()}")
84
+ elif token.ttype is not None and token.ttype is Name:
85
+ alias_map[token.value.lower()] = token.value.lower()
86
+ from_tokens.append(f"{token.value.lower()} {alias_map[token.value.lower()].lower()}")
87
+ else:
88
+ continue
89
+ from_tokens.sort()
90
+ return ",".join(from_tokens)
91
+
92
+ def process_groupby(groupby_, alias_map):
93
+ groupby_tokens = []
94
+ if(isinstance(groupby_, Identifier)):
95
+ groupby_tokens.append(process_identifier(groupby_, alias_map, baseDict).lower())
96
+ elif isinstance(token, IdentifierList):
97
+ for identifier in token.get_identifiers():
98
+ groupby_tokens.append(process_identifier(identifier, alias_map, baseDict).lower())
99
+ groupby_tokens.sort()
100
+ return ",".join(groupby_tokens)
101
+
102
+ def process_condition(token, alias_map, baseDict):
103
+ left, operator, right = [t for t in token.tokens if not t.is_whitespace]
104
+
105
+ flipAllowed = True
106
+ leftLiteral = False
107
+ rightLiteral = False
108
+
109
+ if is_value(right):
110
+ flipAllowed = False
111
+ rightLiteral = True
112
+
113
+ if is_value(left):
114
+ left, right = right, left
115
+ if operator.value == ">":
116
+ operator.value = "<"
117
+ elif operator.value == "<":
118
+ operator.value = ">"
119
+ leftLiteral = rightLiteral
120
+ rightLiteral = True
121
+ flipAllowed = False
122
+
123
+ left = process_identifier(left, alias_map, baseDict)
124
+ right = process_identifier(right, alias_map, baseDict)
125
+
126
+ if flipAllowed and left.lower() >= right.lower():
127
+ left, right = right, left
128
+ if operator.value == ">":
129
+ operator = "<"
130
+ elif operator.value == "<":
131
+ operator = ">"
132
+
133
+ return f"{left if leftLiteral else left.lower()} {operator} {right if rightLiteral else right.lower()}"
134
+
135
+ def is_value(token):
136
+ return token.ttype in (sqlparse.tokens.Token.Literal.Number.Integer,
137
+ sqlparse.tokens.Token.Literal.Number.Float,
138
+ sqlparse.tokens.Token.Literal.String.Single,
139
+ sqlparse.tokens.Token.Literal.String.Symbol)
140
+
141
+ def process_paranthesis(parenthesis, alias_map, baseDict):
142
+ toks = []
143
+
144
+ bracketsRequired = True
145
+
146
+
147
+ for token in parenthesis.tokens:
148
+ if isinstance(token, Comparison):
149
+ toks.append(process_condition(token, alias_map, baseDict))
150
+ elif isinstance(token, Parenthesis):
151
+ toks.append(process_paranthesis(token, alias_map, baseDict))
152
+
153
+ elif token.value == '(':
154
+ toks.append(token.value)
155
+ else:
156
+ continue
157
+ x = parenthesis.flatten()
158
+ return " ".join(toks)
159
+
160
+ def process_where(where, alias_map, baseDict):
161
+ conditions = []
162
+ current_condition = []
163
+
164
+ for token in where.tokens:
165
+ if token.is_whitespace or (token.ttype is Keyword and token.value.upper() == "WHERE"):
166
+ continue
167
+
168
+ if token.ttype is Keyword and token.value.upper() in ('AND', 'OR'):
169
+ if current_condition:
170
+ conditions.append(''.join(str(t) for t in current_condition).strip())
171
+ current_condition = []
172
+ conditions.append(token.value.upper())
173
+ elif isinstance(token, Comparison):
174
+ current_condition.append(process_condition(token, alias_map, baseDict))
175
+ elif isinstance(token, Parenthesis):
176
+ current_condition.append(process_paranthesis(token, alias_map, baseDict))
177
+ else:
178
+ current_condition.append(token)
179
+
180
+ if current_condition:
181
+ conditions.append(''.join(str(t) for t in current_condition).strip())
182
+
183
+ sorted_conditions = []
184
+ current_group = []
185
+ last_connector = ""
186
+
187
+ if "OR" not in conditions:
188
+ for condition in conditions:
189
+ if condition == 'AND':
190
+ pass
191
+ else:
192
+ current_group.append(condition)
193
+
194
+ if current_group:
195
+ sorted_conditions.extend(sorted(current_group))
196
+
197
+ return " AND ".join(sorted_conditions)
198
+ else:
199
+ return " ".join(conditions)
200
+
201
+ where_index = query.upper().find('WHERE')
202
+ normalized_query = query[:where_index + 5] + ' ' + ' '.join(sorted_conditions)
203
+
204
+ def process_where_xx(where, alias_map, baseDict):
205
+ where_tokens = []
206
+ for token in where.tokens:
207
+
208
+ if isinstance(token, Comparison):
209
+ left, operator, right = [t for t in token.tokens if not t.is_whitespace]
210
+ left = process_identifier(left, alias_map, baseDict)
211
+ right = process_identifier(right, alias_map, baseDict)
212
+ if left.lower() >= right.lower():
213
+ left, right = right, left
214
+ if operator.value == ">":
215
+ operator = "<"
216
+ elif operator.value == "<":
217
+ operator = ">"
218
+
219
+ where_tokens.append(f"{left.lower()} {operator} {right.lower()}")
220
+ elif token.ttype is Keyword:
221
+ where_tokens.append(token.value.upper())
222
+ elif token.is_whitespace or (token.ttype is Keyword and token.value.upper() == "WHERE"):
223
+ continue
224
+ else:
225
+ where_tokens.append(str(token))
226
+ return " ".join(where_tokens)
227
+
228
+ # First pass to process FROM clause and populate alias_map
229
+ for token in parsed.tokens:
230
+ if token.is_whitespace:
231
+ continue
232
+ elif token.ttype is Keyword and token.value.upper() == 'FROM':
233
+ formatted_query.append('FROM')
234
+ pass
235
+ elif isinstance(token, Identifier) and formatted_query and formatted_query[-1] == 'FROM':
236
+ il = IdentifierList([token])
237
+ formatted_query.append(process_from(il, alias_map))
238
+ process_from(il, alias_map)
239
+ elif isinstance(token, IdentifierList) and formatted_query and formatted_query[-1] == 'FROM':
240
+ formatted_query.append(process_from(token, alias_map))
241
+ process_from(token, alias_map)
242
+
243
+ formatted_query = []
244
+
245
+ # Second pass to process SELECT and WHERE clauses
246
+ for token in parsed.tokens:
247
+ if token.is_whitespace:
248
+ continue
249
+ elif token.ttype is DML and token.value.upper() == 'SELECT':
250
+ formatted_query.append('SELECT')
251
+ elif token.ttype is Keyword and token.value.upper() == 'FROM':
252
+ formatted_query.append('FROM')
253
+ elif token.ttype is Keyword and token.value.upper() == 'GROUP BY':
254
+ formatted_query.append('GROUP BY')
255
+ elif (isinstance(token, IdentifierList) or isinstance(token, Identifier)) and formatted_query and formatted_query[-1] == 'FROM':
256
+ formatted_query.append(process_from(token, alias_map))
257
+ elif (isinstance(token, IdentifierList) or isinstance(token, Identifier)) and formatted_query and formatted_query[-1] == 'GROUP BY':
258
+ formatted_query.append(process_groupby(token, alias_map))
259
+ elif isinstance(token, Where):
260
+ formatted_query.append('WHERE')
261
+ formatted_query.append(process_where(token, alias_map, baseDict))
262
+ elif formatted_query and formatted_query[-1] == 'SELECT' and (isinstance(token, IdentifierList) or isinstance(token, Function) or isinstance(token, Identifier)):
263
+ if isinstance(token, Function):
264
+ token = IdentifierList([token])
265
+ formatted_query.append(process_select(token, alias_map))
266
+ else:
267
+ formatted_query.append(str(token))
268
+
269
+ return " ".join(formatted_query)
270
+
271
+
272
+ def findTableForColumn(data_dict, target_value, relevantTables):
273
+ l = []
274
+ for key, value_list in data_dict.items():
275
+ if key.lower() in relevantTables:
276
+ for sublist in value_list:
277
+ if sublist and sublist[0].lower() == target_value.lower():
278
+ l.append(key)
279
+ if len(l) == 0:
280
+ for key, value_list in data_dict.items():
281
+ for sublist in value_list:
282
+ if sublist and sublist[0].lower() == target_value.lower():
283
+ l.append(key)
284
+ return l
285
+
286
+
287
+ def getTableScheme(table_name: str, tableDict: dict):
288
+ tab = tableDict[table_name]
289
+
290
+ # Format the schema
291
+ schema = "(" + ",".join([f"{col[0]}:{col[1]}" for col in tab]) + ")"
292
+ return schema
293
+
294
+ def getCosetteKeyFromFile():
295
+ try:
296
+ with open("cosette_apikey.txt", "r") as f:
297
+ return f.read()
298
+ except FileNotFoundError:
299
+ return "NOKEY"
300
+
301
+ def buildAndSendCosetteRequest(baseDict, sql, sol):
302
+
303
+ err = ""
304
+ for i in range(2):
305
+ try:
306
+ apiKey=getCosetteKeyFromFile()
307
+
308
+
309
+ schema = ""
310
+ for tab in baseDict.keys():
311
+ schema += f"schema sch{tab}{getTableScheme(tab, baseDict)};\n"
312
+ for tab in baseDict.keys():
313
+ schema += f"table {tab}(sch{tab});\n"
314
+
315
+ q1 = "query q1\n`"+sql+"`;\n"
316
+ q2 = "query q2\n`"+sol+"`;\n"
317
+
318
+ cosette = "-- random Kommentar\n" + schema + q1 + q2 + "verify q1 q2;\n"
319
+ print(cosette)
320
+
321
+ r = requests.post("https://demo.cosette.cs.washington.edu/solve",data={"api_key": apiKey, "query": cosette}, verify=False)
322
+
323
+ print(r.text)
324
+ return (r.json()['result'],r.text)
325
+ #return r.json()['result']
326
+
327
+ except Exception as e:
328
+ err = str(e)
329
+ return ("ERR", err)
330
+
331
+
332
+ def checkColumns(sqlPath, solPath):
333
+ bd = Ba.getTableDict()
334
+ sql = normalizeSQLQuery(Ba.getSQLFromFile(sqlPath), bd)
335
+ sol = normalizeSQLQuery(Ba.getSQLFromFile(solPath), bd)
336
+
337
+ if("SELECT" in sql and "FROM" in sql):
338
+ start = str.find(sql, "SELECT")
339
+ end = str.find(sql, "FROM")
340
+ submission = str.strip(sql[start:end])
341
+ print("'"+submission+"'")
342
+
343
+ start = str.find(sol, "SELECT")
344
+ end = str.find(sol, "FROM")
345
+ solution = str.strip(sol[start:end])
346
+ print("'"+solution+"'")
347
+
348
+ if submission == solution:
349
+ return ""
350
+ return "Ausgegebene Spalten sind nicht korrekt (oder nicht automatisch überprüfbar)."
351
+
352
+
353
+ def checkTables(sqlPath, solPath):
354
+ bd = Ba.getTableDict()
355
+ sql = normalizeSQLQuery(Ba.getSQLFromFile(sqlPath), bd)
356
+ sol = normalizeSQLQuery(Ba.getSQLFromFile(solPath), bd)
357
+
358
+ if("SELECT" in sql and "FROM" in sql):
359
+ endFromKeywords = ["WHERE", "GROUP", "ORDER", "LIMIT", ";"]
360
+
361
+ start = str.find(sql, "FROM")
362
+ end = -1
363
+
364
+
365
+ for keyword in endFromKeywords:
366
+ if(str.find(sql, keyword) != -1):
367
+ end = str.find(sql, keyword)
368
+ break
369
+ if(end == -1):
370
+ end = len(sql)
371
+
372
+ submission = str.strip(sql[start:end])
373
+ print("'"+submission+"'")
374
+
375
+ start = str.find(sol, "FROM")
376
+ end = -1
377
+
378
+ for keyword in endFromKeywords:
379
+ if(str.find(sol, keyword) != -1):
380
+ end = str.find(sol, keyword)
381
+ break
382
+ if(end == -1):
383
+ end = len(sol)
384
+
385
+ solution = str.strip(sol[start:end])
386
+ print("'"+solution+"'")
387
+
388
+ if submission == solution:
389
+ return ""
390
+ return "Verwendete Tabellen sind nicht korrekt (oder nicht automatisch überprüfbar)."
391
+
392
+
393
+ def checkEquality(sqlPath, solPath):
394
+ bd = Ba.getTableDict()
395
+ sql = normalizeSQLQuery(Ba.getSQLFromFile(sqlPath), bd)
396
+ sol = normalizeSQLQuery(Ba.getSQLFromFile(solPath), bd)
397
+ if(sql=='' or sol==''):
398
+ return "\n\nSQL-Datei ist leer. Aufgabe wurde noch nicht bearbeitet."
399
+
400
+ if(sql==sol):
401
+ return ""
402
+
403
+ result = buildAndSendCosetteRequest(bd, sql, sol)
404
+
405
+ if(result[0] == "ERR"):
406
+ return "\n\nFehler bei der automatischen Überprüfung der Abgabe. Es kann keine Aussage über die Korrektheit der Abgabe getroffen werden."
407
+ elif(result[0] != "EQ"):
408
+ return "\n\nDie Abgabe stimmt nicht mit der Musterlösung überein."
409
+ return ""
410
+
@@ -0,0 +1,3 @@
1
+ from .BaseAccess import *
2
+ from .Helper import *
3
+
Binary file
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.1
2
+ Name: sql_testing_tools
3
+ Version: 0.1.0
4
+ Requires-Dist: sqlparse>=0.5.1
5
+ Requires-Dist: requests>=2.32.3
6
+
@@ -0,0 +1,10 @@
1
+ sql_testing_tools/BaseAccess.py,sha256=tC6dpS6vbb7a7OibGEKlAJiHGnn_OAbmDehweRR0ebA,5188
2
+ sql_testing_tools/Helper.py,sha256=wEun9GAuEP6ibTAHoNnqWqEshWpiT1fJnicVIpwmtJM,16714
3
+ sql_testing_tools/__init__.py,sha256=NYoXmLD8NmuU9TnMS46-gXmMutFHAXxuqaDlITZhRfY,52
4
+ sql_testing_tools/databases/bayern.db,sha256=gREMuoSNYyCeTn2v7e5nPafPKr5IKw5XfYgbn2yxun0,4874240
5
+ sql_testing_tools/databases/ladepunkte.db,sha256=ZR-ibbtdCtovPg6i5eAdKY-E4ahkWkZXha71c1RB6pA,7430144
6
+ sql_testing_tools/databases/straftaten.db,sha256=Z1T5OUI1CTvxNFlFGzyjfesdhK9fivgzHxVW4lyRC2w,50950144
7
+ sql_testing_tools-0.1.0.dist-info/METADATA,sha256=EDuvwT2Gv-AtpZYZWXaOl6H0Cfu3pj7M2KdTph703U0,131
8
+ sql_testing_tools-0.1.0.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
9
+ sql_testing_tools-0.1.0.dist-info/top_level.txt,sha256=qwEkmfJ_lYhIwcLXgfU75m0_JavVbOg96fN2zSatN3E,18
10
+ sql_testing_tools-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.45.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ sql_testing_tools