pyLodStorage 0.0.1__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,36 @@
1
+ Metadata-Version: 2.1
2
+ Name: pylodstorage
3
+ Version: 0.0.1
4
+ Summary: python List of Dict (Table) Storage library
5
+ Home-page: https://github.com/WolfgangFahl/pyLodStorage
6
+ Author: Wolfgang Fahl
7
+ Author-email: wf@bitplan.com
8
+ Maintainer: Wolfgang Fahl
9
+ License: Apache License
10
+ Description: # pyLoDStorage
11
+ python List of Dict (Table) Storage library
12
+
13
+ [![pypi](https://img.shields.io/pypi/pyversions/pyLoDStorage)](https://pypi.org/project/pyLoDStorage/)
14
+ [![Github Actions Build](https://github.com/WolfgangFahl/pyLoDStorage/workflows/Build/badge.svg?branch=master)](https://github.com/WolfgangFahl/pyLoDStorage/actions?query=workflow%3ABuild+branch%3Amaster)
15
+ [![GitHub issues](https://img.shields.io/github/issues/WolfgangFahl/pyLoDStorage.svg)](https://github.com/WolfgangFahl/pyLoDStorage/issues)
16
+ [![GitHub closed issues](https://img.shields.io/github/issues-closed/WolfgangFahl/pyLoDStorage.svg)](https://github.com/WolfgangFahl/pyLoDStorage/issues/?q=is%3Aissue+is%3Aclosed)
17
+ [![License](https://img.shields.io/github/license/WolfgangFahl/pyLoDStorage.svg)](https://www.apache.org/licenses/LICENSE-2.0)
18
+
19
+ What it is
20
+ ==========
21
+ pyLoDStorage allows to store table like data (List of Dicts) via Sqlite3
22
+
23
+ Installation
24
+ ============
25
+ ```bash
26
+ pip install pylodstorage
27
+ ```
28
+
29
+ Usage
30
+ =====
31
+ see [test cases](https://github.com/WolfgangFahl/pyLoDStorage/tests)
32
+ Platform: UNKNOWN
33
+ Classifier: Programming Language :: Python
34
+ Classifier: Programming Language :: Python :: 3.7
35
+ Classifier: Programming Language :: Python :: 3.8
36
+ Description-Content-Type: text/markdown
@@ -0,0 +1,22 @@
1
+ # pyLoDStorage
2
+ python List of Dict (Table) Storage library
3
+
4
+ [![pypi](https://img.shields.io/pypi/pyversions/pyLoDStorage)](https://pypi.org/project/pyLoDStorage/)
5
+ [![Github Actions Build](https://github.com/WolfgangFahl/pyLoDStorage/workflows/Build/badge.svg?branch=master)](https://github.com/WolfgangFahl/pyLoDStorage/actions?query=workflow%3ABuild+branch%3Amaster)
6
+ [![GitHub issues](https://img.shields.io/github/issues/WolfgangFahl/pyLoDStorage.svg)](https://github.com/WolfgangFahl/pyLoDStorage/issues)
7
+ [![GitHub closed issues](https://img.shields.io/github/issues-closed/WolfgangFahl/pyLoDStorage.svg)](https://github.com/WolfgangFahl/pyLoDStorage/issues/?q=is%3Aissue+is%3Aclosed)
8
+ [![License](https://img.shields.io/github/license/WolfgangFahl/pyLoDStorage.svg)](https://www.apache.org/licenses/LICENSE-2.0)
9
+
10
+ What it is
11
+ ==========
12
+ pyLoDStorage allows to store table like data (List of Dicts) via Sqlite3
13
+
14
+ Installation
15
+ ============
16
+ ```bash
17
+ pip install pylodstorage
18
+ ```
19
+
20
+ Usage
21
+ =====
22
+ see [test cases](https://github.com/WolfgangFahl/pyLoDStorage/tests)
@@ -0,0 +1,36 @@
1
+ Metadata-Version: 2.1
2
+ Name: pylodstorage
3
+ Version: 0.0.1
4
+ Summary: python List of Dict (Table) Storage library
5
+ Home-page: https://github.com/WolfgangFahl/pyLodStorage
6
+ Author: Wolfgang Fahl
7
+ Author-email: wf@bitplan.com
8
+ Maintainer: Wolfgang Fahl
9
+ License: Apache License
10
+ Description: # pyLoDStorage
11
+ python List of Dict (Table) Storage library
12
+
13
+ [![pypi](https://img.shields.io/pypi/pyversions/pyLoDStorage)](https://pypi.org/project/pyLoDStorage/)
14
+ [![Github Actions Build](https://github.com/WolfgangFahl/pyLoDStorage/workflows/Build/badge.svg?branch=master)](https://github.com/WolfgangFahl/pyLoDStorage/actions?query=workflow%3ABuild+branch%3Amaster)
15
+ [![GitHub issues](https://img.shields.io/github/issues/WolfgangFahl/pyLoDStorage.svg)](https://github.com/WolfgangFahl/pyLoDStorage/issues)
16
+ [![GitHub closed issues](https://img.shields.io/github/issues-closed/WolfgangFahl/pyLoDStorage.svg)](https://github.com/WolfgangFahl/pyLoDStorage/issues/?q=is%3Aissue+is%3Aclosed)
17
+ [![License](https://img.shields.io/github/license/WolfgangFahl/pyLoDStorage.svg)](https://www.apache.org/licenses/LICENSE-2.0)
18
+
19
+ What it is
20
+ ==========
21
+ pyLoDStorage allows to store table like data (List of Dicts) via Sqlite3
22
+
23
+ Installation
24
+ ============
25
+ ```bash
26
+ pip install pylodstorage
27
+ ```
28
+
29
+ Usage
30
+ =====
31
+ see [test cases](https://github.com/WolfgangFahl/pyLoDStorage/tests)
32
+ Platform: UNKNOWN
33
+ Classifier: Programming Language :: Python
34
+ Classifier: Programming Language :: Python :: 3.7
35
+ Classifier: Programming Language :: Python :: 3.8
36
+ Description-Content-Type: text/markdown
@@ -0,0 +1,10 @@
1
+ README.md
2
+ setup.py
3
+ pylodstorage.egg-info/PKG-INFO
4
+ pylodstorage.egg-info/SOURCES.txt
5
+ pylodstorage.egg-info/dependency_links.txt
6
+ pylodstorage.egg-info/top_level.txt
7
+ storage/__init__.py
8
+ storage/sample.py
9
+ storage/sql.py
10
+ storage/uml.py
@@ -0,0 +1 @@
1
+ storage
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,29 @@
1
+ # ! important
2
+ # see https://stackoverflow.com/a/27868004/1497139
3
+ from setuptools import setup
4
+
5
+ # read the contents of your README file
6
+ from os import path
7
+ this_directory = path.abspath(path.dirname(__file__))
8
+ with open(path.join(this_directory, 'README.md'), encoding='utf-8') as f:
9
+ long_description = f.read()
10
+
11
+ setup(
12
+ name='pylodstorage',
13
+ version='0.0.1',
14
+
15
+ packages=['storage',],
16
+ author='Wolfgang Fahl',
17
+ author_email='wf@bitplan.com',
18
+ maintainer='Wolfgang Fahl',
19
+ url='https://github.com/WolfgangFahl/pyLodStorage',
20
+ license='Apache License',
21
+ description='python List of Dict (Table) Storage library',
22
+ classifiers=[
23
+ 'Programming Language :: Python',
24
+ 'Programming Language :: Python :: 3.7',
25
+ 'Programming Language :: Python :: 3.8'
26
+ ],
27
+ long_description=long_description,
28
+ long_description_content_type='text/markdown'
29
+ )
File without changes
@@ -0,0 +1,71 @@
1
+ '''
2
+ Created on 2020-08-24
3
+
4
+ @author: wf
5
+ '''
6
+ import urllib.request
7
+ import json
8
+ from datetime import date, datetime
9
+
10
+ class Sample(object):
11
+ '''
12
+ Sample dataset generator
13
+ '''
14
+
15
+ def __init__(self):
16
+ '''
17
+ Constructor
18
+ '''
19
+
20
+ @staticmethod
21
+ def getSample(size):
22
+ listOfDicts=[]
23
+ for index in range(size):
24
+ listOfDicts.append({'pkey': "index%d" %index, 'cindex': index})
25
+ return listOfDicts
26
+
27
+ @staticmethod
28
+ def getCountries():
29
+ countryJsonUrl="https://gist.githubusercontent.com/erdem/8c7d26765831d0f9a8c62f02782ae00d/raw/248037cd701af0a4957cce340dabb0fd04e38f4c/countries.json"
30
+ with urllib.request.urlopen(countryJsonUrl) as url:
31
+ countryList=json.loads(url.read().decode())
32
+ return countryList
33
+
34
+ @staticmethod
35
+ def getCities():
36
+ '''
37
+ get a list of cities
38
+ '''
39
+ cityJsonUrl="https://raw.githubusercontent.com/lutangar/cities.json/master/cities.json"
40
+ with urllib.request.urlopen(cityJsonUrl) as url:
41
+ cityList=json.loads(url.read().decode())
42
+ for city in cityList:
43
+ city['cityId']="%s-%s" % (city['country'],city['name'])
44
+ return cityList
45
+
46
+ @staticmethod
47
+ def dob(isoDateString):
48
+ ''' get the date of birth from the given iso date state'''
49
+ #if sys.version_info >= (3, 7):
50
+ # dt=datetime.fromisoformat(isoDateString)
51
+ #else:
52
+ dt=datetime.strptime(isoDateString,"%Y-%m-%d")
53
+ return dt.date()
54
+
55
+ @staticmethod
56
+ def getRoyals():
57
+ listOfDicts=[
58
+ {'name': 'Elizabeth Alexandra Mary Windsor', 'born': Sample.dob('1926-04-21'), 'numberInLine': 0, 'wikidataurl': 'https://www.wikidata.org/wiki/Q9682' },
59
+ {'name': 'Charles, Prince of Wales', 'born': Sample.dob('1948-11-14'), 'numberInLine': 1, 'wikidataurl': 'https://www.wikidata.org/wiki/Q43274' },
60
+ {'name': 'George of Cambridge', 'born': Sample.dob('2013-07-22'), 'numberInLine': 3, 'wikidataurl': 'https://www.wikidata.org/wiki/Q1359041'},
61
+ {'name': 'Harry Duke of Sussex', 'born': Sample.dob('1984-09-15'), 'numberInLine': 6, 'wikidataurl': 'https://www.wikidata.org/wiki/Q152316'}
62
+ ]
63
+ today=date.today()
64
+ for person in listOfDicts:
65
+ born=person['born']
66
+ age=(today - born).days / 365.2425
67
+ person['age']=age
68
+ person['ofAge']=age>=18
69
+ person['lastmodified']=datetime.now()
70
+ return listOfDicts
71
+
@@ -0,0 +1,441 @@
1
+ '''
2
+ Created on 2020-08-24
3
+
4
+ @author: wf
5
+ '''
6
+ # python standard library
7
+ import sqlite3
8
+ import datetime
9
+ import io
10
+ import time
11
+ import sys
12
+ import re
13
+
14
+
15
+ class SQLDB(object):
16
+ '''
17
+ Structured Query Language Database wrapper
18
+
19
+ :ivar dbname(string): name of the database
20
+ :ivar debug(boolean): True if debug info should be provided
21
+ :ivar errorDebug(boolean): True if debug info should be provided on errors (should not be used for production since it might reveal data)
22
+ '''
23
+ RAM=":memory:"
24
+
25
+ def __init__(self,dbname=':memory:',connection=None,debug=False, errorDebug=False):
26
+ '''
27
+ Construct me for the given dbname and debug
28
+
29
+ Args:
30
+
31
+ dbname(string): name of the database - default is a RAM based database
32
+ connection(Connection): an optional connectio to be reused
33
+ debug(boolean): if True switch on debug
34
+ errorDebug(boolean): True if debug info should be provided on errors (should not be used for production since it might reveal data)
35
+ '''
36
+ self.dbname=dbname
37
+ self.debug=debug
38
+ self.errorDebug=errorDebug
39
+ if connection is None:
40
+ self.c=sqlite3.connect(dbname,detect_types=sqlite3.PARSE_DECLTYPES)
41
+ else:
42
+ self.c=connection
43
+
44
+ def close(self):
45
+ ''' close my connection '''
46
+ self.c.close()
47
+
48
+ def createTable(self,listOfRecords,entityName,primaryKey=None,withDrop=False,sampleRecordCount=1):
49
+ '''
50
+ derive Data Definition Language CREATE TABLE command from list of Records by examining first recorda
51
+ as defining sample record and execute DDL command
52
+
53
+ auto detect column types see e.g. https://stackoverflow.com/a/57072280/1497139
54
+
55
+ Args:
56
+ listOfRecords(list): a list of Dicts
57
+ entityName(string): the entity / table name to use
58
+ primaryKey(string): the key/column to use as a primary key
59
+ withDrop(boolean): true if the existing Table should be dropped
60
+ sampleRecords(int): number of sampleRecords expected and to be inspected
61
+
62
+ Returns:
63
+ EntityInfo: meta data information for the created table
64
+ '''
65
+ if len(listOfRecords)<sampleRecordCount:
66
+ raise Exception("Need %d sample records to createTable" % (sampleRecordCount))
67
+ sampleRecords=listOfRecords[:sampleRecordCount]
68
+ entityInfo=EntityInfo(sampleRecords,entityName,primaryKey,debug=self.debug)
69
+ if withDrop:
70
+ self.c.execute(entityInfo.dropTableCmd)
71
+ self.c.execute(entityInfo.createTableCmd)
72
+ return entityInfo
73
+
74
+ def getDebugInfo(self,record,index,executeMany):
75
+ '''
76
+ get the debug info for the given record at the given index depending on the state of executeMany
77
+
78
+ Args:
79
+ record(dict): the record to show
80
+ index(int): the index of the record
81
+ executeMany(boolean): if True the record may be valid else not
82
+ '''
83
+ debugInfo=""
84
+ if not executeMany:
85
+ if self.errorDebug:
86
+ debugInfo="\nrecord #%d=%s" % (index,repr(record))
87
+ return debugInfo
88
+
89
+
90
+ def store(self,listOfRecords,entityInfo,executeMany=False):
91
+ '''
92
+ store the given list of records based on the given entityInfo
93
+
94
+ Args:
95
+
96
+ listOfRecords(list): the list of Dicts to be stored
97
+ entityInfo(EntityInfo): the meta data to be used for storing
98
+ '''
99
+ insertCmd=entityInfo.insertCmd
100
+ record=None
101
+ index=0
102
+ try:
103
+ if executeMany:
104
+ self.c.executemany(insertCmd,listOfRecords)
105
+ else:
106
+ for record in listOfRecords:
107
+ index+=1
108
+ self.c.execute(insertCmd,record)
109
+ self.c.commit()
110
+ except sqlite3.ProgrammingError as pe:
111
+ msg=pe.args[0]
112
+ if "You did not supply a value for binding" in msg:
113
+ columnIndex=int(re.findall(r'\d+',msg)[0])
114
+ columnName=list(entityInfo.typeMap.keys())[columnIndex-1]
115
+ debugInfo=self.getDebugInfo(record, index, executeMany)
116
+ raise Exception("%s\nfailed: no value supplied for column '%s'%s" % (insertCmd,columnName,debugInfo))
117
+ else:
118
+ raise pe
119
+ except sqlite3.InterfaceError as ie:
120
+ msg=ie.args[0]
121
+ if "Error binding parameter" in msg:
122
+ columnName=re.findall(r':[_a-zA-Z]\w*',msg)[0]
123
+ debugInfo=self.getDebugInfo(record, index, executeMany)
124
+ raise Exception("%s\nfailed: error binding column '%s'%s" % (insertCmd,columnName,debugInfo))
125
+ else:
126
+ raise ie
127
+
128
+ def query(self,sqlQuery):
129
+ '''
130
+ run the given sqlQuery and return a list of Dicts
131
+
132
+ Args:
133
+
134
+ sqlQuery(string): the SQL query to be executed
135
+
136
+ Returns:
137
+ list: a list of Dicts
138
+ '''
139
+ if self.debug:
140
+ print(sqlQuery)
141
+ # https://stackoverflow.com/a/13735506/1497139
142
+ cur=self.c.cursor()
143
+ query = cur.execute(sqlQuery)
144
+ colname = [ d[0] for d in query.description ]
145
+ resultList=[]
146
+ for row in query:
147
+ record=dict(zip(colname, row))
148
+ resultList.append(record)
149
+ cur.close()
150
+ return resultList
151
+
152
+ def queryAll(self,entityInfo,fixDates=True):
153
+ '''
154
+ query all records for the given entityName/tableName
155
+
156
+ Args:
157
+ entityName(string): name of the entity/table to qury
158
+ fixDates(boolean): True if date entries should be returned as such and not as strings
159
+ '''
160
+ sqlQuery='SELECT * FROM %s' % entityInfo.name
161
+ resultList=self.query(sqlQuery)
162
+ if fixDates:
163
+ entityInfo.fixDates(resultList)
164
+ return resultList
165
+
166
+ def getTableList(self):
167
+ '''
168
+ get the schema information from this database
169
+ '''
170
+ tableQuery="SELECT name FROM sqlite_master WHERE type='table'"
171
+ tableList=self.query(tableQuery)
172
+ for table in tableList:
173
+ tableName=table['name']
174
+ columnQuery="PRAGMA table_info('%s')" % tableName
175
+ columns=self.query(columnQuery)
176
+ table['columns']=columns
177
+ return tableList
178
+
179
+ def progress(self,status, remaining, total):
180
+ '''
181
+ show progress
182
+ '''
183
+ print('Backup %s at %5.0f%%' % ("... " if status==0 else "done",(total-remaining)/total*100))
184
+
185
+ def backup(self,backupDB,action="Backup",profile=False,showProgress=200,doClose=True):
186
+ '''
187
+ create backup of this SQLDB to the given backup db
188
+
189
+ see https://stackoverflow.com/a/59042442/1497139
190
+
191
+ Args:
192
+ backupDB(string): the path to the backupdb or SQLDB.RAM for in memory
193
+ action(string): the action to display
194
+ profile(boolean): True if timing information shall be shown
195
+ showProgress(int): show progress at each showProgress page (0=show no progress)
196
+ '''
197
+ if sys.version_info <= (3, 6):
198
+ raise Exception("backup via stdlibrary not available in python <=3.6 use copyToDB instead")
199
+ startTime=time.time()
200
+ bck=sqlite3.connect(backupDB)
201
+ if showProgress>0:
202
+ progress=self.progress
203
+ else:
204
+ progress=None
205
+ with bck:
206
+ self.c.backup(bck,pages=showProgress,progress=progress)
207
+ elapsed=time.time()-startTime
208
+ if profile:
209
+ print("%s to %s took %5.1f s" % (action,backupDB,elapsed))
210
+ if doClose:
211
+ bck.close()
212
+ return None
213
+ else:
214
+ return bck
215
+
216
+ def showDump(self,dump,limit=10):
217
+ '''
218
+ show the given dump up to the given limit
219
+
220
+ Args:
221
+ dump(string): the SQL dump to show
222
+ limit(int): the maximum number of lines to display
223
+ '''
224
+ s=io.StringIO(dump)
225
+ index=0
226
+ for line in s:
227
+ if index <= limit:
228
+ print(line)
229
+ index+=1
230
+ else:
231
+ break
232
+
233
+ def executeDump(self,connection,dump,title,maxErrors=100,errorDisplayLimit=12,profile=True):
234
+ '''
235
+ execute the given dump for the given connection
236
+
237
+ Args:
238
+ connection(Connection): the sqlite3 connection to use
239
+ dump(string): the SQL commands for the dump
240
+ title(string): the title of the dump
241
+ maxErrors(int): maximum number of errors to be tolerated before stopping and doing a rollback
242
+ profile(boolean): True if profiling information should be shown
243
+ Returns:
244
+ a list of errors
245
+ '''
246
+ if self.debug:
247
+ self.showDump(dump)
248
+ startTime=time.time()
249
+ if profile:
250
+ print("dump of %s has size %4.1f MB" % (title,len(dump)/1024/1024))
251
+ errors=[]
252
+ index=0
253
+ # fixes https://github.com/WolfgangFahl/ProceedingsTitleParser/issues/37
254
+ for line in dump.split(";\n"):
255
+ try:
256
+ connection.execute(line)
257
+ except sqlite3.OperationalError as soe:
258
+ msg="SQL error %s in line %d:\n\t%s" % (soe,index,line)
259
+ errors.append(msg)
260
+ if len(errors)<=errorDisplayLimit:
261
+ print(msg)
262
+ if len(errors)>=maxErrors:
263
+ connection.execute("ROLLBACK;")
264
+ break
265
+
266
+ index=index+1
267
+ if profile:
268
+ print("finished executing dump %s with %d lines and %d errors in %5.1f s" % (title,index,len(errors),time.time()-startTime))
269
+ return errors
270
+
271
+ def copyTo(self,copyDB,profile=True):
272
+ '''
273
+ copy my content to another database
274
+
275
+ Args:
276
+
277
+ copyDB(Connection): the target database
278
+ profile(boolean): if True show profile information
279
+ '''
280
+ startTime=time.time()
281
+ dump="\n".join(self.c.iterdump())
282
+ #cursor.executescript(dump)
283
+ if profile:
284
+ print("finished getting dump of %s in %5.1f s" % (self.dbname,time.time()-startTime))
285
+ dumpErrors=self.executeDump(copyDB.c,dump,self.dbname,profile=profile)
286
+ return dumpErrors
287
+
288
+
289
+ @staticmethod
290
+ def restore(backupDB,restoreDB,profile=False,showProgress=200,debug=False):
291
+ '''
292
+ restore the restoreDB from the given backup DB
293
+
294
+ Args:
295
+ backupDB(string): path to the backupDB e.g. backup.db
296
+ restoreDB(string): path to the restoreDB or in Memory SQLDB.RAM
297
+ profile(boolean): True if timing information should be shown
298
+ showProgress(int): show progress at each showProgress page (0=show no progress)
299
+ '''
300
+ backupSQLDB=SQLDB(backupDB)
301
+ connection=backupSQLDB.backup(restoreDB,action="Restore",profile=profile,showProgress=showProgress,doClose=False)
302
+ restoreSQLDB=SQLDB(restoreDB,connection=connection,debug=debug)
303
+ return restoreSQLDB
304
+
305
+
306
+ class EntityInfo(object):
307
+ """
308
+ holds entity meta Info
309
+
310
+ :ivar name(string): entity name = table name
311
+
312
+ :ivar primaryKey(string): the name of the primary key column
313
+
314
+ :ivar typeMap(dict): maps column names to python types
315
+
316
+ :ivar debug(boolean): True if debug information should be shown
317
+
318
+ """
319
+
320
+ def __init__(self,sampleRecords,name,primaryKey=None,debug=False):
321
+ '''
322
+ construct me from the given name and primary key
323
+
324
+ Args:
325
+ name(string): the name of the entity
326
+ primaryKey(string): the name of the primary key column
327
+ debug(boolean): True if debug information should be shown
328
+ '''
329
+ self.sampleRecords=sampleRecords
330
+ self.name=name
331
+ self.primaryKey=primaryKey
332
+ self.debug=debug
333
+ self.typeMap={}
334
+ self.sqlTypeMap={}
335
+ self.createTableCmd=self.getCreateTableCmd(sampleRecords)
336
+ self.dropTableCmd="DROP TABLE IF EXISTS %s" % self.name
337
+ self.insertCmd=self.getInsertCmd()
338
+
339
+ def getCreateTableCmd(self,sampleRecords):
340
+ '''
341
+ get the CREATE TABLE DDL command for the given sample records
342
+
343
+ Args:
344
+ sampleRecords(list): a list of Dicts of sample Records
345
+
346
+ Returns:
347
+ string: CREATE TABLE DDL command for this entity info
348
+
349
+ Example:
350
+
351
+ .. code-block:: sql
352
+
353
+ CREATE TABLE Person(name TEXT PRIMARY KEY,born DATE,numberInLine INTEGER,wikidataurl TEXT,age FLOAT,ofAge BOOLEAN)
354
+
355
+ '''
356
+ ddlCmd="CREATE TABLE %s(" %self.name
357
+ delim=""
358
+ for sampleRecord in sampleRecords:
359
+ for key,value in sampleRecord.items():
360
+ sqlType=None
361
+ valueType=None
362
+ if value is None:
363
+ if len(sampleRecords)==1:
364
+ print("Warning sampleRecord column %s is None - using TEXT as type" % key)
365
+ valueType=str
366
+ else:
367
+ valueType=type(value)
368
+ if valueType == str:
369
+ sqlType="TEXT"
370
+ elif valueType == int:
371
+ sqlType="INTEGER"
372
+ elif valueType == float:
373
+ sqlType="FLOAT"
374
+ elif valueType == bool:
375
+ sqlType="BOOLEAN"
376
+ elif valueType == datetime.date:
377
+ sqlType="DATE"
378
+ elif valueType== datetime.datetime:
379
+ sqlType="TIMESTAMP"
380
+ else:
381
+ if valueType is not None:
382
+ msg="warning: unsupported type %s for column %s " % (str(valueType),key)
383
+ print(msg)
384
+ if sqlType is not None and valueType is not None:
385
+ self.addType(key,valueType,sqlType)
386
+ for key,sqlType in self.sqlTypeMap.items():
387
+ ddlCmd+="%s%s %s%s" % (delim,key,sqlType," PRIMARY KEY" if key==self.primaryKey else "")
388
+ delim=","
389
+ ddlCmd+=")"
390
+ if self.debug:
391
+ print (ddlCmd)
392
+ return ddlCmd
393
+
394
+ def getInsertCmd(self):
395
+ '''
396
+ get the INSERT command for this entityInfo
397
+
398
+ Returns:
399
+ the INSERT INTO SQL command for his entityInfo e.g.
400
+
401
+ Example:
402
+
403
+ .. code-block:: sql
404
+
405
+ INSERT INTO Person (name,born,numberInLine,wikidataurl,age,ofAge) values (?,?,?,?,?,?).
406
+
407
+ '''
408
+ columns =','.join(self.typeMap.keys())
409
+ placeholders=':'+',:'.join(self.typeMap.keys())
410
+ insertCmd="INSERT INTO %s (%s) values (%s)" % (self.name, columns,placeholders)
411
+ if self.debug:
412
+ print(insertCmd)
413
+ return insertCmd
414
+
415
+ def addType(self,column,valueType,sqlType):
416
+ '''
417
+ add the python type for the given column to the typeMap
418
+
419
+ Args:
420
+ column(string): the name of the column
421
+
422
+ valueType(type): the python type of the column
423
+ '''
424
+ if not column in self.typeMap:
425
+ self.typeMap[column]=valueType
426
+ self.sqlTypeMap[column]=sqlType
427
+
428
+ def fixDates(self,resultList):
429
+ '''
430
+ fix date entries in the given resultList by parsing the date content e.g.
431
+ converting '1926-04-21' back to datetime.date(1926, 4, 21)
432
+
433
+ Args:
434
+ resultList(list): the list of records to be fixed
435
+ '''
436
+ for record in resultList:
437
+ for key,valueType in self.typeMap.items():
438
+ if valueType==datetime.date:
439
+ dt=datetime.datetime.strptime(record[key],"%Y-%m-%d")
440
+ dateValue=dt.date()
441
+ record[key]=dateValue
@@ -0,0 +1,187 @@
1
+ '''
2
+ Created on 2020-09-04
3
+
4
+ @author: wf
5
+ '''
6
+ from collections import Counter
7
+
8
+ class UML(object):
9
+ '''
10
+ UML diagrams via plantuml
11
+
12
+ '''
13
+
14
+ skinparams="""
15
+ ' BITPlan Corporate identity skin params
16
+ ' Copyright (c) 2015 BITPlan GmbH
17
+ ' see http://wiki.bitplan.com/PlantUmlSkinParams#BITPlanCI
18
+ ' skinparams generated by com.bitplan.restmodelmanager
19
+ skinparam note {
20
+ BackGroundColor #FFFFFF
21
+ FontSize 12
22
+ ArrowColor #FF8000
23
+ BorderColor #FF8000
24
+ FontColor black
25
+ FontName Technical
26
+ }
27
+ skinparam component {
28
+ BackGroundColor #FFFFFF
29
+ FontSize 12
30
+ ArrowColor #FF8000
31
+ BorderColor #FF8000
32
+ FontColor black
33
+ FontName Technical
34
+ }
35
+ skinparam package {
36
+ BackGroundColor #FFFFFF
37
+ FontSize 12
38
+ ArrowColor #FF8000
39
+ BorderColor #FF8000
40
+ FontColor black
41
+ FontName Technical
42
+ }
43
+ skinparam usecase {
44
+ BackGroundColor #FFFFFF
45
+ FontSize 12
46
+ ArrowColor #FF8000
47
+ BorderColor #FF8000
48
+ FontColor black
49
+ FontName Technical
50
+ }
51
+ skinparam activity {
52
+ BackGroundColor #FFFFFF
53
+ FontSize 12
54
+ ArrowColor #FF8000
55
+ BorderColor #FF8000
56
+ FontColor black
57
+ FontName Technical
58
+ }
59
+ skinparam classAttribute {
60
+ BackGroundColor #FFFFFF
61
+ FontSize 12
62
+ ArrowColor #FF8000
63
+ BorderColor #FF8000
64
+ FontColor black
65
+ FontName Technical
66
+ }
67
+ skinparam interface {
68
+ BackGroundColor #FFFFFF
69
+ FontSize 12
70
+ ArrowColor #FF8000
71
+ BorderColor #FF8000
72
+ FontColor black
73
+ FontName Technical
74
+ }
75
+ skinparam class {
76
+ BackGroundColor #FFFFFF
77
+ FontSize 12
78
+ ArrowColor #FF8000
79
+ BorderColor #FF8000
80
+ FontColor black
81
+ FontName Technical
82
+ }
83
+ skinparam object {
84
+ BackGroundColor #FFFFFF
85
+ FontSize 12
86
+ ArrowColor #FF8000
87
+ BorderColor #FF8000
88
+ FontColor black
89
+ FontName Technical
90
+ }
91
+ hide Circle
92
+ ' end of skinparams '
93
+ """
94
+
95
+ def __init__(self, debug=False):
96
+ '''
97
+ Constructor
98
+ Args:
99
+ debug(boolean): True if debug information should be shown
100
+ '''
101
+ self.debug=debug
102
+
103
+ def generalizeColumn(self,tableList,colName):
104
+ '''
105
+ remove the column with the given name from all tables in the tablelist and
106
+ return it
107
+
108
+ Args:
109
+ tableList(list): a list of Tables
110
+ colName(string): the name of the column to generalize
111
+
112
+ Returns:
113
+ string: the column having been generalized and removed
114
+ '''
115
+ gCol=None
116
+ for table in tableList:
117
+ for col in table['columns']:
118
+ if col['name']==colName:
119
+ gCol=col
120
+ table['columns'].remove(col)
121
+ return gCol
122
+
123
+ def getGeneral(self,tableList,name):
124
+ '''
125
+ derive a general table from the given table list
126
+ Args:
127
+ tableList(list): a list of tables
128
+ name(string): name of the general table
129
+
130
+ Returns:
131
+ at table dict for the generalized table
132
+ '''
133
+ general={'name':name,'columns':[]}
134
+ colCount=Counter()
135
+ for table in tableList:
136
+ for col in table['columns']:
137
+ columnId="%s.%s" % (col['name'],col['type'])
138
+ if self.debug:
139
+ print (columnId)
140
+ colCount[columnId]+=1
141
+ for columnId,count in colCount.items():
142
+ if count==len(tableList):
143
+ colName=columnId.split('.')[0]
144
+ generalCol=self.generalizeColumn(tableList, colName)
145
+ general['columns'].append(generalCol)
146
+ return general
147
+
148
+ def tableListToPlantUml(self,tableList, packageName=None,generalizeTo=None,withSkin=True):
149
+ '''
150
+ convert tableList to PlantUml notation
151
+
152
+ Args:
153
+ tableList(list): the tableList list of Dicts from getTableList() to convert
154
+ packageName(string): optional packageName to be added
155
+ generalizeTo(string): optional name of a general table to be derived
156
+ withSkin(boolean): if True add default BITPlan skin parameters
157
+
158
+ Returns:
159
+ string: the Plantuml notation for the entities in columns of the given tablelist
160
+ '''
161
+ uml=""
162
+ indent=""
163
+ inherit=""
164
+ if packageName is not None:
165
+ uml+="package %s {\n" % packageName
166
+ indent=" "
167
+ if generalizeTo is not None:
168
+ generalTable=self.getGeneral(tableList,generalizeTo)
169
+ for table in tableList:
170
+ inherit+="%s%s <|-- %s\n" % (indent,generalizeTo,table['name'])
171
+ tableList.insert(0,generalTable)
172
+ for table in tableList:
173
+ colUml=""
174
+ for col in table['columns']:
175
+ mandatory="*" if col['notnull']==1 else ""
176
+ pk="<<PK>>" if col['pk']==1 else ""
177
+ colUml+="%s %s%s : %s %s\n" % (indent,mandatory,col['name'],col['type'],pk)
178
+ uml+="%sentity %s {\n%s%s}\n" % (indent,table['name'],colUml,indent)
179
+ uml+=inherit
180
+ if packageName is not None:
181
+ uml+="}\n"
182
+ if withSkin:
183
+ uml+=UML.skinparams
184
+ return uml
185
+
186
+
187
+