Annotation of ZSQLExtend/importFMPXML.py, revision 1.2
1.1 casties 1: #!/usr/local/bin/python
2: #
3:
4: import string
5: from xml.dom.pulldom import parse
6: import logging
7: import sys
8:
1.2 ! casties 9: try:
! 10: import psycopg2 as psycopg
! 11: psyco = 2
! 12: except:
! 13: import psycopg
! 14: psyco = 1
! 15:
1.1 casties 16:
17: def getTextFromNode(nodename):
18: """get the cdata content of a node"""
19: if nodename is None:
20: return ""
21: nodelist=nodename.childNodes
22: rc = ""
23: for node in nodelist:
24: if node.nodeType == node.TEXT_NODE:
25: rc = rc + node.data
26: return rc
27:
28: def sql_quote(v):
29: # quote dictionary
30: quote_dict = {"\'": "''", "\\": "\\\\"}
31: for dkey in quote_dict.keys():
32: if string.find(v, dkey) >= 0:
33: v=string.join(string.split(v,dkey),quote_dict[dkey])
34: return "'%s'" % v
35:
36: def SimpleSearch(curs,query, args=None):
37: """execute sql query and return data"""
38: logging.debug("executing: "+query)
1.2 ! casties 39: if psyco == 1:
! 40: query = query.encode("UTF-8")
1.1 casties 41: curs.execute(query, args)
42: logging.debug("sql done")
43: return curs.fetchall()
44:
45:
46: def importXMLFileFMP(dsn,table,filename,update_fields=None,id_field=None,sync_mode=False):
47: '''
48: Import FileMaker XML file (FMPXMLRESULT format) into the table.
49: @param table: name of the table the xml shall be imported into
50: @param filename: xmlfile filename
51: @param update_fields: (optional) list of fields to update; default is to create all fields
52: @param id_field: (optional) field which uniquely identifies an entry for updating purposes.
53: @param sync_mode: (optional) really synchronise, i.e. delete entries not in XML file
54: '''
55:
56: # connect database
57: dbCon = psycopg.connect(dsn)
58: db = dbCon.cursor()
59: assert db, "AIIEE no db cursor for %s!!"%dsn
60:
61: # read XML file
62: fh=file(filename)
63: logging.info("reading file "+filename)
64: doc=parse(fh)
65: logging.info("file read")
66:
67: dbIDs = {}
68: rowcnt = 0
69: ret = ""
70:
71: logging.debug("dsn: "+dsn)
72: logging.debug("table: "+table)
73: logging.debug("update_fields: "+repr(update_fields))
74: logging.debug("id_field: "+id_field)
75: logging.debug("sync_mode: "+repr(sync_mode))
76:
77: if id_field is not None:
78: # prepare a list of ids for sync mode
79: qstr="select %s from %s"%(id_field,table)
80: for id in SimpleSearch(db, qstr):
81: # value 0: not updated
82: dbIDs[id[0]] = 0;
83: rowcnt += 1
84:
85: logging.info("%d entries in DB to sync"%rowcnt)
86:
87: fieldNames = []
88: rowcnt = 0
89: id_val = ''
90:
91: while 1:
92: node=doc.getEvent()
93:
94: if node is None:
95: break;
96:
97: # METADATA tag defines number and names of fields in FMPXMLRESULT
98: if node[1].nodeName == 'METADATA':
99: doc.expandNode(node[1])
100:
101: names=node[1].getElementsByTagName('FIELD')
102:
103: for name in names:
104: fn = name.getAttribute('NAME')
105: fieldNames.append(fn)
106:
107: if update_fields is None:
108: # update all fields
109: update_fields = fieldNames
110:
111: logging.debug("fieldnames:"+repr(fieldNames))
112: # get list of fields in db table
113: qstr="""select attname from pg_attribute, pg_class where attrelid = pg_class.oid and relname = '%s'"""
114: columns=[x[0] for x in SimpleSearch(db, qstr%table)]
115:
116: # adjust db table to fields in XML and fieldlist
117: for fieldName in fieldNames:
118: logging.debug("db-fieldname:"+repr(fieldName))
119: if (fieldName not in columns) and (fieldName in update_fields):
120: qstr="alter table %s add %%s %%s"%table
121: logging.info("db add field:"+qstr%(fieldName,'text'))
122: db.execute(qstr, (fieldName,'text'))
123: db.commit()
124:
125:
126: # ROW tags (in RESULTSET tag) hold data
127: elif node[1].nodeName == 'ROW':
128: rowcnt += 1
129:
130: doc.expandNode(node[1])
131: cols=node[1].getElementsByTagName('COL')
132: dataSet={}
133: i = 0
134: # populate with data
135: for col in cols:
136: data=col.getElementsByTagName('DATA')
137: dataSet[fieldNames[i]] = getTextFromNode(data[0])
138: i += 1
139:
140: update=False
141:
142: # synchronize by id_field
143: if id_field:
144: id_val=dataSet[id_field]
145: if id_val in dbIDs:
146: dbIDs[id_val] += 1
147: update=True
148:
149: if update:
150: # update existing row (by id_field)
151: setvals=[]
152: for fieldName in update_fields:
153: setvals.append("%s = %s"%(fieldName,sql_quote(dataSet[fieldName])))
154: setStr=string.join(setvals, ',')
155: id_val=dataSet[id_field]
156: qstr="UPDATE %s SET %s WHERE %s = '%s' "%(table,setStr,id_field,id_val)
1.2 ! casties 157: if psyco == 1:
! 158: query = query.encode("UTF-8")
1.1 casties 159: db.execute(qstr)
160: ret += "up: %s"%id_val
161: else:
162: # create new row
163: fields=string.join(update_fields, ',')
164: values=string.join([" %s "%sql_quote(dataSet[x]) for x in update_fields], ',')
165: qstr="INSERT INTO %s (%s) VALUES (%s)"%(table,fields,values)
1.2 ! casties 166: if psyco == 1:
! 167: query = query.encode("UTF-8")
1.1 casties 168: db.execute(qstr)
169: ret += "ad: %s"%dataSet.get(id_field, rowcnt)
170:
171: #logging.info(" row:"+"%d (%s)"%(rowcnt,id_val))
172: if (rowcnt % 10) == 0:
173: logging.info(" row:"+"%d (%s)"%(rowcnt,id_val))
174: dbCon.commit()
175:
176: dbCon.commit()
177: if sync_mode:
178: # delete unmatched entries in db
179: for id in dbIDs.keys():
180: # find all not-updated fields
181: if dbIDs[id] == 0:
182: logging.info(" delete:"+id)
183: qstr = "DELETE FROM %s WHERE %%s = '%%s'"%table
1.2 ! casties 184: if psyco == 1:
! 185: query = query.encode("UTF-8")
1.1 casties 186: db.execute(qstr, (id_field,id))
187:
188: elif dbIDs[id] > 1:
189: logging.info(" sync:"+"id used more than once?"+id)
190:
191: dbCon.commit()
192:
193: return ret
194:
195: ##
196: ## public static int main()
197: ##
198:
199: from optparse import OptionParser
200:
201: opars = OptionParser()
202: opars.add_option("-f", "--file",
203: dest="filename",
204: help="FMPXML file name", metavar="FILE")
205: opars.add_option("-c", "--dsn",
206: dest="dsn",
207: help="database connection string")
208: opars.add_option("-t", "--table",
209: dest="table",
210: help="database table name")
211: opars.add_option("--fields", default=None,
212: dest="update_fields",
213: help="list of fields to update (comma separated)", metavar="LIST")
214: opars.add_option("--id-field", default=None,
215: dest="id_field",
216: help="name of id field for synchronisation", metavar="NAME")
217: opars.add_option("--sync-mode", default=False, action="store_true",
218: dest="sync_mode",
219: help="do full sync (remove unmatched fields from db)")
220: opars.add_option("-d", "--debug", default=False, action="store_true",
221: dest="debug",
222: help="debug mode (more output)")
223:
224: (options, args) = opars.parse_args()
225:
226: if len(sys.argv) < 2 or options.filename is None or options.dsn is None:
227: opars.print_help()
228: sys.exit(1)
229:
230: if options.debug:
231: loglevel = logging.DEBUG
232: else:
233: loglevel = logging.INFO
234:
235: logging.basicConfig(level=loglevel,
236: format='%(asctime)s %(levelname)s %(message)s',
237: datefmt='%H:%M:%S')
238:
239: update_fields = None
240:
241: if options.update_fields:
242: update_fields = [string.strip(s) for s in options.update_fields.split(',')]
243:
244: importXMLFileFMP(dsn=options.dsn,table=options.table,filename=options.filename,
245: update_fields=update_fields,id_field=options.id_field,
246: sync_mode=options.sync_mode)
247:
248: print "DONE!"
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>