fpml-convert 0.6.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,40 @@
1
+ """fpml_convert — FpML interest-rate-swap XML <-> JSON, standard library only.
2
+
3
+ The Python sibling of ``structile-fpml``'s JavaScript interpreter bundle.
4
+ Same projection mapping (the ``@``-prefixed, ``«id»``-reference,
5
+ local-name-reduced one that round-trips without the schema), same
6
+ supported-element subset and ``xs:sequence`` child-order binding — both
7
+ read from the shared ``src/rules.json``.
8
+
9
+ >>> from fpml_convert import fpml_to_json, json_to_fpml
10
+ >>> value = fpml_to_json(xml_text) # -> dict, == the JS bundle's output
11
+ >>> xml_text2 = json_to_fpml(value) # -> schema-ordered FpML
12
+
13
+ A ``--mapping fpml-awg`` interchange mode (the ISDA FpML AWG "Guidelines on
14
+ FpML to JSON Conversion" shape) is planned; this module ships only the
15
+ structile projection for now.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ from ._project import (
20
+ FpmlUnsupportedError,
21
+ LINK_CLOSE,
22
+ LINK_ID_FIELD,
23
+ LINK_OPEN,
24
+ fpml_to_json,
25
+ )
26
+ from ._serialize import json_to_fpml
27
+
28
+ #: FpML schema version this converter targets. Mirrors the JS bundle's
29
+ #: ``INTERPRETER_VERSION`` / ``package.json`` ``fpmlVersion``.
30
+ FPML_VERSION = "5.10"
31
+
32
+ __all__ = [
33
+ "fpml_to_json",
34
+ "json_to_fpml",
35
+ "FpmlUnsupportedError",
36
+ "FPML_VERSION",
37
+ "LINK_OPEN",
38
+ "LINK_CLOSE",
39
+ "LINK_ID_FIELD",
40
+ ]
@@ -0,0 +1,49 @@
1
+ """``python -m fpml_convert IN [-o OUT]`` — convert an FpML IR-swap
2
+ ``dataDocument`` between XML and JSON, direction inferred from the input
3
+ (``<`` first non-space char -> XML -> JSON, else JSON -> XML) or forced
4
+ with ``--to xml|json``.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import argparse
9
+ import json
10
+ import sys
11
+
12
+ from . import FPML_VERSION, fpml_to_json, json_to_fpml
13
+
14
+
15
+ def _looks_like_xml(text: str) -> bool:
16
+ stripped = text.lstrip()
17
+ return stripped.startswith("<")
18
+
19
+
20
+ def main(argv=None) -> int:
21
+ p = argparse.ArgumentParser(
22
+ prog="python -m fpml_convert",
23
+ description="FpML interest-rate-swap XML <-> JSON (structile projection).",
24
+ )
25
+ p.add_argument("input", help="path to an FpML .xml or a projected .json file, or - for stdin")
26
+ p.add_argument("-o", "--output", help="write here instead of stdout")
27
+ p.add_argument("--to", choices=("xml", "json"), help="force the output direction")
28
+ p.add_argument("--indent", type=int, default=2, help="JSON indent (default 2)")
29
+ p.add_argument("--version", action="version", version=f"fpml_convert (FpML {FPML_VERSION})")
30
+ args = p.parse_args(argv)
31
+
32
+ text = sys.stdin.read() if args.input == "-" else open(args.input, encoding="utf-8").read()
33
+
34
+ to = args.to or ("json" if _looks_like_xml(text) else "xml")
35
+ if to == "json":
36
+ out_text = json.dumps(fpml_to_json(text), indent=args.indent, ensure_ascii=False) + "\n"
37
+ else:
38
+ out_text = json_to_fpml(json.loads(text))
39
+
40
+ if args.output:
41
+ with open(args.output, "w", encoding="utf-8") as fh:
42
+ fh.write(out_text)
43
+ else:
44
+ sys.stdout.write(out_text)
45
+ return 0
46
+
47
+
48
+ if __name__ == "__main__":
49
+ sys.exit(main())
@@ -0,0 +1,225 @@
1
+ """FpML XML -> plain value projection — a faithful port of the JS bundle's
2
+ ``src/20-ns.js`` + ``src/30-project.js`` + ``src/40-support.js`` +
3
+ ``src/50-swap.js``. The rule tables come from the shared ``src/rules.json``
4
+ (see :mod:`fpml_convert._rules`); only this traversal logic is duplicated
5
+ between the two implementations, and the committed JS snapshots
6
+ (``test/__snapshots__/*.json``) are the cross-language contract.
7
+
8
+ Projection rules (identical to the JS interpreter):
9
+
10
+ * element local name -> key; a repeated tag under one parent -> a list,
11
+ in document order.
12
+ * attributes -> ``@``-prefixed keys: ``@id`` first, then ``@xsi:type``,
13
+ then ``@href``, then any other plain attribute, all BEFORE the child
14
+ keys. ``xmlns*`` and other ``xsi:*`` attributes are dropped.
15
+ * a pure reference element (``href``, nothing else) -> the link scalar
16
+ ``«href»``. One that also carries content stays a dict but still gets
17
+ ``@href = «href»``.
18
+ * ``xsi:type`` -> ``@xsi:type`` verbatim. Typed lexical forms are NEVER
19
+ coerced — every leaf value stays the exact source string.
20
+ * every element descended into is checked against ``FPML_SUPPORTED``
21
+ first; an unknown element is a loud error, not a best-effort guess.
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import xml.etree.ElementTree as ET
26
+
27
+ from ._rules import FPML_SUPPORTED
28
+
29
+ LINK_OPEN = "«" # «
30
+ LINK_CLOSE = "»" # »
31
+ LINK_ID_FIELD = "@id"
32
+
33
+ _XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
34
+
35
+
36
+ class FpmlUnsupportedError(Exception):
37
+ """Raised for an element outside the supported IR-swap subset. A
38
+ structural "I don't recognise this" — distinct from the soft rule
39
+ violations Structile's ``__structileDiagnostic`` is for."""
40
+
41
+
42
+ def fpml_local_name(name) -> str:
43
+ """``{uri}local`` -> ``local``; ``prefix:local`` -> ``local``;
44
+ ``local`` -> ``local``. Reduces both DOM providers' spellings."""
45
+ t = "" if name is None else str(name)
46
+ brace = t.rfind("}")
47
+ if brace != -1:
48
+ return t[brace + 1:]
49
+ colon = t.rfind(":")
50
+ return t[colon + 1:] if colon != -1 else t
51
+
52
+
53
+ def fpml_element_local_name(el) -> str:
54
+ return fpml_local_name(getattr(el, "tag", None))
55
+
56
+
57
+ def _attr_items(el):
58
+ """``el.attrib`` items in source order (ElementTree preserves it since
59
+ Python 3.8)."""
60
+ return list(el.attrib.items())
61
+
62
+
63
+ def fpml_xsi_type(el):
64
+ for raw, val in _attr_items(el):
65
+ if raw == "xsi:type":
66
+ return val
67
+ if raw.startswith("{") and "}" in raw:
68
+ uri = raw[1:raw.index("}")]
69
+ if uri == _XSI_NS and fpml_local_name(raw) == "type":
70
+ return val
71
+ return None
72
+
73
+
74
+ def fpml_plain_attributes(el):
75
+ """Non-namespaced "business" attributes as an ordered ``dict``, with the
76
+ structural ones removed: ``xmlns*``, ``xsi:*`` (``xsi:type`` / ``xsi:nil``
77
+ are surfaced separately), and ``id`` / ``href`` (projected explicitly as
78
+ ``@id`` / ``@href``)."""
79
+ out = {}
80
+ for raw, val in _attr_items(el):
81
+ if raw == "xmlns" or raw.startswith("xmlns:"):
82
+ continue
83
+ if raw in ("id", "href"):
84
+ continue
85
+ if raw.startswith("{"):
86
+ uri = raw[1:raw.index("}")]
87
+ if uri == _XSI_NS:
88
+ continue
89
+ elif raw.startswith("xsi:"):
90
+ continue
91
+ out[fpml_local_name(raw)] = val
92
+ return out
93
+
94
+
95
+ def fpml_is_xsi_nil(el) -> bool:
96
+ for raw, val in _attr_items(el):
97
+ is_nil_attr = raw == "xsi:nil" or (
98
+ raw.startswith("{") and "}" in raw
99
+ and fpml_local_name(raw) == "nil" and raw[1:raw.index("}")] == _XSI_NS
100
+ )
101
+ if is_nil_attr:
102
+ return str(val).strip() == "true"
103
+ return False
104
+
105
+
106
+ def fpml_own_text(el) -> str:
107
+ """A leaf element's own trimmed text, or ``""``. Only ever called on a
108
+ childless element, so ``el.text`` IS its whole text."""
109
+ return (el.text or "").strip()
110
+
111
+
112
+ def _wrap_ref(id_: str) -> str:
113
+ return LINK_OPEN + id_ + LINK_CLOSE
114
+
115
+
116
+ def fpml_assert_supported(el) -> None:
117
+ name = fpml_element_local_name(el)
118
+ if name not in FPML_SUPPORTED:
119
+ raise FpmlUnsupportedError(
120
+ "fpml converter (read-only IR-swap subset): element <" + name + "> "
121
+ "is outside the supported subset. This is by design — the subset "
122
+ "is widened only as real FpML example files are added to the test "
123
+ "corpus."
124
+ )
125
+
126
+
127
+ def fpml_project_element(el):
128
+ fpml_assert_supported(el)
129
+
130
+ id_ = el.get("id")
131
+ href = el.get("href")
132
+ xsi_type = fpml_xsi_type(el)
133
+ is_nil = fpml_is_xsi_nil(el)
134
+ plain = fpml_plain_attributes(el)
135
+ kids = list(el)
136
+ has_plain = len(plain) > 0
137
+
138
+ # Pure reference: href, nothing else of substance.
139
+ if href is not None and not kids and not has_plain and xsi_type is None and fpml_own_text(el) == "":
140
+ return _wrap_ref(href)
141
+
142
+ # Childless: a scalar (or a small dict when it has attributes to keep).
143
+ if not kids:
144
+ text = fpml_own_text(el)
145
+ bare = None if is_nil else (None if text == "" else text)
146
+ if id_ is None and href is None and xsi_type is None and not has_plain:
147
+ return bare
148
+ leaf = {}
149
+ if id_ is not None:
150
+ leaf["@id"] = str(id_)
151
+ if xsi_type is not None:
152
+ leaf["@xsi:type"] = str(xsi_type)
153
+ if href is not None:
154
+ leaf["@href"] = _wrap_ref(href)
155
+ for k, v in plain.items():
156
+ leaf["@" + k] = v
157
+ if is_nil:
158
+ leaf["@xsi:nil"] = "true"
159
+ if text != "":
160
+ leaf["#text"] = text
161
+ return leaf
162
+
163
+ # Container: attributes first (fixed order), then grouped children.
164
+ out = {}
165
+ if id_ is not None:
166
+ out["@id"] = str(id_)
167
+ if xsi_type is not None:
168
+ out["@xsi:type"] = str(xsi_type)
169
+ if href is not None:
170
+ out["@href"] = _wrap_ref(href)
171
+ for k, v in plain.items():
172
+ out["@" + k] = v
173
+ if is_nil:
174
+ out["@xsi:nil"] = "true"
175
+
176
+ order = []
177
+ groups = {}
178
+ for kid in kids:
179
+ key = fpml_element_local_name(kid)
180
+ if key not in groups:
181
+ groups[key] = []
182
+ order.append(key)
183
+ groups[key].append(kid)
184
+ for key in order:
185
+ g = groups[key]
186
+ out[key] = fpml_project_element(g[0]) if len(g) == 1 else [fpml_project_element(x) for x in g]
187
+ return out
188
+
189
+
190
+ def _has_descendant(root, local_name: str) -> bool:
191
+ return any(fpml_element_local_name(e) == local_name for e in root.iter())
192
+
193
+
194
+ def fpml_assert_is_ir_swap_data_document(root) -> None:
195
+ """Root gate — the throw that tells interpreter selection FpML apart
196
+ from every other candidate. Rejects anything that isn't an FpML
197
+ interest-rate-swap ``<dataDocument>``."""
198
+ if root is None or fpml_element_local_name(root) != "dataDocument":
199
+ got = fpml_element_local_name(root) if root is not None else "?"
200
+ raise ValueError(
201
+ "fpml converter: expected a root <dataDocument> element, got <" + got + "> "
202
+ "— this doesn't look like an FpML dataDocument."
203
+ )
204
+ ver = root.get("fpmlVersion")
205
+ if ver is not None and not str(ver).startswith("5-"):
206
+ raise ValueError(
207
+ 'fpml converter: fpmlVersion="' + str(ver) + '" is not an FpML 5.x document.'
208
+ )
209
+ if not _has_descendant(root, "swap"):
210
+ raise ValueError(
211
+ "fpml converter: this <dataDocument> contains no <swap> — only the "
212
+ "interest-rate-swap product family is supported in this read-only build."
213
+ )
214
+
215
+
216
+ def fpml_to_json(xml_text: str):
217
+ """FpML interest-rate-swap XML -> the nested value Structile renders.
218
+
219
+ Raises :class:`ValueError` if the document isn't an FpML IR-swap
220
+ ``dataDocument``, or :class:`FpmlUnsupportedError` for an element
221
+ outside the supported subset.
222
+ """
223
+ root = ET.fromstring(xml_text)
224
+ fpml_assert_is_ir_swap_data_document(root)
225
+ return fpml_project_element(root)
fpml_convert/_rules.py ADDED
@@ -0,0 +1,40 @@
1
+ """Loads the language-neutral projection rule tables from ``src/rules.json``
2
+ — the SAME file ``scripts/build.mjs`` injects into the JS bundle as
3
+ ``const FPML_RULES``. One source of truth for the supported-element set and
4
+ the ``xs:sequence`` child-order binding across both implementations.
5
+
6
+ Lookup order:
7
+ 1. ``rules.json`` sitting next to this package (a packaging step copies
8
+ it there for the wheel — see the FpML interpreter plan Phase 8);
9
+ 2. ``../../src/rules.json`` relative to this file (a repo checkout).
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ from pathlib import Path
15
+
16
+ _HERE = Path(__file__).resolve().parent
17
+
18
+
19
+ def _locate() -> Path:
20
+ packaged = _HERE / "rules.json"
21
+ if packaged.is_file():
22
+ return packaged
23
+ repo_copy = _HERE.parents[1] / "src" / "rules.json"
24
+ if repo_copy.is_file():
25
+ return repo_copy
26
+ raise FileNotFoundError(
27
+ "fpml_convert: rules.json not found next to the package or at "
28
+ f"{repo_copy} — a repo checkout or a packaged copy is required"
29
+ )
30
+
31
+
32
+ _RAW = json.loads(_locate().read_text(encoding="utf-8"))
33
+
34
+ #: ``set`` of every element local-name the projection is allowed to descend
35
+ #: into; anything else is a :class:`~fpml_convert.FpmlUnsupportedError`.
36
+ FPML_SUPPORTED: frozenset = frozenset(_RAW["supported"])
37
+
38
+ #: ``{parent_local_name: [child_local_name, ...]}`` — serialize re-emits a
39
+ #: dict's children in this order, not the (user-editable) key order.
40
+ FPML_CHILD_ORDER: dict = dict(_RAW["childOrder"])
@@ -0,0 +1,124 @@
1
+ """Value -> FpML XML — the inverse of :func:`fpml_convert._project.fpml_to_json`,
2
+ a faithful port of the JS bundle's ``src/70-serialize.js`` (which uses the
3
+ shared ``FPML_CHILD_ORDER`` from ``src/rules.json``). Round-trips at the
4
+ VALUE level: ``fpml_to_json(json_to_fpml(fpml_to_json(x))) == fpml_to_json(x)``.
5
+
6
+ Policies, matching the JS side exactly:
7
+
8
+ * typed lexical forms are written back verbatim (no coercion — ``1000000``
9
+ vs ``1000000.00`` survives);
10
+ * the confirmation-view default namespace + ``xsi:`` are re-declared on the
11
+ root; ``xsi:schemaLocation`` is not reproduced;
12
+ * children are re-emitted in ``FPML_CHILD_ORDER`` order, so a value whose
13
+ keys were reordered still serialises as schema-ordered FpML.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ from ._project import LINK_CLOSE, LINK_OPEN
18
+ from ._rules import FPML_CHILD_ORDER
19
+
20
+ _FPML_CONFIRMATION_NS = "http://www.fpml.org/FpML-5/confirmation"
21
+ _FPML_XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
22
+
23
+
24
+ def _esc_attr(s) -> str:
25
+ return (str(s).replace("&", "&amp;").replace("<", "&lt;")
26
+ .replace(">", "&gt;").replace('"', "&quot;"))
27
+
28
+
29
+ def _esc_text(s) -> str:
30
+ return str(s).replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
31
+
32
+
33
+ def _is_ref(v) -> bool:
34
+ return isinstance(v, str) and v.startswith(LINK_OPEN) and v.endswith(LINK_CLOSE)
35
+
36
+
37
+ def _unwrap_ref(v: str) -> str:
38
+ return v[len(LINK_OPEN):len(v) - len(LINK_CLOSE)]
39
+
40
+
41
+ def _order_child_pairs(parent_local_name, pairs):
42
+ order = FPML_CHILD_ORDER.get(parent_local_name)
43
+ if not order:
44
+ return pairs
45
+ rank = {name: i for i, name in enumerate(order)}
46
+ decorated = [
47
+ (rank[p[0]] if p[0] in rank else len(order) + i, i, p)
48
+ for i, p in enumerate(pairs)
49
+ ]
50
+ decorated.sort(key=lambda t: (t[0], t[1]))
51
+ return [t[2] for t in decorated]
52
+
53
+
54
+ def _serialize_value(key, val, indent, out):
55
+ if isinstance(val, list):
56
+ for item in val:
57
+ _serialize_value(key, item, indent, out)
58
+ return
59
+ if val is None:
60
+ out.append(indent + "<" + key + "/>")
61
+ return
62
+ if not isinstance(val, dict):
63
+ if _is_ref(val):
64
+ out.append(indent + "<" + key + ' href="' + _esc_attr(_unwrap_ref(val)) + '"/>')
65
+ else:
66
+ out.append(indent + "<" + key + ">" + _esc_text(val) + "</" + key + ">")
67
+ return
68
+
69
+ attrs = []
70
+ nil = False
71
+ text = None
72
+ child_pairs = []
73
+ for k in val:
74
+ if k == "@xsi:nil":
75
+ nil = str(val[k]) == "true"
76
+ elif k == "@id":
77
+ attrs.append(("id", val[k]))
78
+ elif k == "@xsi:type":
79
+ attrs.append(("xsi:type", val[k]))
80
+ elif k == "@href":
81
+ attrs.append(("href", _unwrap_ref(val[k]) if _is_ref(val[k]) else val[k]))
82
+ elif k.startswith("@"):
83
+ attrs.append((k[1:], val[k]))
84
+ elif k == "#text":
85
+ text = val[k]
86
+ else:
87
+ child_pairs.append((k, val[k]))
88
+
89
+ attr_str = "".join(' %s="%s"' % (name, _esc_attr(v)) for name, v in attrs)
90
+ if nil:
91
+ attr_str += ' xsi:nil="true"'
92
+ open_tag = "<" + key + attr_str
93
+
94
+ if nil or (not child_pairs and text is None):
95
+ out.append(indent + open_tag + "/>")
96
+ return
97
+ if not child_pairs:
98
+ out.append(indent + open_tag + ">" + _esc_text(text) + "</" + key + ">")
99
+ return
100
+
101
+ out.append(indent + open_tag + ">")
102
+ ci = indent + " "
103
+ if text is not None:
104
+ out.append(ci + _esc_text(text)) # mixed content (not seen in the subset)
105
+ for cp in _order_child_pairs(key, child_pairs):
106
+ _serialize_value(cp[0], cp[1], ci, out)
107
+ out.append(indent + "</" + key + ">")
108
+
109
+
110
+ def json_to_fpml(value) -> str:
111
+ """The projected ``dataDocument`` value -> FpML text."""
112
+ if not isinstance(value, dict):
113
+ raise ValueError("fpml json_to_fpml: expected the projected <dataDocument> object")
114
+ ver = value["@fpmlVersion"] if isinstance(value.get("@fpmlVersion"), str) else "5-10"
115
+ out = [
116
+ '<?xml version="1.0" encoding="UTF-8"?>',
117
+ '<dataDocument xmlns="%s" xmlns:xsi="%s" fpmlVersion="%s">'
118
+ % (_FPML_CONFIRMATION_NS, _FPML_XSI_NS, _esc_attr(ver)),
119
+ ]
120
+ child_pairs = [(k, value[k]) for k in value if not k.startswith("@")]
121
+ for cp in _order_child_pairs("dataDocument", child_pairs):
122
+ _serialize_value(cp[0], cp[1], " ", out)
123
+ out.append("</dataDocument>")
124
+ return "\n".join(out) + "\n"
@@ -0,0 +1,97 @@
1
+ {
2
+ "_comment": "Language-neutral FpML projection rule tables, the single source of truth shared by the JS bundle (injected by scripts/build.mjs as `const FPML_RULES`, consumed by src/40-support.js and src/60-binding.js) and the Python converter (python/fpml_convert/_rules.py). Widen the same way as before: against a real corpus file or the FpML 5.10 XSD, never speculatively. See src/40-support.js and src/60-binding.js headers for the rationale behind each table.",
3
+
4
+ "supported": [
5
+ "dataDocument", "trade", "tradeHeader", "partyTradeIdentifier",
6
+ "partyReference", "tradeId", "tradeDate", "party", "partyId", "partyName",
7
+ "calculationAgent", "calculationAgentPartyReference",
8
+
9
+ "swap", "swapStream",
10
+ "payerPartyReference", "receiverPartyReference",
11
+
12
+ "calculationPeriodDates", "effectiveDate", "terminationDate",
13
+ "unadjustedDate", "adjustedStartDate", "adjustedEndDate",
14
+ "dateAdjustments", "businessDayConvention", "businessCenters",
15
+ "businessCenter", "businessCentersReference",
16
+ "calculationPeriodDatesAdjustments", "calculationPeriodFrequency",
17
+ "periodMultiplier", "period", "rollConvention",
18
+ "firstPeriodStartDate", "firstRegularPeriodStartDate",
19
+ "lastRegularPeriodEndDate", "firstPaymentDate",
20
+ "stubCalculationPeriodAmount", "initialStub", "finalStub", "stubRate",
21
+
22
+ "paymentDates", "calculationPeriodDatesReference", "paymentFrequency",
23
+ "payRelativeTo", "paymentDatesAdjustments", "paymentDaysOffset",
24
+ "adjustedPaymentDate", "paymentDate",
25
+
26
+ "resetDates", "resetRelativeTo", "fixingDates", "dayType", "dateRelativeTo",
27
+ "resetFrequency", "resetDatesAdjustments", "adjustedFixingDate",
28
+
29
+ "calculationPeriodAmount", "calculation", "compoundingMethod",
30
+ "notionalSchedule", "notionalStepSchedule", "notionalAmount",
31
+ "initialValue", "currency", "step", "stepDate", "stepValue",
32
+ "fixedRateSchedule", "fixedRate", "spreadSchedule",
33
+ "floatingRateCalculation", "floatingRateDefinition", "floatingRate",
34
+ "floatingRateIndex", "indexTenor", "finalRateRounding",
35
+ "roundingDirection", "precision", "rateObservation", "observationWeight",
36
+ "dayCountFraction",
37
+
38
+ "calculationPeriod", "paymentCalculationPeriod",
39
+ "cashflows", "cashflowsMatchParameters", "additionalPayment",
40
+ "paymentAmount", "amount",
41
+
42
+ "principalExchanges", "initialExchange", "intermediateExchange",
43
+ "finalExchange", "principalExchange", "principalExchangeAmount",
44
+ "principalExchangeDate", "adjustedPrincipalExchangeDate"
45
+ ],
46
+
47
+ "childOrder": {
48
+ "additionalPayment": ["payerPartyReference", "receiverPartyReference", "paymentAmount", "paymentDate"],
49
+ "businessCenters": ["businessCenter"],
50
+ "calculation": ["notionalSchedule", "fixedRateSchedule", "floatingRateCalculation", "dayCountFraction", "compoundingMethod"],
51
+ "calculationAgent": ["calculationAgentPartyReference"],
52
+ "calculationPeriod": ["adjustedStartDate", "adjustedEndDate", "notionalAmount", "floatingRateDefinition", "fixedRate"],
53
+ "calculationPeriodAmount": ["calculation"],
54
+ "calculationPeriodDates": ["effectiveDate", "terminationDate", "calculationPeriodDatesAdjustments", "firstPeriodStartDate", "firstRegularPeriodStartDate", "lastRegularPeriodEndDate", "calculationPeriodFrequency"],
55
+ "calculationPeriodDatesAdjustments": ["businessDayConvention", "businessCentersReference", "businessCenters"],
56
+ "calculationPeriodFrequency": ["periodMultiplier", "period", "rollConvention"],
57
+ "cashflows": ["cashflowsMatchParameters", "principalExchange", "paymentCalculationPeriod"],
58
+ "dataDocument": ["trade", "party"],
59
+ "dateAdjustments": ["businessDayConvention", "businessCenters", "businessCentersReference"],
60
+ "effectiveDate": ["unadjustedDate", "dateAdjustments"],
61
+ "finalRateRounding": ["roundingDirection", "precision"],
62
+ "finalStub": ["floatingRate"],
63
+ "firstPeriodStartDate": ["unadjustedDate", "dateAdjustments"],
64
+ "fixedRateSchedule": ["initialValue", "step"],
65
+ "fixingDates": ["periodMultiplier", "period", "dayType", "businessDayConvention", "businessCenters", "dateRelativeTo"],
66
+ "floatingRate": ["floatingRateIndex", "indexTenor"],
67
+ "floatingRateCalculation": ["floatingRateIndex", "indexTenor", "finalRateRounding", "spreadSchedule"],
68
+ "floatingRateDefinition": ["rateObservation"],
69
+ "indexTenor": ["periodMultiplier", "period"],
70
+ "initialStub": ["floatingRate", "stubRate"],
71
+ "notionalSchedule": ["notionalStepSchedule"],
72
+ "notionalStepSchedule": ["initialValue", "step", "currency"],
73
+ "party": ["partyId", "partyName"],
74
+ "partyTradeIdentifier": ["partyReference", "tradeId"],
75
+ "paymentAmount": ["currency", "amount"],
76
+ "paymentCalculationPeriod": ["adjustedPaymentDate", "calculationPeriod"],
77
+ "paymentDate": ["unadjustedDate", "dateAdjustments"],
78
+ "paymentDates": ["calculationPeriodDatesReference", "paymentFrequency", "firstPaymentDate", "payRelativeTo", "paymentDaysOffset", "paymentDatesAdjustments"],
79
+ "paymentDatesAdjustments": ["businessDayConvention", "businessCentersReference", "businessCenters"],
80
+ "paymentDaysOffset": ["periodMultiplier", "period", "dayType"],
81
+ "paymentFrequency": ["periodMultiplier", "period"],
82
+ "principalExchange": ["adjustedPrincipalExchangeDate", "principalExchangeAmount"],
83
+ "principalExchanges": ["initialExchange", "finalExchange", "intermediateExchange"],
84
+ "rateObservation": ["adjustedFixingDate", "observationWeight"],
85
+ "resetDates": ["calculationPeriodDatesReference", "resetRelativeTo", "fixingDates", "resetFrequency", "resetDatesAdjustments"],
86
+ "resetDatesAdjustments": ["businessDayConvention", "businessCentersReference", "businessCenters"],
87
+ "resetFrequency": ["periodMultiplier", "period"],
88
+ "spreadSchedule": ["initialValue"],
89
+ "step": ["stepDate", "stepValue"],
90
+ "stubCalculationPeriodAmount": ["calculationPeriodDatesReference", "initialStub", "finalStub"],
91
+ "swap": ["swapStream", "additionalPayment"],
92
+ "swapStream": ["payerPartyReference", "receiverPartyReference", "calculationPeriodDates", "paymentDates", "resetDates", "calculationPeriodAmount", "stubCalculationPeriodAmount", "principalExchanges", "cashflows"],
93
+ "terminationDate": ["unadjustedDate", "dateAdjustments"],
94
+ "trade": ["tradeHeader", "swap", "calculationAgent", "fra"],
95
+ "tradeHeader": ["partyTradeIdentifier", "tradeDate"]
96
+ }
97
+ }
@@ -0,0 +1,75 @@
1
+ Metadata-Version: 2.4
2
+ Name: fpml-convert
3
+ Version: 0.6.0
4
+ Summary: FpML interest-rate-swap XML <-> JSON, standard library only. One projection mapping, also shipped as a JS interpreter and a C++17 converter.
5
+ License-Expression: Apache-2.0
6
+ Project-URL: Homepage, https://danieltuzes.github.io/fpml-convert/
7
+ Project-URL: Source, https://github.com/danieltuzes/fpml-convert
8
+ Keywords: fpml,xml,json,structile,derivatives,swap
9
+ Requires-Python: >=3.8
10
+ Description-Content-Type: text/markdown
11
+
12
+ # fpml-convert (Python)
13
+
14
+ Standard-library-only conversion between **FpML interest-rate-swap XML and
15
+ plain JSON**, in both directions. No `lxml`, no third-party JSON — just
16
+ `xml.etree.ElementTree` and `json`.
17
+
18
+ One projection mapping, also implemented as a JavaScript interpreter (for
19
+ the [Structile](https://danieltuzes.github.io/structile/) viewer) and a
20
+ pure-STL C++17 converter; the three agree byte-for-byte on a shared test
21
+ corpus. Downloads, the full version archive, and the mapping's precise
22
+ rules: <https://danieltuzes.github.io/fpml-convert/>.
23
+
24
+ ## Install
25
+
26
+ ```sh
27
+ pip install fpml-convert
28
+ ```
29
+
30
+ No runtime dependencies. Python 3.8+.
31
+
32
+ ## Use
33
+
34
+ ```python
35
+ from fpml_convert import fpml_to_json, json_to_fpml
36
+
37
+ value = fpml_to_json(xml_text) # -> dict
38
+ xml_text2 = json_to_fpml(value) # -> schema-ordered FpML, round-trips at the value level
39
+ ```
40
+
41
+ ```sh
42
+ # CLI (direction inferred from the input, or forced with --to)
43
+ python -m fpml_convert swap.xml -o swap.json
44
+ python -m fpml_convert swap.json -o swap.xml
45
+ cat swap.xml | python -m fpml_convert - > swap.json
46
+ ```
47
+
48
+ ## The mapping
49
+
50
+ - Element local name → object key; a tag repeated under one parent → an
51
+ array in document order.
52
+ - Namespace prefixes and ElementTree Clark notation (`{uri}local`) are both
53
+ reduced to the local name, so the result is identical across a browser
54
+ `DOMParser` and a server-side parser.
55
+ - Attributes → `@`-prefixed keys, before any child keys, ordered `@id`,
56
+ `@xsi:type`, `@href`, then the rest. `xsi:type` is kept verbatim.
57
+ - A pure reference element (`href`, no children) → the scalar `«id»`;
58
+ Structile renders that as a link to the element whose `@id` matches.
59
+ - Typed lexical forms are preserved as strings, never coerced
60
+ (`1000000.00` stays exactly that).
61
+ - Every element the projection descends into is checked against a
62
+ supported-subset allowlist — an unknown element raises
63
+ `FpmlUnsupportedError` rather than projecting partially.
64
+ - Serialization re-emits children in schema (`xs:sequence`) order.
65
+
66
+ ## Scope
67
+
68
+ FpML 5.10 confirmation view, interest-rate **swap** `dataDocument` only. A
69
+ non-IR-swap document raises `ValueError`. FRA and other products widen the
70
+ allowlist in later releases. An FpML-AWG interchange-shape mode is planned.
71
+
72
+ ## License
73
+
74
+ Apache-2.0. The FpML example files used for testing are redistributed
75
+ under the [FpML Public License](https://www.fpml.org/the_standard/fpml-public-license/).
@@ -0,0 +1,11 @@
1
+ fpml_convert/__init__.py,sha256=sduXmwcB3l_cqCjmHyCqGQO8BhEIVMn5aPudjeyPjsw,1294
2
+ fpml_convert/__main__.py,sha256=0bWfh5bwOolBBxazp3nvov6c5x_okFHHegLdtqF-1Fg,1706
3
+ fpml_convert/_project.py,sha256=R45jLMwqBpewO6rTXNmt4WPpduRZ8TaXvgyYQNauEOE,7916
4
+ fpml_convert/_rules.py,sha256=nDGukMH7K2uph4gQ1-AhJnf_KGDF9SpG0E1bhDMRUGw,1507
5
+ fpml_convert/_serialize.py,sha256=dcUy1S4A235PerRq75qQ_jBUgOiadrgzz43akW2gjs0,4427
6
+ fpml_convert/rules.json,sha256=gfqLyVcSt8XKXhyh3Iis0yq4rZiOQGvphL1CfkrKToA,6360
7
+ fpml_convert-0.6.0.dist-info/METADATA,sha256=ZeIY_m6m6uENixfzXBRYFNUQjkBxn9uh6B5znvCxfxE,2913
8
+ fpml_convert-0.6.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
9
+ fpml_convert-0.6.0.dist-info/entry_points.txt,sha256=XFT4A778pZGisykQBZYHZRJPtcLmLRs65f6SU0JGpQI,60
10
+ fpml_convert-0.6.0.dist-info/top_level.txt,sha256=ob07tfqEr_wbCRQlyWRAQDyKc3cs4ARq1t-Z13Wq7Ns,13
11
+ fpml_convert-0.6.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ fpml-convert = fpml_convert.__main__:main
@@ -0,0 +1 @@
1
+ fpml_convert