py61850 0.3.0.dev2__tar.gz → 0.4.0__tar.gz
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.
- py61850-0.4.0/.gitignore +45 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/PKG-INFO +23 -2
- {py61850-0.3.0.dev2 → py61850-0.4.0}/README.md +22 -1
- {py61850-0.3.0.dev2 → py61850-0.4.0}/ROADMAP.md +36 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/pyproject.toml +6 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/__init__.py +1 -1
- {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/core/__init__.py +7 -2
- {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/scl/__init__.py +12 -0
- py61850-0.4.0/src/py61850/scl/document.py +870 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/BENCH.md +4 -4
- py61850-0.4.0/tests/fixtures/scl/README.md +82 -0
- py61850-0.4.0/tests/unit/roundtrip.py +382 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/unit/scl_fixtures.py +11 -0
- py61850-0.4.0/tests/unit/test_fixture_hygiene.py +108 -0
- py61850-0.4.0/tests/unit/test_scl_comments.py +332 -0
- py61850-0.4.0/tests/unit/test_scl_namespaces.py +346 -0
- py61850-0.4.0/tests/unit/test_scl_roundtrip.py +200 -0
- py61850-0.4.0/tests/unit/test_scl_write.py +303 -0
- py61850-0.3.0.dev2/.gitignore +0 -24
- py61850-0.3.0.dev2/src/py61850/scl/document.py +0 -326
- {py61850-0.3.0.dev2 → py61850-0.4.0}/CLA.md +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/COMMERCIAL.md +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/CONTRIBUTING.md +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/LICENSE +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/examples/01_connect_and_scan.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/examples/02_read_values.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/examples/03_file_transfer.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/examples/04_error_handling.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/examples/05_fleet_inventory.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/examples/06_find_files.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/examples/07_read_object_references.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/examples/README.md +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/__main__.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/cli/__init__.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/cli/__main__.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/cli/files.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/cli/main.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/cli/scan.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/core/ber.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/core/data.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/core/fc.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/core/quality.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/core/refs.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/core/time.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/errors.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/goose/__init__.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/link/__init__.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/mms/__init__.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/mms/client.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/mms/pdu.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/mms/service_error.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/mms/services/__init__.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/mms/services/directory.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/mms/services/files.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/mms/services/read.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/mms/types.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/osi/__init__.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/osi/acse.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/osi/cotp.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/osi/oids.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/osi/presentation.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/osi/session.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/osi/stack.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/osi/tpkt.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/py.typed +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/scl/_xmlsafe.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/scl/communication.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/scl/controls.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/scl/model.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/scl/templates.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/sv/__init__.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/README.md +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/__init__.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/fixtures/README.md +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/unit/__init__.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/unit/test_ber.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/unit/test_client.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/unit/test_data.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/unit/test_fc.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/unit/test_file_filter.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/unit/test_logical_nodes.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/unit/test_osi.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/unit/test_pdu.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/unit/test_public_api.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/unit/test_refs.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/unit/test_scl_api.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/unit/test_scl_communication.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/unit/test_scl_controls.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/unit/test_scl_document.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/unit/test_scl_model.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/unit/test_scl_templates.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/unit/test_types.py +0 -0
- {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/unit/test_xmlsafe.py +0 -0
py61850-0.4.0/.gitignore
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.pyc
|
|
3
|
+
*.egg-info/
|
|
4
|
+
build/
|
|
5
|
+
dist/
|
|
6
|
+
.venv/
|
|
7
|
+
venv/
|
|
8
|
+
.pytest_cache/
|
|
9
|
+
downloads/
|
|
10
|
+
relay_files/
|
|
11
|
+
inventory.csv
|
|
12
|
+
|
|
13
|
+
# SCL / IED capability files (vendor-supplied, large, not redistributable)
|
|
14
|
+
*.icd
|
|
15
|
+
*.ICD
|
|
16
|
+
*.cid
|
|
17
|
+
*.CID
|
|
18
|
+
*.scd
|
|
19
|
+
*.SCD
|
|
20
|
+
*.iid
|
|
21
|
+
*.IID
|
|
22
|
+
*.sed
|
|
23
|
+
*.SED
|
|
24
|
+
fixtures/ICD/
|
|
25
|
+
|
|
26
|
+
# The round-trip corpus is tracked, by name and never by wildcard.
|
|
27
|
+
#
|
|
28
|
+
# The `*.scd` rule above is right for every SCD except these four: a vendor
|
|
29
|
+
# file dropped in this tree is somebody's substation and must not be
|
|
30
|
+
# committed by accident. The three real ones below were anonymised by
|
|
31
|
+
# `tools/anonymise_scd.py` before they were added -- substation, utility,
|
|
32
|
+
# Windows accounts, machine names and addressing all substituted, IED names
|
|
33
|
+
# deliberately kept. The fourth is hand-written and carries no identity.
|
|
34
|
+
#
|
|
35
|
+
# Named one by one on purpose. A negation like `!tests/fixtures/scl/*.scd`
|
|
36
|
+
# would re-include ANY .scd dropped in that directory, which is exactly how
|
|
37
|
+
# an un-anonymised file reached a sibling repository once.
|
|
38
|
+
!tests/fixtures/scl/sel.scd
|
|
39
|
+
!tests/fixtures/scl/mixed.scd
|
|
40
|
+
!tests/fixtures/scl/siemens.scd
|
|
41
|
+
!tests/fixtures/scl/namespaces.scd
|
|
42
|
+
|
|
43
|
+
# The substitution mapping names what was substituted -- the substation, the
|
|
44
|
+
# utility, the engineers' accounts. Tracking it would undo the anonymisation.
|
|
45
|
+
tools/*.local.json
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: py61850
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Pure-Python IEC 61850 toolkit — MMS (ISO 9506) client today; GOOSE and SV planned. Standard library only.
|
|
5
5
|
Project-URL: Homepage, https://github.com/GuilhermeMarini/py61850
|
|
6
6
|
Project-URL: Repository, https://github.com/GuilhermeMarini/py61850
|
|
@@ -135,7 +135,7 @@ with FileTransfer("192.0.2.22") as ft:
|
|
|
135
135
|
progress=lambda got, total: print(f"{got}/{total}"))
|
|
136
136
|
```
|
|
137
137
|
|
|
138
|
-
### Reading an SCL file
|
|
138
|
+
### Reading an SCL file — and writing it back unchanged
|
|
139
139
|
|
|
140
140
|
```python
|
|
141
141
|
from py61850.scl import SclDocument
|
|
@@ -153,6 +153,27 @@ for ln in ied.logical_nodes():
|
|
|
153
153
|
print(attr.reference(), attr.mms_item(), attr.fc, attr.btype)
|
|
154
154
|
```
|
|
155
155
|
|
|
156
|
+
A parsed document goes back out as the file it came from:
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
doc.write("station.scd") # atomic: temp file beside it, then os.replace
|
|
160
|
+
raw = doc.to_bytes() # or the bytes, to hand somewhere else
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**Byte for byte**, with no edit applied — comments, indentation, attribute
|
|
164
|
+
order, namespace prefixes, `xmlns` declarations nothing uses, the line ending
|
|
165
|
+
the file was written with and the XML declaration as it was spelled all
|
|
166
|
+
survive. Five differences are permitted, none of them observable through an
|
|
167
|
+
XML parser and every one a limit of the standard library's serialiser:
|
|
168
|
+
attribute quote style, empty-element spacing and form, CDATA boundaries, and
|
|
169
|
+
the spelling of a numeric character reference. `SclDocument.to_bytes` states
|
|
170
|
+
all five; the round-trip test holds them against 44 MB of real station
|
|
171
|
+
exports from three vendors.
|
|
172
|
+
|
|
173
|
+
That guarantee is the point of the write side. The file goes back into DIGSI
|
|
174
|
+
and SEL Architect, and a library that reformats the 99 % of an SCD it did not
|
|
175
|
+
touch turns every save into a whole-file diff.
|
|
176
|
+
|
|
156
177
|
`py61850.scl` is imported explicitly and is never pulled in by `import
|
|
157
178
|
py61850`, so the MMS client keeps installing and running unprivileged.
|
|
158
179
|
|
|
@@ -108,7 +108,7 @@ with FileTransfer("192.0.2.22") as ft:
|
|
|
108
108
|
progress=lambda got, total: print(f"{got}/{total}"))
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
-
### Reading an SCL file
|
|
111
|
+
### Reading an SCL file — and writing it back unchanged
|
|
112
112
|
|
|
113
113
|
```python
|
|
114
114
|
from py61850.scl import SclDocument
|
|
@@ -126,6 +126,27 @@ for ln in ied.logical_nodes():
|
|
|
126
126
|
print(attr.reference(), attr.mms_item(), attr.fc, attr.btype)
|
|
127
127
|
```
|
|
128
128
|
|
|
129
|
+
A parsed document goes back out as the file it came from:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
doc.write("station.scd") # atomic: temp file beside it, then os.replace
|
|
133
|
+
raw = doc.to_bytes() # or the bytes, to hand somewhere else
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Byte for byte**, with no edit applied — comments, indentation, attribute
|
|
137
|
+
order, namespace prefixes, `xmlns` declarations nothing uses, the line ending
|
|
138
|
+
the file was written with and the XML declaration as it was spelled all
|
|
139
|
+
survive. Five differences are permitted, none of them observable through an
|
|
140
|
+
XML parser and every one a limit of the standard library's serialiser:
|
|
141
|
+
attribute quote style, empty-element spacing and form, CDATA boundaries, and
|
|
142
|
+
the spelling of a numeric character reference. `SclDocument.to_bytes` states
|
|
143
|
+
all five; the round-trip test holds them against 44 MB of real station
|
|
144
|
+
exports from three vendors.
|
|
145
|
+
|
|
146
|
+
That guarantee is the point of the write side. The file goes back into DIGSI
|
|
147
|
+
and SEL Architect, and a library that reformats the 99 % of an SCD it did not
|
|
148
|
+
touch turns every save into a whole-file diff.
|
|
149
|
+
|
|
129
150
|
`py61850.scl` is imported explicitly and is never pulled in by `import
|
|
130
151
|
py61850`, so the MMS client keeps installing and running unprivileged.
|
|
131
152
|
|
|
@@ -111,11 +111,36 @@ Make the client dependable enough for unattended fleet jobs to build on.
|
|
|
111
111
|
documented read-preference ranking. In `core` rather than `scl` because
|
|
112
112
|
a client matching items against `GetLogicalDeviceDirectory` needs the
|
|
113
113
|
same vocabulary as a reader walking a file.
|
|
114
|
+
- ✅ `core/fc.py` also carries the control model's own data attributes
|
|
115
|
+
(`Oper`, `SBOw`, `SBO`, `Cancel`) and `is_control_attribute`, which asks
|
|
116
|
+
"command or reading?" of an attribute NAME. Both directions are needed:
|
|
117
|
+
an attribute path is often in hand when its FC is not.
|
|
114
118
|
- ✅ `core/refs.py` — object reference ↔ MMS domain and item name (61850-8-1).
|
|
115
119
|
- ✅ `scl/` — see 1.0 below.
|
|
116
120
|
|
|
117
121
|
---
|
|
118
122
|
|
|
123
|
+
## 0.4 — The SCL round trip ✅
|
|
124
|
+
|
|
125
|
+
Turns a read-only reader into a read-write one. The model is unchanged; what
|
|
126
|
+
is new is that a document can go back out as the file it came from.
|
|
127
|
+
|
|
128
|
+
- ✅ **Comment-preserving parse** — `insert_comments` on the tree builder, and
|
|
129
|
+
every walk in the package tolerates a node whose `tag` is a factory
|
|
130
|
+
rather than a name. An engineer's note beside a setting is content.
|
|
131
|
+
- ✅ **Namespace fidelity** — prefixes are the document's own, and a
|
|
132
|
+
declaration the file carries but nothing uses is re-emitted. `sxy:`
|
|
133
|
+
coordinates are read by DIGSI and SEL Architect to draw a single-line
|
|
134
|
+
diagram; losing that declaration would damage one silently.
|
|
135
|
+
- ✅ **`SclDocument.to_bytes` / `.write`** — the public write API, atomic on a
|
|
136
|
+
real path, with the fidelity guarantee and its five cosmetic exceptions
|
|
137
|
+
stated where a consumer reads them.
|
|
138
|
+
- ✅ **The round-trip test** — four station exports, 44 MB, parsed and written
|
|
139
|
+
with no edit and compared at the level of bytes. The comparison names
|
|
140
|
+
what differed rather than that something did.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
119
144
|
## 1.0 — MMS simulation 🧭
|
|
120
145
|
|
|
121
146
|
**Goal:** read an SCL file (`.scd` / `.cid` / `.icd`) and stand up a virtual IED
|
|
@@ -137,6 +162,17 @@ image of today's client.
|
|
|
137
162
|
|
|
138
163
|
Not modelled, because no file in the reference corpus carries them: the
|
|
139
164
|
`Substation` section and `Log`.
|
|
165
|
+
|
|
166
|
+
**The vendor seam is proven by two unrelated vendors**, which is what
|
|
167
|
+
0.3.0 waited for rather than shipping on its author's word. Two
|
|
168
|
+
libraries outside this project were rebuilt on the model, sharing no
|
|
169
|
+
code with each other and neither written against it: one reads SEL's
|
|
170
|
+
`sAddr` value grammar (178,406 configured attributes in one station),
|
|
171
|
+
the other Siemens' `Private` elements (5,886 of a single type). Both
|
|
172
|
+
reproduce every field their own XML readers produced, on the same
|
|
173
|
+
files — one of which is a Siemens export containing SEL relays, read by
|
|
174
|
+
both without either opening it twice. Nothing was added to this package
|
|
175
|
+
for the second one.
|
|
140
176
|
- 🧭 **Model → MMS server** — a listening `MmsServer` on TCP 102 that answers the
|
|
141
177
|
confirmed services the client already speaks, driven by the SCL model:
|
|
142
178
|
- Initiate / association (server side of `associate.py`)
|
|
@@ -52,3 +52,9 @@ packages = ["src/py61850"]
|
|
|
52
52
|
[tool.hatch.build.targets.sdist]
|
|
53
53
|
include = ["src", "tests", "examples", "README.md", "ROADMAP.md", "LICENSE",
|
|
54
54
|
"COMMERCIAL.md", "CONTRIBUTING.md", "CLA.md"]
|
|
55
|
+
# The SCL round-trip corpus is 44 MB of real vendor SCDs -- a development
|
|
56
|
+
# artefact, not something an installer needs. Shipping it would grow the sdist
|
|
57
|
+
# of a zero-dependency standard-library package by ~2.3 MB compressed and buy
|
|
58
|
+
# whoever downloads it nothing. `tests/fixtures/scl/README.md` says how to
|
|
59
|
+
# obtain the corpus; the tests that need it skip when it is absent.
|
|
60
|
+
exclude = ["tests/fixtures/scl/*.scd"]
|
|
@@ -86,7 +86,7 @@ from .core.fc import CONTROL_DATA_ATTRIBUTES, FUNCTIONAL_CONSTRAINTS, \
|
|
|
86
86
|
read_rank as fc_read_rank
|
|
87
87
|
from .core.refs import da_parts, mms_item, object_reference, split_item
|
|
88
88
|
|
|
89
|
-
__version__ = "0.
|
|
89
|
+
__version__ = "0.4.0"
|
|
90
90
|
|
|
91
91
|
__all__ = [
|
|
92
92
|
"MmsClient",
|
|
@@ -20,6 +20,11 @@ subscriber and a publisher (GOOSE/SV), and be unit-tested offline.
|
|
|
20
20
|
quality the IEC 61850 13-bit Quality bitstring
|
|
21
21
|
time MMS ``UtcTime`` / ``BinaryTime``
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
Most of this is internal and may change between releases. The exception is
|
|
24
|
+
what the top level re-exports -- ``FUNCTIONAL_CONSTRAINTS``,
|
|
25
|
+
``CONTROL_DATA_ATTRIBUTES``, ``fc_is_control``, ``fc_is_control_attribute``,
|
|
26
|
+
``fc_read_rank``, ``mms_item``, ``object_reference``, ``split_item`` and
|
|
27
|
+
``da_parts``. Those are public API and are reached as ``py61850.<name>``; the
|
|
28
|
+
rest is reached as ``py61850.core.<module>.<name>`` and carries no such
|
|
29
|
+
promise.
|
|
25
30
|
"""
|
|
@@ -16,6 +16,18 @@
|
|
|
16
16
|
for attr in ln.walk():
|
|
17
17
|
print(attr.reference(), attr.mms_item(), attr.btype)
|
|
18
18
|
|
|
19
|
+
doc.write("station.scd") # atomically, and byte for byte
|
|
20
|
+
|
|
21
|
+
**Reading is not the whole of it: a document written back out is the file it
|
|
22
|
+
came from.** ``SclDocument.to_bytes`` and ``SclDocument.write`` hold a
|
|
23
|
+
fidelity guarantee -- comments, indentation, attribute order, namespace
|
|
24
|
+
prefixes, declarations nothing uses, the line ending and the XML declaration
|
|
25
|
+
all survive a parse and a serialise, with five exceptions no XML parser can
|
|
26
|
+
observe. :meth:`SclDocument.to_bytes` names all five. It matters because the
|
|
27
|
+
file goes back to DIGSI and to SEL Architect: a library that reformats the
|
|
28
|
+
99 % of a station export it did not touch turns every save into a whole-file
|
|
29
|
+
diff.
|
|
30
|
+
|
|
19
31
|
**What this package is for.** It is a general IEC 61850-6 implementation,
|
|
20
32
|
judged against what any SCL tool would need -- IEDScout, IEC Browser, OpenSCD
|
|
21
33
|
-- not against what one consumer happens to extract today. If a vendor-neutral
|