FB Prompts Xml¶
Sync Firebase “prompts” collection with XML file.
import argparse
import xml.etree.ElementTree as ET
from datetime import datetime, timezone
import dateutil.parser
import firebase_admin
from firebase_admin import credentials, firestore
def parse_args():
parser = argparse.ArgumentParser(
description="Synchronize Firebase 'prompts' collection with an XML file"
)
parser.add_argument(
'--xml', '-x', required=True,
help='Path to the XML file'
)
parser.add_argument(
'--service-account', '-s', required=True,
help='Path to your Firebase service account JSON'
)
return parser.parse_args()
def parse_iso(dt_str):
"""Parse an ISO8601 string into a timezone-aware datetime."""
return dateutil.parser.isoparse(dt_str)
def fmt_iso(dt):
"""Format a datetime as ISO8601 in UTC."""
return dt.astimezone(timezone.utc).isoformat().replace('+00:00', 'Z')
def load_xml(path):
tree = ET.parse(path)
root = tree.getroot()
records = {}
now = datetime.now(timezone.utc)
for elem in root.findall('prompt'):
name = elem.findtext('name')
note = elem.findtext('note') or ''
tags = [t.text for t in elem.findall('tags/tag')]
created_el = elem.find('created_at')
created_at = parse_iso(created_el.text) if created_el is not None else now
updated_el = elem.find('updated_at')
if updated_el is not None:
updated_at = parse_iso(updated_el.text)
else:
# if missing, treat as now
updated_at = now
# add the missing node
updated_el = ET.SubElement(elem, 'updated_at')
updated_el.text = fmt_iso(now)
records[name] = {
'elem': elem,
'note': note,
'tags': tags,
'created_at': created_at,
'updated_at': updated_at
}
return tree, root, records
def load_firebase(db):
docs = db.collection('prompts').stream()
records = {}
for doc in docs:
data = doc.to_dict()
name = data.get('name')
if not name:
continue
ca = data.get('created_at')
ua = data.get('updated_at')
records[name] = {
'ref': db.collection('prompts').document(doc.id),
'note': data.get('note', ''),
'tags': data.get('tags', []),
'created_at': ca if isinstance(ca, datetime) else now,
'updated_at': ua if isinstance(ua, datetime) else now
}
return records
def sync(tree, root, xml_recs, fb_recs):
now = datetime.now(timezone.utc)
all_names = set(xml_recs) | set(fb_recs)
for name in all_names:
xml = xml_recs.get(name)
fb = fb_recs.get(name)
# exists both
if xml and fb:
if fb['updated_at'] > xml['updated_at']:
# Firebase is newer -> update XML
elem = xml['elem']
elem.find('note').text = fb['note']
tags_el = elem.find('tags')
# clear tags
for t in tags_el.findall('tag'):
tags_el.remove(t)
# add tags
for tag in fb['tags']:
t_el = ET.SubElement(tags_el, 'tag')
t_el.text = tag
# update timestamps
elem.find('updated_at').text = fmt_iso(fb['updated_at'])
elif xml['updated_at'] > fb['updated_at']:
# XML is newer -> update Firebase
fb['ref'].update({
'note': xml['note'],
'tags': xml['tags'],
'updated_at': xml['updated_at']
})
# only in XML -> create in Firebase
elif xml and not fb:
data = {
'name': name,
'note': xml['note'],
'tags': xml['tags'],
'created_at': xml['created_at'],
'updated_at': xml['updated_at']
}
db.collection('prompts').add(data)
# only in Firebase -> add to XML
elif fb and not xml:
elem = ET.SubElement(root, 'prompt')
ET.SubElement(elem, 'name').text = name
ET.SubElement(elem, 'note').text = fb['note']
tags_el = ET.SubElement(elem, 'tags')
for tag in fb['tags']:
t_el = ET.SubElement(tags_el, 'tag')
t_el.text = tag
ET.SubElement(elem, 'created_at').text = fmt_iso(fb['created_at'])
ET.SubElement(elem, 'updated_at').text = fmt_iso(fb['updated_at'])
# write back XML
ET.indent(tree, space=" ", level=0)
tree.write(args.xml, encoding="utf-8", xml_declaration=True)
if __name__ == '__main__':
args = parse_args()
# init Firebase
cred = credentials.Certificate(args.service_account)
firebase_admin.initialize_app(cred)
db = firestore.client()
tree, root, xml_recs = load_xml(args.xml)
fb_recs = load_firebase(db)
sync(tree, root, xml_recs, fb_recs)
print(f"Synchronized Firebase and XML file: {args.xml}")