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.
Files changed (93) hide show
  1. py61850-0.4.0/.gitignore +45 -0
  2. {py61850-0.3.0.dev2 → py61850-0.4.0}/PKG-INFO +23 -2
  3. {py61850-0.3.0.dev2 → py61850-0.4.0}/README.md +22 -1
  4. {py61850-0.3.0.dev2 → py61850-0.4.0}/ROADMAP.md +36 -0
  5. {py61850-0.3.0.dev2 → py61850-0.4.0}/pyproject.toml +6 -0
  6. {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/__init__.py +1 -1
  7. {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/core/__init__.py +7 -2
  8. {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/scl/__init__.py +12 -0
  9. py61850-0.4.0/src/py61850/scl/document.py +870 -0
  10. {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/BENCH.md +4 -4
  11. py61850-0.4.0/tests/fixtures/scl/README.md +82 -0
  12. py61850-0.4.0/tests/unit/roundtrip.py +382 -0
  13. {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/unit/scl_fixtures.py +11 -0
  14. py61850-0.4.0/tests/unit/test_fixture_hygiene.py +108 -0
  15. py61850-0.4.0/tests/unit/test_scl_comments.py +332 -0
  16. py61850-0.4.0/tests/unit/test_scl_namespaces.py +346 -0
  17. py61850-0.4.0/tests/unit/test_scl_roundtrip.py +200 -0
  18. py61850-0.4.0/tests/unit/test_scl_write.py +303 -0
  19. py61850-0.3.0.dev2/.gitignore +0 -24
  20. py61850-0.3.0.dev2/src/py61850/scl/document.py +0 -326
  21. {py61850-0.3.0.dev2 → py61850-0.4.0}/CLA.md +0 -0
  22. {py61850-0.3.0.dev2 → py61850-0.4.0}/COMMERCIAL.md +0 -0
  23. {py61850-0.3.0.dev2 → py61850-0.4.0}/CONTRIBUTING.md +0 -0
  24. {py61850-0.3.0.dev2 → py61850-0.4.0}/LICENSE +0 -0
  25. {py61850-0.3.0.dev2 → py61850-0.4.0}/examples/01_connect_and_scan.py +0 -0
  26. {py61850-0.3.0.dev2 → py61850-0.4.0}/examples/02_read_values.py +0 -0
  27. {py61850-0.3.0.dev2 → py61850-0.4.0}/examples/03_file_transfer.py +0 -0
  28. {py61850-0.3.0.dev2 → py61850-0.4.0}/examples/04_error_handling.py +0 -0
  29. {py61850-0.3.0.dev2 → py61850-0.4.0}/examples/05_fleet_inventory.py +0 -0
  30. {py61850-0.3.0.dev2 → py61850-0.4.0}/examples/06_find_files.py +0 -0
  31. {py61850-0.3.0.dev2 → py61850-0.4.0}/examples/07_read_object_references.py +0 -0
  32. {py61850-0.3.0.dev2 → py61850-0.4.0}/examples/README.md +0 -0
  33. {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/__main__.py +0 -0
  34. {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/cli/__init__.py +0 -0
  35. {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/cli/__main__.py +0 -0
  36. {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/cli/files.py +0 -0
  37. {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/cli/main.py +0 -0
  38. {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/cli/scan.py +0 -0
  39. {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/core/ber.py +0 -0
  40. {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/core/data.py +0 -0
  41. {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/core/fc.py +0 -0
  42. {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/core/quality.py +0 -0
  43. {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/core/refs.py +0 -0
  44. {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/core/time.py +0 -0
  45. {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/errors.py +0 -0
  46. {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/goose/__init__.py +0 -0
  47. {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/link/__init__.py +0 -0
  48. {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/mms/__init__.py +0 -0
  49. {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/mms/client.py +0 -0
  50. {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/mms/pdu.py +0 -0
  51. {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/mms/service_error.py +0 -0
  52. {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/mms/services/__init__.py +0 -0
  53. {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/mms/services/directory.py +0 -0
  54. {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/mms/services/files.py +0 -0
  55. {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/mms/services/read.py +0 -0
  56. {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/mms/types.py +0 -0
  57. {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/osi/__init__.py +0 -0
  58. {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/osi/acse.py +0 -0
  59. {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/osi/cotp.py +0 -0
  60. {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/osi/oids.py +0 -0
  61. {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/osi/presentation.py +0 -0
  62. {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/osi/session.py +0 -0
  63. {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/osi/stack.py +0 -0
  64. {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/osi/tpkt.py +0 -0
  65. {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/py.typed +0 -0
  66. {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/scl/_xmlsafe.py +0 -0
  67. {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/scl/communication.py +0 -0
  68. {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/scl/controls.py +0 -0
  69. {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/scl/model.py +0 -0
  70. {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/scl/templates.py +0 -0
  71. {py61850-0.3.0.dev2 → py61850-0.4.0}/src/py61850/sv/__init__.py +0 -0
  72. {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/README.md +0 -0
  73. {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/__init__.py +0 -0
  74. {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/fixtures/README.md +0 -0
  75. {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/unit/__init__.py +0 -0
  76. {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/unit/test_ber.py +0 -0
  77. {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/unit/test_client.py +0 -0
  78. {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/unit/test_data.py +0 -0
  79. {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/unit/test_fc.py +0 -0
  80. {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/unit/test_file_filter.py +0 -0
  81. {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/unit/test_logical_nodes.py +0 -0
  82. {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/unit/test_osi.py +0 -0
  83. {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/unit/test_pdu.py +0 -0
  84. {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/unit/test_public_api.py +0 -0
  85. {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/unit/test_refs.py +0 -0
  86. {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/unit/test_scl_api.py +0 -0
  87. {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/unit/test_scl_communication.py +0 -0
  88. {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/unit/test_scl_controls.py +0 -0
  89. {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/unit/test_scl_document.py +0 -0
  90. {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/unit/test_scl_model.py +0 -0
  91. {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/unit/test_scl_templates.py +0 -0
  92. {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/unit/test_types.py +0 -0
  93. {py61850-0.3.0.dev2 → py61850-0.4.0}/tests/unit/test_xmlsafe.py +0 -0
@@ -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.0.dev2
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.3.0.dev2"
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
- Import these from ``py61850.core.<module>``; they are internal to the package
24
- and may change between releases.
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