lvxml 0.2.1__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.
lvxml/_treejson.py ADDED
@@ -0,0 +1,162 @@
1
+ """Check a tree's shape against the schema and convert it to and from JSON-ready dicts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ._errors import LVXMLError
6
+ from ._node import LVDATA_NAMESPACE, Document, Node
7
+ from ._parse import MAX_DEPTH
8
+ from ._schema import ARRAY_ITEM_TAGS, CLUSTERED_TAGS, ENUM_RANGES, SIMPLE_TAGS, VALUE_TAGS
9
+
10
+ _EXTRA_ORDER = ("RefKind", "RefType", "ActiveXType.GUID", "ActiveXType.Name", "DotNetType.Name")
11
+ _NEEDS = {"RefType": "RefKind", "ActiveXType.Name": "ActiveXType.GUID"}
12
+ _FIELDS = ("name", "val", "choices", "dims", "num_elts", "items", "extra")
13
+
14
+
15
+ def check_shape(node: Node, path: str, depth: int = 1) -> None:
16
+ """Raise unless `node` only uses the parts LVXMLSchema.xsd gives its tag."""
17
+ tag = node.tag
18
+ here = f"{path}/{tag}" if path else tag
19
+ if depth > MAX_DEPTH:
20
+ raise LVXMLError(f"nesting deeper than {MAX_DEPTH} levels is not supported", path)
21
+ if tag not in VALUE_TAGS:
22
+ raise LVXMLError(f"unknown LabVIEW type tag {tag!r}", here)
23
+ used = {f for f in _FIELDS[1:] if getattr(node, f) not in (None, ())}
24
+ allowed = _allowed(tag)
25
+ if used - allowed or (allowed & {"val", "num_elts", "dims", "choices"}) - used:
26
+ raise LVXMLError(f"<{tag}> takes {sorted(allowed)}, got {sorted(used)}", here)
27
+ if (node.num_elts or 0) < 0 or any(dim < 0 for dim in node.dims):
28
+ raise LVXMLError("NumElts and Dimsize cannot be negative", here)
29
+ keys = [key for key, _ in node.extra]
30
+ order = [_EXTRA_ORDER.index(k) if k in _EXTRA_ORDER else -1 for k in keys]
31
+ if -1 in order or order != sorted(set(order)):
32
+ raise LVXMLError(f"Refnum fields must be a subset of {list(_EXTRA_ORDER)} in order", here)
33
+ for key, needed in _NEEDS.items():
34
+ if key in keys and needed not in keys:
35
+ raise LVXMLError(f"Refnum field {key} needs {needed}", here)
36
+ members = frozenset({"Cluster"}) if tag in CLUSTERED_TAGS else _members(tag)
37
+ for item in node.items:
38
+ if item.tag not in members:
39
+ raise LVXMLError(f"<{item.tag}> is not allowed inside <{tag}>", here)
40
+ check_shape(item, here, depth + 1)
41
+
42
+
43
+ def _allowed(tag: str) -> set[str]:
44
+ if tag in SIMPLE_TAGS:
45
+ return {"val"}
46
+ if tag in ENUM_RANGES:
47
+ return {"val", "choices"}
48
+ if tag == "Refnum":
49
+ return {"val", "extra"}
50
+ if tag == "Array":
51
+ return {"dims", "items"}
52
+ if tag == "Cluster":
53
+ return {"num_elts", "items"}
54
+ return {"items"}
55
+
56
+
57
+ def _members(tag: str) -> frozenset[str]:
58
+ return ARRAY_ITEM_TAGS if tag == "Array" else VALUE_TAGS
59
+
60
+
61
+ def node_to_dict(node: Node) -> dict[str, object]:
62
+ data: dict[str, object] = {"tag": node.tag}
63
+ if node.tag != "Version":
64
+ data["name"] = node.name
65
+ if node.val is not None:
66
+ data["val"] = node.val
67
+ if node.choices:
68
+ data["choices"] = list(node.choices)
69
+ if node.dims:
70
+ data["dims"] = list(node.dims)
71
+ if node.num_elts is not None:
72
+ data["num_elts"] = node.num_elts
73
+ if node.extra:
74
+ data["extra"] = [list(pair) for pair in node.extra]
75
+ if node.items or node.tag in {"Array", "Cluster", "LvVariant"} | CLUSTERED_TAGS:
76
+ data["items"] = [node_to_dict(item) for item in node.items]
77
+ return data
78
+
79
+
80
+ def document_to_dict(document: Document) -> dict[str, object]:
81
+ return {
82
+ "wrapped": document.wrapped,
83
+ "namespace": document.namespace,
84
+ "items": [node_to_dict(node) for node in document.items],
85
+ }
86
+
87
+
88
+ def _get(data: dict[str, object], key: str, kind: type, path: str) -> object:
89
+ value = data.get(key)
90
+ if value is not None and (
91
+ not isinstance(value, kind) or isinstance(value, bool) != (kind is bool)
92
+ ):
93
+ raise LVXMLError(f"field {key!r} must be {kind.__name__}", path)
94
+ return value
95
+
96
+
97
+ def _strings(value: object, key: str, path: str) -> tuple[str, ...]:
98
+ items = value if isinstance(value, list) else []
99
+ if not all(isinstance(item, str) for item in items):
100
+ raise LVXMLError(f"field {key!r} must be a list of strings", path)
101
+ return tuple(str(item) for item in items)
102
+
103
+
104
+ def node_from_dict(data: object, path: str, depth: int = 1) -> Node:
105
+ if depth > MAX_DEPTH:
106
+ raise LVXMLError(f"nesting deeper than {MAX_DEPTH} levels is not supported", path)
107
+ if not isinstance(data, dict):
108
+ raise LVXMLError("a node must be an object", path)
109
+ unknown = set(data) - {"tag", *_FIELDS}
110
+ if unknown:
111
+ raise LVXMLError(f"unknown node fields {sorted(unknown)}", path)
112
+ tag = _get(data, "tag", str, path)
113
+ if not isinstance(tag, str):
114
+ raise LVXMLError("a node needs a string 'tag'", path)
115
+ here = f"{path}/{tag}" if path else tag
116
+ if tag != "Version" and "name" not in data:
117
+ raise LVXMLError("a node needs a 'name' (use \"\" for an unnamed one)", here)
118
+ dims = _get(data, "dims", list, here) or []
119
+ assert isinstance(dims, list)
120
+ if not all(isinstance(d, int) and not isinstance(d, bool) and d >= 0 for d in dims):
121
+ raise LVXMLError("field 'dims' must be a list of non-negative ints", here)
122
+ extra = _get(data, "extra", list, here) or []
123
+ assert isinstance(extra, list)
124
+ pairs = tuple(_strings(pair, "extra", here) for pair in extra)
125
+ if any(len(pair) != 2 for pair in pairs):
126
+ raise LVXMLError("field 'extra' must hold [key, text] pairs", here)
127
+ items = _get(data, "items", list, here) or []
128
+ assert isinstance(items, list)
129
+ num_elts = _get(data, "num_elts", int, here)
130
+ name, val = _get(data, "name", str, here), _get(data, "val", str, here)
131
+ return Node(
132
+ tag,
133
+ name=name if isinstance(name, str) else "",
134
+ val=val if isinstance(val, str) else None,
135
+ choices=_strings(_get(data, "choices", list, here), "choices", here),
136
+ dims=tuple(int(d) for d in dims),
137
+ num_elts=num_elts if isinstance(num_elts, int) else None,
138
+ items=tuple(node_from_dict(item, here, depth + 1) for item in items),
139
+ extra=tuple((pair[0], pair[1]) for pair in pairs),
140
+ )
141
+
142
+
143
+ def document_from_dict(data: object) -> Document:
144
+ if not isinstance(data, dict) or not isinstance(data.get("items"), list):
145
+ raise LVXMLError("a document must be an object with an 'items' list")
146
+ unknown = set(data) - {"wrapped", "namespace", "items"}
147
+ if unknown:
148
+ raise LVXMLError(f"unknown document fields {sorted(unknown)}")
149
+ wrapped, namespace = data.get("wrapped", True), data.get("namespace", LVDATA_NAMESPACE)
150
+ if not isinstance(wrapped, bool) or not (namespace is None or isinstance(namespace, str)):
151
+ raise LVXMLError("'wrapped' must be a bool and 'namespace' a string or null")
152
+ root = "LVData" if wrapped else ""
153
+ nodes = tuple(node_from_dict(item, root) for item in data["items"])
154
+ for node in nodes:
155
+ if node.tag == "Version" and wrapped and node.name == "" and node.val is not None:
156
+ if node.items or node.choices or node.dims or node.extra or node.num_elts is not None:
157
+ raise LVXMLError("a Version entry only takes 'val'", root)
158
+ continue
159
+ check_shape(node, root)
160
+ if not wrapped and len(nodes) != 1:
161
+ raise LVXMLError("a bare document holds exactly one value")
162
+ return Document(nodes, wrapped=wrapped, namespace=namespace)
lvxml/py.typed ADDED
File without changes
@@ -0,0 +1,254 @@
1
+ Metadata-Version: 2.5
2
+ Name: lvxml
3
+ Version: 0.2.1
4
+ Summary: Codec for LabVIEW Flatten To XML / Unflatten From XML between its fixed schema and native Python values
5
+ Project-URL: Homepage, https://lianes8studio.com
6
+ Project-URL: Repository, https://github.com/JaviLianes8/lvxml
7
+ Project-URL: Issues, https://github.com/JaviLianes8/lvxml/issues
8
+ Project-URL: Changelog, https://github.com/JaviLianes8/lvxml/blob/main/CHANGELOG.md
9
+ Project-URL: LinkedIn, https://www.linkedin.com/in/jlianes/
10
+ Project-URL: Funding, https://buymeacoffee.com/jlianesglrs
11
+ Author-email: Javier Lianes <javier@lianes8studio.com>
12
+ License-Expression: MIT
13
+ License-File: LICENSE
14
+ Keywords: codec,flatten,labview,ni,unflatten,xml
15
+ Classifier: Development Status :: 4 - Beta
16
+ Classifier: Environment :: Console
17
+ Classifier: Intended Audience :: Developers
18
+ Classifier: Intended Audience :: Science/Research
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Topic :: Scientific/Engineering
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.10
23
+ Provides-Extra: dev
24
+ Requires-Dist: build; extra == 'dev'
25
+ Requires-Dist: editables; extra == 'dev'
26
+ Requires-Dist: hatchling; extra == 'dev'
27
+ Requires-Dist: lxml; extra == 'dev'
28
+ Requires-Dist: mypy; extra == 'dev'
29
+ Requires-Dist: pytest; extra == 'dev'
30
+ Requires-Dist: ruff; extra == 'dev'
31
+ Requires-Dist: twine; extra == 'dev'
32
+ Description-Content-Type: text/markdown
33
+
34
+ # lvxml
35
+
36
+ Codec for LabVIEW Flatten To XML / Unflatten From XML between its fixed schema and native
37
+ Python values. Typed, zero dependencies, Python 3.10+.
38
+
39
+ ## Install
40
+
41
+ ```
42
+ pip install lvxml
43
+ ```
44
+
45
+ ## Read LabVIEW XML as Python values
46
+
47
+ LabVIEW's Flatten To XML emits a fixed shape: each value is an element named after its
48
+ datatype (`DBL`, `I32`, `Boolean`, `String`, `Array`, `Cluster`, ...), carrying a `<Name>`
49
+ and either a `<Val>` or, for containers, `<Dimsize>` or `<NumElts>` and child elements.
50
+
51
+ ```python
52
+ from lvxml import from_lv_xml, from_lv_xml_all
53
+
54
+ from_lv_xml("<I32><Name>count</Name><Val>-7</Val></I32>")
55
+ # -7
56
+
57
+ from_lv_xml(
58
+ "<Cluster><Name>pt</Name><NumElts>2</NumElts>"
59
+ "<DBL><Name>x</Name><Val>1.0</Val></DBL>"
60
+ "<I32><Name>y</Name><Val>5</Val></I32></Cluster>"
61
+ )
62
+ # {'x': 1.0, 'y': 5}
63
+
64
+ with open("Example Flattened XML File.xml", encoding="utf-8") as f:
65
+ from_lv_xml_all(f.read()) # every value in an <LVData> file, Version header skipped
66
+ ```
67
+
68
+ | LabVIEW element | Python value |
69
+ | --- | --- |
70
+ | `I8` `I16` `I32` `I64` `U8` `U16` `U32` `U64` | `int`, range checked |
71
+ | `SGL` `DBL` `EXT` | `float` (`NaN`, `Inf`, `-Inf` accepted) |
72
+ | `CSG` `CDB` `CXT` | `complex`, from `re +im i` text |
73
+ | `Boolean` | `bool` (`1`/`0`, also `true`/`false`) |
74
+ | `String` `Path` `DAQChannel` | `str` |
75
+ | `EB` `EW` `EL` enums | the selected `<Choice>` label |
76
+ | `Refnum` | its `<Val>` text |
77
+ | `Array` | `list`, nested once per `<Dimsize>`, row-major |
78
+ | `Cluster` | `dict` keyed by field name, in order |
79
+ | `LvVariant`, `Timestamp`, `WDT`, `DynamicData`, `DigitalData`, waveforms | `list` of their items' values |
80
+
81
+ A cluster whose fields share a name (LabVIEW allows unnamed fields) cannot become a dict
82
+ without losing data, so it raises; read it through the typed tree below instead.
83
+
84
+ ## Write Python values as LabVIEW XML
85
+
86
+ A schema names the LabVIEW type of each value, since a Python `int` could be any width
87
+ and a `list` any element type:
88
+
89
+ ```python
90
+ from lvxml import Array, Cluster, LVEnum, to_lv_xml
91
+
92
+ to_lv_xml([1.0, 2.5], Array("DBL"), name="xs")
93
+ to_lv_xml([[1, 2], [3, 4]], Array("I32", ndim=2), name="grid")
94
+ to_lv_xml(
95
+ {"x": 1.5, "flag": True, "mode": "Run", "z": 1 - 2j},
96
+ Cluster({"x": "DBL", "flag": "Boolean", "mode": LVEnum(("Idle", "Run")), "z": "CDB"}),
97
+ name="pt",
98
+ )
99
+ ```
100
+
101
+ A scalar schema is a tag string: any integer, float or complex tag above, `"Boolean"`,
102
+ `"String"`, `"Path"` or `"DAQChannel"`. `Array(element, ndim=1)` is an array (LabVIEW has
103
+ no array of arrays, so `element` cannot be an `Array`), `Cluster(fields)` an ordered
104
+ mapping of field name to schema, and `LVEnum(choices, tag="EW")` an enum whose value may
105
+ be given as a label or an index. An empty array is written with `Dimsize` 0 and one
106
+ default element, so the element type stays visible. `to_node` builds the typed tree
107
+ instead of text.
108
+
109
+ ## The typed tree: lossless read and write
110
+
111
+ `parse` keeps everything: names, the raw `<Val>` text, enum choices, `Dimsize`,
112
+ `NumElts`, refnum fields and the `<Version>` header. `dump` writes it back in the layout
113
+ LabVIEW's file writer uses (one element per line, CRLF, no indentation), so a file
114
+ LabVIEW wrote comes back byte for byte.
115
+
116
+ ```python
117
+ from lvxml import dump, parse, to_value
118
+
119
+ with open("Example Flattened XML File.xml", encoding="utf-8", newline="") as f:
120
+ text = f.read() # newline="" keeps LabVIEW's CRLF line breaks
121
+
122
+ document = parse(text)
123
+ document.version # '26.3.1f1'
124
+ node = document.values[3] # Node(tag='Cluster', name='Random Graph Data', num_elts=3, ...)
125
+ to_value(node) # the plain Python value
126
+ assert dump(document) == text
127
+ document.to_dict() # JSON-ready; Document.from_dict() reverses it
128
+ ```
129
+
130
+ `dump(tree, newline="\r\n", indent="", declaration=None)` also writes a single `Node`
131
+ bare, the way Flatten To XML returns it, and refuses a node the schema cannot express.
132
+
133
+ ## Validate
134
+
135
+ `validate(text)` returns a list of problems, empty when the document is good. It first
136
+ checks the element structure `LVXMLSchema.xsd` defines, then every value: numbers that
137
+ parse and fit their type, enum indexes inside their choices, and `NumElts` and `Dimsize`
138
+ that match the elements present. Each problem names its location, such as
139
+ `Cluster[pt]/I8[y]: 300 is out of range for I8 (-128..127)`. Problems in the data raise
140
+ `LVXMLError` (a `ValueError`) with the same location in its `path` attribute. Building an
141
+ impossible schema object, such as `LVEnum(())`, raises `ValueError` or `TypeError`.
142
+
143
+ `parse`, `from_lv_xml`, `from_lv_xml_all` and `validate` take `str` or `bytes`; bytes are
144
+ decoded by the encoding the XML declaration names, or as UTF-8.
145
+
146
+ ## Command line
147
+
148
+ ```
149
+ lvxml to-json data.xml -o data.json # lossless tree, every name and raw value kept
150
+ lvxml to-json --values data.xml # plain values: numbers, strings, lists, dicts
151
+ lvxml to-xml data.json -o data.xml # tree JSON back to LabVIEW XML
152
+ lvxml validate data.xml other.xml # check shape and values, exit 1 on any problem
153
+ echo "<I32><Name>n</Name><Val>7</Val></I32>" | lvxml to-json --values -
154
+ ```
155
+
156
+ `-` reads standard input. `--encoding` sets the file encoding (default `utf-8`); if a
157
+ file is not UTF-8, name its encoding, for example `--encoding cp1252`.
158
+ `to-xml --newline lf` switches from CRLF. `--values` writes strict JSON: complex numbers
159
+ become `{"re": ..., "im": ...}` and non-finite floats the strings `"NaN"`, `"Inf"` and
160
+ `"-Inf"`. Errors go to standard error with exit status 1. `lvxml --help` lists
161
+ everything.
162
+
163
+ ## What each rule rests on
164
+
165
+ Checked against the real LabVIEW file in the tests:
166
+
167
+ - The layout `dump` writes by default: the `<?xml version='1.0' standalone='yes' ?>`
168
+ declaration, `<LVData xmlns="http://www.ni.com/LVData">`, a `<Version>` header, one
169
+ element per line, CRLF, no indentation and no final line break.
170
+ - `DBL`, `Cluster` and one-dimensional `Array` values, and decimals written with a period.
171
+
172
+ Checked against the shipped XSD: the element structure of every type, including
173
+ multi-dimensional `Array` (repeated `<Dimsize>`), the enum `<Choice>` list and the
174
+ optional `Refnum` fields. The XSD declares the target namespace
175
+ `http://www.ni.com/labview` while the real file uses `http://www.ni.com/LVData`; the
176
+ reader accepts either, or none, keeps whichever it found, and requires every element to
177
+ share the root's namespace.
178
+
179
+ Taken from the format strings in LabVIEW 2026's run-time library (`lvrt.dll`), with no
180
+ real sample in the tests yet:
181
+
182
+ - complex values as `re +im i` (the writer's `<Val>%s +%s i</Val>`);
183
+ - only `&`, `<` and `>` escaped in text (the only entity strings beside the XML
184
+ writer's tags are `&lt;`, `&gt;` and `&amp;`), so quotes are written as they are;
185
+ - integers and `Dimsize` as plain decimals (`%d`, `%lld`, `%llu`).
186
+
187
+ Choices `lvxml` makes where neither source settles the question:
188
+
189
+ - `Boolean` is written as `1` or `0`; the reader also accepts `true` and `false`.
190
+ - Non-finite floats are written as `NaN`, `Inf` and `-Inf` (the spellings found in the
191
+ run-time library) and read case-insensitively.
192
+ - Floats are written with the shortest text that reads back exactly. A comma decimal
193
+ separator raises.
194
+ - A carriage return inside a value is written as is and survives a read with `lvxml`,
195
+ where a plain XML parser would turn it into a line feed.
196
+ - A DOCTYPE or a CDATA section is refused, nesting deeper than 100 levels raises, and an
197
+ empty array whose shape would expand to more than 100000 nested lists raises.
198
+
199
+ ## Test fixtures
200
+
201
+ The tests check the codec against ground truth from a LabVIEW 2026 installation:
202
+
203
+ - `LVXMLSchema.xsd` is the schema LabVIEW ships in
204
+ `C:\Program Files\National Instruments\LabVIEW 2026\vi.lib\Utility\LVXMLSchema.xsd`.
205
+ It belongs to NI, so it is not in the repository or the sdist: run
206
+ `python scripts/capture_ni.py` on a machine with LabVIEW to copy it into
207
+ `tests/fixtures/ni` (size and sha256 are checked against `tests/fixtures/ni/MANIFEST.json`).
208
+ Without it, the tests that validate against the schema skip.
209
+ - `tests/fixtures/real/example-flattened-xml-file.xml` is **real LabVIEW output**,
210
+ kept byte for byte: the file `Example Flattened XML File.xml` that LabVIEW 2026
211
+ (version header `26.3.1f1`) wrote to `Documents\LabVIEW Data` when running the shipped
212
+ example `examples\File IO\XML\Flatten and Unflatten XML\Flatten Data and Write to XML
213
+ File.vi`. It holds `DBL` values and a `Cluster` with a ten-element `Array`.
214
+ - `tests/fixtures/schema/*.xml` are **schema-derived**: written by hand from the XSD to
215
+ cover every element it defines. They are valid against the XSD, but their payloads are
216
+ illustrative; in particular the cluster contents of the timestamp and waveform elements
217
+ are not LabVIEW's actual internal layout, which the schema does not specify.
218
+
219
+ The real file and every schema-derived fixture are validated against the XSD with `lxml`
220
+ (a test-only dependency) when the schema is present, parsed, decoded, and written back: the real file byte for byte,
221
+ the others canonically equal. Samples that the XSD rejects are rejected by the reader as
222
+ well. An end-to-end test installs the package and drives the installed `lvxml` command
223
+ on the real file.
224
+
225
+ ## Not covered
226
+
227
+ Fixed-point `FXP` numbers and the `<Default>` and `<Attributes>` children of a variant
228
+ appear among the run-time library's writer strings but are absent from the shipped
229
+ schema, so the reader raises a clear error for them. The
230
+ timestamp and waveform elements are read as the schema describes them (a name and
231
+ clusters), not interpreted further.
232
+
233
+ ## Why
234
+
235
+ LabVIEW's Flatten To XML and Unflatten From XML use one fixed schema
236
+ (`LVXMLSchema.xsd`): datatype-named elements, `Name`-ordered fields, a period decimal
237
+ separator independent of locale. LabVIEW produces and reads this schema natively, and NI's
238
+ community GXML library also converts data to and from XML, but both run inside LabVIEW and
239
+ ship no Python reader. On the Python side, with no LabVIEW running, a generic XML library
240
+ parses the tree but knows nothing of that convention, so turning it into usable Python, or
241
+ producing XML LabVIEW will accept, is left to hand-written glue every time. Existing LabVIEW
242
+ Python packages (pylabview, lvkit, autolv) parse VI source files, not this flattened data
243
+ format, and no package on PyPI covers it. `lvxml` is a small, typed, zero-dependency codec
244
+ for it.
245
+
246
+ ## License
247
+
248
+ MIT. See [LICENSE](LICENSE).
249
+
250
+ ## Links
251
+
252
+ - Website: https://lianes8studio.com
253
+ - GitHub: https://github.com/JaviLianes8
254
+ - LinkedIn: https://www.linkedin.com/in/jlianes/
@@ -0,0 +1,20 @@
1
+ lvxml/__init__.py,sha256=9kdAkJ6PDUtLxACYM6xvgNeEg7CsFU7wQ35rw2Kwtp8,711
2
+ lvxml/__main__.py,sha256=NLpeMnqLH4XzLgQ3fPCHFeDMnSZeEZnsayREEBSUfqQ,166
3
+ lvxml/_checks.py,sha256=cp3PhaqK9x_vOAKu7aWCUXyWPLgwkbkjiEczmnuOqkA,4014
4
+ lvxml/_cli.py,sha256=VrEqbFSFPrRFSMyccdn9nq3-dTRj4xsgL57w4Mra-T0,6204
5
+ lvxml/_decode.py,sha256=HQldZuAHgIVIT8tG4cnXdCEJbzGofOdl-gLwtw8eGHs,3898
6
+ lvxml/_dump.py,sha256=dDcpXdFYqN9Z0fo34Ruw52Giq3Uti2e07kmohF9cVUk,4178
7
+ lvxml/_encode.py,sha256=fN2QyGoTYZObabLQ8YNuDs7DYdgJduO2HEwbWzVpmhE,6358
8
+ lvxml/_errors.py,sha256=FLku5mi8pmmQ4u705R5VbBYGEvrb6m-0Fz4jz-4NENI,565
9
+ lvxml/_load.py,sha256=AxyAfEj_e1UKta1EIUhHqj8py-zr56LnYW2ESxi4sPo,2256
10
+ lvxml/_node.py,sha256=9_lloysh7j3Vly_QT9ataUbblh8Zn_PXoO375kH2FIk,3037
11
+ lvxml/_numbers.py,sha256=l2hfDFVh3lcO5IEPePst9azyN_OaVU52iRXvB3sLnQQ,3133
12
+ lvxml/_parse.py,sha256=Xh7bkDRPx11DFPdQ6gK1vz9sm6g3z9jNgQYvDxiJ-9M,6310
13
+ lvxml/_schema.py,sha256=GAHQwDeeE0Z82DhOF3a2aiRcquzfaR4HynViwp9D-L8,4129
14
+ lvxml/_treejson.py,sha256=9QcFfoV8d8pyfvPHDT4qklOfo7BqTNp7m2Jsd_NCDEs,7263
15
+ lvxml/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ lvxml-0.2.1.dist-info/METADATA,sha256=RdYIFeAyuSpwVIEnR2bq6goaIVF7WExS0uVboaMpRl0,11860
17
+ lvxml-0.2.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
18
+ lvxml-0.2.1.dist-info/entry_points.txt,sha256=v-2V1KUgAKDBVy7sm8fVfO0wkujOfr1KbDcX72oJd4Q,42
19
+ lvxml-0.2.1.dist-info/licenses/LICENSE,sha256=wirD5nCA2aQa5UusK_1YHLBogI3CGcw23xAnulp3L_U,1091
20
+ lvxml-0.2.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ lvxml = lvxml._cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Javier Lianes
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.