XML Import Project
Build an importer that checks required ids, duplicate ids and non-negative quantities before calculating a total. Treat a failed record as a rejected exchange.
Your brief
Attempt the rules before opening the worked solution. The fixed fixture is small, so a set of ids and one pass are enough. Parsing alone does not enforce unique business identifiers or integer quantities. Make validation finish before applying any persistent change; otherwise a late error can leave a partially imported exchange. This project stays in memory and deliberately performs no database writes.
Run the code
Use Python 3 with lxml 6.1.1 in a separate virtual environment. The complete listing embeds its own XML and any schema or stylesheet; run python import-project.py. It does not need Apache or MySQL. The shared restrictive parse function rejects declarations for untrusted input. The trusted DTD/entity and streaming lessons explicitly explain their separate fixed-input boundaries.
Download the complete import-project.py source. Keep each lab in its own folder. Sources are served as text, not as public executable server scripts. Review the starting setup.
Worked solution
Show the worked solution
from lxml import etree as E
class DenyExternal(E.Resolver):
def resolve(self, url, public_id, context):
raise ValueError("External resources are disabled")
def parse(text):
if "<!DOCTYPE" in text.upper() or "<!ENTITY" in text.upper():
raise ValueError("Declarations are disabled for untrusted XML")
parser = E.XMLParser(resolve_entities=False, load_dtd=False,
no_network=True, huge_tree=False)
parser.resolvers.add(DenyExternal())
return E.fromstring(text.encode("utf-8"), parser)
source = "<exchange region=\"west\">\n <packet id=\"p1\" available=\"true\"><name>Marigold</name><quantity>12</quantity><category>flower</category></packet>\n <packet id=\"p2\" available=\"false\"><name>Fenugreek</name><quantity>0</quantity><category>herb</category></packet>\n <packet id=\"p3\" available=\"true\"><name>Basil</name><quantity>7</quantity><category>herb</category></packet>\n</exchange>"
root = parse(source)
seen = set()
total = 0
for packet in root.findall("packet"):
identifier = packet.get("id")
if not identifier or identifier in seen:
raise ValueError("Missing or repeated id")
seen.add(identifier)
text = packet.findtext("quantity", "")
if not text.isascii() or not text.isdigit():
raise ValueError("Quantity must be non-negative digits")
total += int(text)
print("Accepted", len(seen), "packets; total", total)
Expected result
Accepted 3 packets; total 19
The set catches repeated identity before any total is accepted. The digit rule is intentionally narrow and rejects signs, spaces and decimal fractions. A production contract should state whether those alternative numeric spellings are allowed.
Extend and test
Change the second id to p1, remove a quantity and set one quantity to -1 in separate runs. Each change should reject the exchange rather than produce a partial total.
Check the exact contract in the XML specification, XPath 1.0, the XSD 1.0 primer, lxml validation documentation and Saxon’s processor documentation.