py61850 0.3.0.dev2__tar.gz → 0.5.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 (128) hide show
  1. py61850-0.5.0/.gitignore +60 -0
  2. py61850-0.5.0/CHANGELOG.md +224 -0
  3. py61850-0.5.0/NOTICE-IEC.txt +144 -0
  4. py61850-0.3.0.dev2/README.md → py61850-0.5.0/PKG-INFO +116 -6
  5. py61850-0.3.0.dev2/PKG-INFO → py61850-0.5.0/README.md +91 -33
  6. {py61850-0.3.0.dev2 → py61850-0.5.0}/ROADMAP.md +85 -2
  7. py61850-0.5.0/pyproject.toml +73 -0
  8. {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/__init__.py +1 -1
  9. {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/core/__init__.py +7 -2
  10. py61850-0.5.0/src/py61850/scl/__init__.py +311 -0
  11. py61850-0.5.0/src/py61850/scl/_content_models.py +361 -0
  12. py61850-0.5.0/src/py61850/scl/_nsd_types.py +2483 -0
  13. py61850-0.5.0/src/py61850/scl/address.py +689 -0
  14. py61850-0.5.0/src/py61850/scl/control_block.py +625 -0
  15. {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/scl/controls.py +19 -6
  16. py61850-0.5.0/src/py61850/scl/data_set.py +794 -0
  17. py61850-0.5.0/src/py61850/scl/data_types.py +882 -0
  18. py61850-0.5.0/src/py61850/scl/document.py +920 -0
  19. py61850-0.5.0/src/py61850/scl/edit.py +602 -0
  20. py61850-0.5.0/src/py61850/scl/extref.py +736 -0
  21. py61850-0.5.0/src/py61850/scl/generator.py +517 -0
  22. py61850-0.5.0/src/py61850/scl/ied.py +939 -0
  23. py61850-0.5.0/src/py61850/scl/ordering.py +177 -0
  24. py61850-0.5.0/src/py61850/scl/report_control.py +531 -0
  25. py61850-0.5.0/src/py61850/scl/sampled_value_control.py +354 -0
  26. py61850-0.5.0/src/py61850/scl/substation.py +1056 -0
  27. py61850-0.5.0/src/py61850/scl/supervision.py +1433 -0
  28. {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/BENCH.md +4 -4
  29. py61850-0.5.0/tests/fixtures/scl/README.md +82 -0
  30. py61850-0.5.0/tests/unit/editgen.py +791 -0
  31. py61850-0.5.0/tests/unit/roundtrip.py +382 -0
  32. py61850-0.5.0/tests/unit/scl_fixtures.py +717 -0
  33. py61850-0.5.0/tests/unit/test_fixture_hygiene.py +108 -0
  34. py61850-0.5.0/tests/unit/test_scl_address.py +1019 -0
  35. {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/unit/test_scl_api.py +44 -0
  36. py61850-0.5.0/tests/unit/test_scl_comments.py +332 -0
  37. py61850-0.5.0/tests/unit/test_scl_control_block.py +847 -0
  38. py61850-0.5.0/tests/unit/test_scl_data_set.py +1100 -0
  39. py61850-0.5.0/tests/unit/test_scl_data_types.py +774 -0
  40. py61850-0.5.0/tests/unit/test_scl_edit.py +601 -0
  41. py61850-0.5.0/tests/unit/test_scl_edit_invertibility.py +358 -0
  42. py61850-0.5.0/tests/unit/test_scl_extref.py +817 -0
  43. py61850-0.5.0/tests/unit/test_scl_generator.py +651 -0
  44. py61850-0.5.0/tests/unit/test_scl_ied.py +1712 -0
  45. py61850-0.5.0/tests/unit/test_scl_namespaces.py +346 -0
  46. py61850-0.5.0/tests/unit/test_scl_ordering.py +374 -0
  47. py61850-0.5.0/tests/unit/test_scl_report_control.py +659 -0
  48. py61850-0.5.0/tests/unit/test_scl_roundtrip.py +200 -0
  49. py61850-0.5.0/tests/unit/test_scl_sampled_value_control.py +488 -0
  50. py61850-0.5.0/tests/unit/test_scl_substation.py +1434 -0
  51. py61850-0.5.0/tests/unit/test_scl_supervision.py +1758 -0
  52. py61850-0.5.0/tests/unit/test_scl_write.py +303 -0
  53. py61850-0.3.0.dev2/.gitignore +0 -24
  54. py61850-0.3.0.dev2/pyproject.toml +0 -54
  55. py61850-0.3.0.dev2/src/py61850/scl/__init__.py +0 -98
  56. py61850-0.3.0.dev2/src/py61850/scl/document.py +0 -326
  57. py61850-0.3.0.dev2/tests/unit/scl_fixtures.py +0 -221
  58. {py61850-0.3.0.dev2 → py61850-0.5.0}/CLA.md +0 -0
  59. {py61850-0.3.0.dev2 → py61850-0.5.0}/COMMERCIAL.md +0 -0
  60. {py61850-0.3.0.dev2 → py61850-0.5.0}/CONTRIBUTING.md +0 -0
  61. {py61850-0.3.0.dev2 → py61850-0.5.0}/LICENSE +0 -0
  62. {py61850-0.3.0.dev2 → py61850-0.5.0}/examples/01_connect_and_scan.py +0 -0
  63. {py61850-0.3.0.dev2 → py61850-0.5.0}/examples/02_read_values.py +0 -0
  64. {py61850-0.3.0.dev2 → py61850-0.5.0}/examples/03_file_transfer.py +0 -0
  65. {py61850-0.3.0.dev2 → py61850-0.5.0}/examples/04_error_handling.py +0 -0
  66. {py61850-0.3.0.dev2 → py61850-0.5.0}/examples/05_fleet_inventory.py +0 -0
  67. {py61850-0.3.0.dev2 → py61850-0.5.0}/examples/06_find_files.py +0 -0
  68. {py61850-0.3.0.dev2 → py61850-0.5.0}/examples/07_read_object_references.py +0 -0
  69. {py61850-0.3.0.dev2 → py61850-0.5.0}/examples/README.md +0 -0
  70. {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/__main__.py +0 -0
  71. {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/cli/__init__.py +0 -0
  72. {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/cli/__main__.py +0 -0
  73. {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/cli/files.py +0 -0
  74. {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/cli/main.py +0 -0
  75. {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/cli/scan.py +0 -0
  76. {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/core/ber.py +0 -0
  77. {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/core/data.py +0 -0
  78. {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/core/fc.py +0 -0
  79. {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/core/quality.py +0 -0
  80. {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/core/refs.py +0 -0
  81. {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/core/time.py +0 -0
  82. {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/errors.py +0 -0
  83. {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/goose/__init__.py +0 -0
  84. {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/link/__init__.py +0 -0
  85. {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/mms/__init__.py +0 -0
  86. {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/mms/client.py +0 -0
  87. {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/mms/pdu.py +0 -0
  88. {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/mms/service_error.py +0 -0
  89. {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/mms/services/__init__.py +0 -0
  90. {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/mms/services/directory.py +0 -0
  91. {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/mms/services/files.py +0 -0
  92. {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/mms/services/read.py +0 -0
  93. {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/mms/types.py +0 -0
  94. {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/osi/__init__.py +0 -0
  95. {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/osi/acse.py +0 -0
  96. {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/osi/cotp.py +0 -0
  97. {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/osi/oids.py +0 -0
  98. {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/osi/presentation.py +0 -0
  99. {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/osi/session.py +0 -0
  100. {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/osi/stack.py +0 -0
  101. {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/osi/tpkt.py +0 -0
  102. {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/py.typed +0 -0
  103. {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/scl/_xmlsafe.py +0 -0
  104. {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/scl/communication.py +0 -0
  105. {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/scl/model.py +0 -0
  106. {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/scl/templates.py +0 -0
  107. {py61850-0.3.0.dev2 → py61850-0.5.0}/src/py61850/sv/__init__.py +0 -0
  108. {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/README.md +0 -0
  109. {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/__init__.py +0 -0
  110. {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/fixtures/README.md +0 -0
  111. {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/unit/__init__.py +0 -0
  112. {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/unit/test_ber.py +0 -0
  113. {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/unit/test_client.py +0 -0
  114. {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/unit/test_data.py +0 -0
  115. {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/unit/test_fc.py +0 -0
  116. {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/unit/test_file_filter.py +0 -0
  117. {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/unit/test_logical_nodes.py +0 -0
  118. {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/unit/test_osi.py +0 -0
  119. {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/unit/test_pdu.py +0 -0
  120. {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/unit/test_public_api.py +0 -0
  121. {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/unit/test_refs.py +0 -0
  122. {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/unit/test_scl_communication.py +0 -0
  123. {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/unit/test_scl_controls.py +0 -0
  124. {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/unit/test_scl_document.py +0 -0
  125. {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/unit/test_scl_model.py +0 -0
  126. {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/unit/test_scl_templates.py +0 -0
  127. {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/unit/test_types.py +0 -0
  128. {py61850-0.3.0.dev2 → py61850-0.5.0}/tests/unit/test_xmlsafe.py +0 -0
@@ -0,0 +1,60 @@
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
+ # Written by release.yml, from CHANGELOG.md, to be the GitHub release body.
13
+ RELEASE_NOTES.md
14
+
15
+ # SCL / IED capability files (vendor-supplied, large, not redistributable)
16
+ *.icd
17
+ *.ICD
18
+ *.cid
19
+ *.CID
20
+ *.scd
21
+ *.SCD
22
+ *.iid
23
+ *.IID
24
+ *.sed
25
+ *.SED
26
+ fixtures/ICD/
27
+
28
+ # The round-trip corpus is tracked, by name and never by wildcard.
29
+ #
30
+ # The `*.scd` rule above is right for every SCD except these four: a vendor
31
+ # file dropped in this tree is somebody's substation and must not be
32
+ # committed by accident. The three real ones below were anonymised by
33
+ # `tools/anonymise_scd.py` before they were added -- substation, utility,
34
+ # Windows accounts, machine names and addressing all substituted, IED names
35
+ # deliberately kept. The fourth is hand-written and carries no identity.
36
+ #
37
+ # Named one by one on purpose. A negation like `!tests/fixtures/scl/*.scd`
38
+ # would re-include ANY .scd dropped in that directory, which is exactly how
39
+ # an un-anonymised file reached a sibling repository once.
40
+ !tests/fixtures/scl/sel.scd
41
+ !tests/fixtures/scl/mixed.scd
42
+ !tests/fixtures/scl/siemens.scd
43
+ !tests/fixtures/scl/namespaces.scd
44
+
45
+ # The substitution mapping names what was substituted -- the substation, the
46
+ # utility, the engineers' accounts. Tracking it would undo the anonymisation.
47
+ tools/*.local.json
48
+
49
+ # IEC reference material -- the schema and namespace packages themselves.
50
+ #
51
+ # `docs/IEC61850 files/` holds IEC's own code-component distributions: the SCL
52
+ # XSD sets (2003, 2007B, 2007B4, 2007C5), the 7-2/7-3/7-4 NSD packages and
53
+ # IEC's note on handling code components. They are IEC copyright, the terms
54
+ # are at www.iec.ch/CCv1, and this repository is public and ships to PyPI.
55
+ #
56
+ # Excluded as a directory rather than by extension, because the useful ones
57
+ # arrive as .zip and a future .xsd dropped beside them must not be committed
58
+ # either. What may be DERIVED from them and shipped -- the content-model
59
+ # ordering table -- is a separate question, recorded as Q17 in the plan.
60
+ docs/IEC61850 files/
@@ -0,0 +1,224 @@
1
+ # Changelog — py61850
2
+
3
+ What changed in each released version. `ROADMAP.md` is the other half and is
4
+ forward-looking: it says what the library can do and what is planned, this says
5
+ what moved and when.
6
+
7
+ **This file starts at 0.5.0.** Releases up to 0.4.0 were written up in their
8
+ GitHub release bodies and are still there; they are not reconstructed here,
9
+ because a record invented after the fact is worth less than a link to the one
10
+ that was written at the time.
11
+
12
+ ---
13
+
14
+ ## 0.5.0 — 2026-09-15
15
+
16
+ **`py61850.scl` can now CHANGE an SCL document, not only read one and write it
17
+ back.** 0.3.0 gave the object model, 0.4.0 gave the byte-faithful round trip,
18
+ and this release puts an edit layer between them: four invertible primitives, a
19
+ schema-ordering table that decides where a new element goes, and the IEC 61850-6
20
+ rules that make an edit valid rather than merely applied.
21
+
22
+ The surface grows from **31 names to 132**. Nothing was removed and nothing
23
+ changed meaning — see *Compatibility* at the end, where that is measured rather
24
+ than asserted.
25
+
26
+ ### The shape of it
27
+
28
+ Every function returns **a list of edits the caller applies in one go**, and
29
+ `apply_edit` returns the edit that undoes it. That is the whole design: an
30
+ operation that touches six elements in four sections of the file is one call,
31
+ one application and one undo step.
32
+
33
+ ```python
34
+ from py61850.scl import SclDocument, Connection, subscribe
35
+
36
+ doc = SclDocument.parse("station.scd")
37
+
38
+ sink = doc.ied("QMA1_MU1").ext_refs()[2] # an input awaiting a source
39
+ block = doc.ied("TR01_2414").control_blocks()[3] # the GOOSE that publishes it
40
+ fcda = block.logical_node.data_sets[block.dat_set].fcdas[12]
41
+
42
+ edits = subscribe(doc, [Connection(sink=sink.element,
43
+ fcda=fcda.element,
44
+ control_block=block.element)])
45
+
46
+ undo = doc.apply_edit(edits) # ONE history entry, however many edits it is
47
+ doc.write("station.scd") # byte-faithful everywhere it did not touch
48
+ doc.apply_edit(undo) # and exactly reversible
49
+ ```
50
+
51
+ **The edit layer works on `xml.etree` elements, not on the model objects.** The
52
+ model is how you find things; `.element` is what you hand to an edit function.
53
+ That is deliberate — an edit has to be expressible for a part of the file the
54
+ read model does not cover, and the `Substation` section below is exactly that
55
+ case.
56
+
57
+ Depending on what the document already holds, that one `subscribe` call writes
58
+ the ExtRef's binding attributes, creates the `Inputs` element if the logical
59
+ node has none, and — **when asked for, with `ignore_supervision=False`, which is
60
+ not the default** — instantiates the `LGOS`/`LSVS` supervision logical node that
61
+ watches the subscribed control block, allocating its `inst` against what the IED
62
+ already uses. Whether that is one edit or six, it is one call, one application
63
+ and one undo, and undoing it serialises byte-identically to what was there
64
+ before. A property test generates edit sequences and asserts exactly that.
65
+
66
+ It also refuses: the pair above was chosen by the type-restriction guard, and
67
+ the first candidate tried was rejected with
68
+
69
+ ```
70
+ EditRejected: ExtRef expects SPS.t (Timestamp); SPS.stVal is BOOLEAN
71
+ ```
72
+
73
+ **A rejected edit changes nothing.** Every guard raises `EditRejected` before
74
+ touching the tree; there is no partially applied state to clean up.
75
+
76
+ ### What was added
77
+
78
+ 101 names, in fourteen modules. By area:
79
+
80
+ | Area | New names | What it covers |
81
+ |---|---:|---|
82
+ | `edit` | 5 | `Insert`, `Remove`, `SetAttributes`, `SetTextContent`, `EditRejected` — the primitives, each computing its inverse before it applies |
83
+ | `ordering` | 3 | `reference_for`, `may_contain`, `content_model` — schema-ordered insertion, table-driven from the SCL content models |
84
+ | `extref` | 12 | `subscribe` / `unsubscribe` / `is_subscribed`, the type-restriction matching behind them, and `fcda_type` |
85
+ | `control_block` | 9 | `control_blocks`, `update_dat_set`, `remove_control_block`, `updated_conf_rev`, `path_id`, `control_block_obj_ref` |
86
+ | `data_set` | 10 | `can_add_data_set`, create / update / remove, `can_add_fcda`, `remove_fcda`, `max_attributes` |
87
+ | `address` | 6 | `create_gse`, `create_smv`, `change_gse_content`, `change_smv_content`, `change_gse_or_smv_address` |
88
+ | `report_control` | 7 | `can_add_report_control`, create / update, `max_report_control`, `number_report_control_instances` |
89
+ | `sampled_value_control` | 3 | `can_add_sampled_value_control`, create, update |
90
+ | `ied` | 5 | `insert_ied`, `remove_ied`, `update_ied` — and the rename fan-out, which is the substance |
91
+ | `supervision` | 11 | `can_instantiate_supervision`, `instantiate_supervision`, `remove_supervision`, `max_supervision` |
92
+ | `data_types` | 8 | `import_lnode_types`, `update_lnode_type`, `remove_data_type`, `same_data_type`, `lnode_type_conflicts` |
93
+ | `substation` | 10 | `update_substation`, `update_voltage_level`, `update_bay`, `remove_process_element`, `prune_lnode_specification` |
94
+ | `generator` | 11 | `next_mac_address`, `next_app_id`, `next_ln_inst`, `unique_element_name` and the ranges they allocate from |
95
+ | `controls` | 1 | `CONTROL_BLOCK_TAGS` |
96
+
97
+ Three of those are worth naming individually, because they are the ones that do
98
+ more than their name suggests:
99
+
100
+ - **`update_ied` renames across the whole document, not just the `IED`.**
101
+ Renaming an IED has to follow every `ExtRef@iedName`, every concatenated
102
+ object reference that embeds it, and the control-block and supervision
103
+ references that name it — six element names in all, which is what the schema
104
+ requires rather than what a search-and-replace would find. `remove_ied`
105
+ cleans up the subscriptions and supervisions pointing at what it removes.
106
+ - **`import_lnode_types` merges type templates between documents**, with a
107
+ three-way conflict policy, because 165 type ids are shared between a pair of
108
+ the three reference exports and **141 of them carry different content**. Reuse
109
+ is decided on the type's whole closure, not on the element that names it.
110
+ - **`remove_control_block` sweeps by the block**, taking the `GSE` or `SMV`
111
+ address with it and the supervisions that watch it.
112
+
113
+ ### Where this differs from OpenSCD
114
+
115
+ `open-scd-core` and `scl-lib` are this layer's reference, and the comparison is
116
+ behavioural: what theirs does against what ours does. Most divergences are
117
+ Python idiom or the fact that this document is an `xml.etree` tree on a server
118
+ rather than a DOM in a browser. These are the ones a caller actually meets.
119
+
120
+ - **Every function returns everything you should apply, input edit first.** The
121
+ reference is not uniform here — one function returns the array including the
122
+ edit you passed it, another returns only the extra edit, a third returns a
123
+ plural array again — so a caller has to remember, per function, whether to
124
+ also apply its own input. Ours has one rule. The cost is a return type that
125
+ disagrees with the reference's for some functions, and the benefit is that
126
+ applying a fragment on its own and leaving the document half-corrected is not
127
+ a mistake you can make.
128
+ - **Refusals where the reference guesses.** Subscribing refuses a connection it
129
+ cannot supervise; importing a type refuses an id collision rather than picking
130
+ a winner; inserting an IED refuses a name the target already holds. Each of
131
+ those is a place the reference proceeds. The reasoning is the same every
132
+ time: this library produces files that go back into DIGSI and SEL Architect,
133
+ and a wrong answer that loads is worse than a refusal that does not.
134
+ - **Three named outcomes replace a boolean** where an import can reuse, rename
135
+ or conflict, because two of the three are success and a boolean cannot say
136
+ which.
137
+ - **`fcda_type` keeps its name**, where the reference calls the same function
138
+ `fcdaBaseTypes`. It was already in 0.4.0's public surface and this is a MINOR
139
+ release; renaming a published name is not available, and the existing name is
140
+ the better one anyway — it returns the same `TypeRestriction` that
141
+ `ext_ref_type_restrictions` returns, so the two read as the pair they are.
142
+ - **Allocated names are `new<Tag>_01`**, matching the reference's documented
143
+ behaviour. Ours briefly produced `DataSet`/`DataSet_1` and that was corrected
144
+ before this tag.
145
+ - **`prune_lnode_specification` is not called `updateLnType`**, because the
146
+ reference's name for it disagrees with its own doc comment, its return type
147
+ and its directory: it updates no `lnType`, it removes an `LNode`'s
148
+ specification children, and those children live in IEC TR 61850-6-100's
149
+ namespace inside a `Private` rather than in 61850-6 at all.
150
+ - **`unique_element_name` is public**, where the reference does not export it.
151
+
152
+ ### What this release does not implement
153
+
154
+ Stated because overstating it is the one thing a release note can get wrong that
155
+ nobody notices for a year.
156
+
157
+ - **Nothing renames a `GSEControl`.** The reference exports `createGSEControl`
158
+ and `updateGSEControl`; there is no Python equivalent. `ReportControl` and
159
+ `SampledValueControl` have their full create/update pair, `GSEControl` does
160
+ not. This is a gap the work found in itself and did not close.
161
+ - **No IEC namespace (NSD) data ships**, so neither `nsdToJson` nor anything
162
+ built on it has an equivalent. This is a licensing posture, not an oversight.
163
+ It was measured rather than assumed: `pDO` resolves from the document for
164
+ **0 of 1,244** ExtRefs in one reference export, so the data genuinely is not
165
+ in the file.
166
+ - **No plugin adapters.** `lNodeTypeToSelection` shapes a type for an
167
+ `oscd-tree-grid` widget; the structure it walks is already reachable through
168
+ `TemplatePool`.
169
+ - **No `checkPermission`.** Authorisation belongs to whatever is driving the
170
+ library.
171
+ - **`Substation` and `Log` still have no READ MODEL**, and the distinction from
172
+ the edit layer above is real. `update_substation`, `update_voltage_level`,
173
+ `update_bay` and `remove_process_element` work on the tree directly and are
174
+ tested against fixtures built by hand. There is no `doc.substation()` object
175
+ tree, because **all twenty-three Substation-section element names appear zero
176
+ times in all three reference exports**, in every namespace — a test asserts
177
+ that emptiness by scanning the bytes. Building a read model with nothing to
178
+ check it against is how a reader acquires confident wrong answers.
179
+
180
+ ### Python 3.13 is the new floor
181
+
182
+ `requires-python` moves from `>=3.9` to `>=3.13`. **Two reasons, and they are
183
+ not the same kind of reason:**
184
+
185
+ - **3.9 and 3.10 are dead weight.** 3.9 reached end of life in October 2025 and
186
+ 3.10 does so in October 2026. That half is a date.
187
+ - **3.11 and 3.12 are dropped for breadth, not for age.** Both are still
188
+ supported upstream. Six interpreters is more compatibility surface than a
189
+ library whose consumers all move together has any reason to carry, and one
190
+ fewer axis on the matrix is one fewer place a difference can hide. That half
191
+ is a judgement, and it is recorded as one.
192
+
193
+ **This cannot break code that already runs.** A floor is enforced by the
194
+ resolver, not by the interpreter: `pip install py61850` on 3.12 is served 0.4.0
195
+ and never a failure. That is why it travels in a MINOR release alongside a
196
+ strictly additive API, and the distinction is the one that matters — removing a
197
+ name breaks a program, raising a floor declines to hand one a newer library.
198
+
199
+ Nothing in the source branched on the interpreter (`sys.version_info` appears
200
+ nowhere in `src/py61850`), so the bump deletes no logic. The CI matrix narrows
201
+ from six versions to two; Windows still covers both ends of the supported range,
202
+ which is now 3.13 and 3.14.
203
+
204
+ ### Compatibility
205
+
206
+ **Strictly additive, measured one name at a time.** All 31 names of 0.4.0 were
207
+ compared against this tree at the level of kind, module, MRO and the signature of
208
+ every public member. The entire difference is four additions: `ExtRef` and
209
+ `FCDA` each gained an `element` attribute, and `SclDocument` gained `apply_edit`
210
+ and `parent_of`. No name was removed, no signature changed, no class lost a
211
+ member and nothing moved module.
212
+
213
+ One consequence worth stating for anyone holding model objects across an edit:
214
+ **`apply_edit` invalidates the lazy caches by ancestry, and a model object you
215
+ already hold is not updated.** Editing inside one IED forgets that IED and leaves
216
+ the other 57 of a station warm; re-reading the edited one costs about 41 ms
217
+ against 3 ms warm on a 22 MB export.
218
+
219
+ ### Verification
220
+
221
+ 1,411 tests, the round trip held against 44 MB of real station exports from
222
+ three vendors, and the edit-invertibility property test run over generated
223
+ sequences. `py61850` still has **zero runtime dependencies**, which CI asserts
224
+ on every run.
@@ -0,0 +1,144 @@
1
+ Third-party notice: IEC code components
2
+ =======================================
3
+
4
+ Two files in this distribution are DERIVED from IEC code components:
5
+
6
+ src/py61850/scl/_content_models.py
7
+ src/py61850/scl/_nsd_types.py
8
+
9
+ Neither code component is itself part of this distribution, and each derived
10
+ file takes one narrow dimension of the component it came from.
11
+
12
+
13
+ 1. src/py61850/scl/_content_models.py
14
+ -------------------------------------
15
+
16
+ It carries the SCL content models -- which child elements each SCL element may
17
+ hold, and the order they go in -- extracted from the SCL XML schema published
18
+ by IEC as a code component of IEC 61850-6. The schema itself is NOT part of
19
+ this distribution. What is derived from it is one structural dimension: element
20
+ names and their order, and none of the schema's attributes, types,
21
+ enumerations, cardinalities, uniqueness constraints or documentation.
22
+
23
+ Attribution, as the licence below requires:
24
+
25
+ This code was derived from IEC 61850-6:2009/AMD1:2018 within modifications
26
+ permitted in the relevant IEC standard. Please reproduce this note if
27
+ possible.
28
+
29
+ The code component it was derived from:
30
+
31
+ IEC 61850-6 SCL schema V2007B4
32
+ Code component id: IEC_61850-6.2007B4.SCL.XSD
33
+ Publication: IEC 61850-6:2009/AMD1:2018 (Ed. 2.1), dated 2018-01-22
34
+
35
+ The copyright notice carried by that code component:
36
+
37
+ COPYRIGHT (c) IEC, 2018. This version of this XSD is part of
38
+ IEC 61850-6:2009/AMD1:2018; see the IEC 61850-6:2009/AMD1:2018 for full
39
+ legal notices. In case of any differences between the here-below code and
40
+ the IEC published content, the here-below definition supersedes the IEC
41
+ publication; it may contain updates. See history files. The whole document
42
+ has to be taken into account to have a full description of this code
43
+ component.
44
+ See www.iec.ch/CCv1 for copyright details.
45
+
46
+
47
+ 2. src/py61850/scl/_nsd_types.py
48
+ --------------------------------
49
+
50
+ It carries two facts about the IEC 61850 data model -- which common data class
51
+ each data object name has, and which basic type each of a class's data
52
+ attributes has -- extracted from the namespace definitions published by IEC as
53
+ code components of IEC 61850-7-4 and IEC 61850-7-3. The namespace definitions
54
+ themselves are NOT part of this distribution. What is derived from them is the
55
+ type dimension alone, and none of their functional constraints, presence
56
+ conditions, enumerations and their literals, abbreviations, service parameters,
57
+ UML identifiers or documentation.
58
+
59
+ Attribution, as the licence below requires:
60
+
61
+ This code was derived from IEC 61850-7-3:2010 and IEC 61850-7-4:2020 within
62
+ modifications permitted in the relevant IEC standard. Please reproduce this
63
+ note if possible.
64
+
65
+ The code components it was derived from:
66
+
67
+ IEC 61850-7-3 namespace definition V2007B5
68
+ Code component id: IEC 61850-7-3:2007B5
69
+ Publication: IEC 61850-7-3:2010 (Ed. 2.1), dated 2024-02-12
70
+
71
+ IEC 61850-7-4 namespace definition V2007B5
72
+ Code component id: IEC 61850-7-4:2007B5
73
+ Publication: IEC 61850-7-4:2020 (Ed. 2.1), dated 2024-02-14
74
+
75
+ The copyright notice carried by those code components:
76
+
77
+ COPYRIGHT (c) IEC, www.iec.ch/tc57/supportdocuments. This version of this
78
+ NSD is part of IEC_61850-7-3:2010 Edition 2.1 [respectively
79
+ IEC_61850-7-4:2020 Edition 2.1]; see that publication for full legal
80
+ notices. In case of any differences between the here-below code and the IEC
81
+ published content, the here-below definition supersedes the IEC
82
+ publication; it may contain updates. See history files. The whole document
83
+ has to be taken into account to have a full description of this code
84
+ component.
85
+ See www.iec.ch/CCv1 for copyright details.
86
+
87
+
88
+ py61850 is not endorsed by, affiliated with, or approved by IEC.
89
+
90
+
91
+ Code Components -- End-user licence agreement
92
+ ---------------------------------------------
93
+
94
+ Reproduced from www.iec.ch/CCv1, which the code component's manifest names as
95
+ its licence.
96
+
97
+ Code Components in IEC standards (International Standards, Technical
98
+ Specifications or Technical Reports) which have been identified and approved
99
+ for licensing, are licensed subject to the following conditions:
100
+
101
+ - Redistributions of software must retain the Copyright Notice, this list of
102
+ conditions and the disclaimer below ("Disclaimer").
103
+ - The software license extends to modifications permitted under the relevant
104
+ IEC standard.
105
+ - The software license extends to clarifications and corrections approved by
106
+ IEC.
107
+ - Neither the name of IEC, nor the names of specific contributors, may be used
108
+ to endorse or promote products derived from this software without specific
109
+ prior written permission. The relevant IEC standard may be referenced when
110
+ claiming compliance with the relevant IEC standard.
111
+ - The user of Code Components shall attribute each such Code Component to IEC
112
+ and identify the IEC standard from which it is taken. Such attribution (e.g.,
113
+ "This code was derived from IEC [insert standard reference number:publication
114
+ year] within modifications permitted in the relevant IEC standard. Please
115
+ reproduce this note if possible."), may be placed in the code itself or any
116
+ other reasonable location.
117
+
118
+ Code Components means components included in IEC standards that are intended to
119
+ be directly processed by a computer and also includes any text found between
120
+ the markers <CODE BEGINS> and <CODE ENDS>, or otherwise clearly labeled in this
121
+ standard as a Code Component.
122
+
123
+ The Disclaimer is:
124
+
125
+ EACH OF THE CODE COMPONENTS IS PROVIDED BY THE COPYRIGHT HOLDERS AND
126
+ CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
127
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
128
+ PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
129
+ CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
130
+ EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
131
+ PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
132
+ BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
133
+ IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
134
+ ARISING IN ANY WAY OUT OF THE USE OF THE CODE COMPONENTS, EVEN IF ADVISED OF
135
+ THE POSSIBILITY OF SUCH DAMAGE.
136
+
137
+
138
+ Everything else in this distribution
139
+ ------------------------------------
140
+
141
+ Licensed under AGPL-3.0-or-later; see LICENSE. A commercial licence is
142
+ available from the copyright holder -- see COMMERCIAL.md. The terms above
143
+ apply to the derived files named at the top of this notice, and travel with
144
+ them.
@@ -1,3 +1,28 @@
1
+ Metadata-Version: 2.5
2
+ Name: py61850
3
+ Version: 0.5.0
4
+ Summary: Pure-Python IEC 61850 toolkit — an MMS (ISO 9506) client, and an SCL reader/writer/editor with a byte-faithful round trip. Standard library only.
5
+ Project-URL: Homepage, https://github.com/GuilhermeMarini/py61850
6
+ Project-URL: Repository, https://github.com/GuilhermeMarini/py61850
7
+ Project-URL: Issues, https://github.com/GuilhermeMarini/py61850/issues
8
+ Project-URL: Changelog, https://github.com/GuilhermeMarini/py61850/blob/main/CHANGELOG.md
9
+ Project-URL: Roadmap, https://github.com/GuilhermeMarini/py61850/blob/main/ROADMAP.md
10
+ Author: Guilherme
11
+ License-Expression: AGPL-3.0-or-later
12
+ License-File: LICENSE
13
+ License-File: NOTICE-IEC.txt
14
+ Keywords: goose,iec61850,ied,iso9506,mms,relay,sampled-values,scada,substation
15
+ Classifier: Development Status :: 4 - Beta
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Scientific/Engineering
22
+ Classifier: Topic :: System :: Networking
23
+ Requires-Python: >=3.13
24
+ Description-Content-Type: text/markdown
25
+
1
26
  # py61850 — IEC 61850 toolkit (pure Python)
2
27
 
3
28
  An importable library (and its CLIs) for talking to an IEC 61850 IED over MMS
@@ -10,8 +35,14 @@ TCP → TPKT (RFC1006) → COTP (ISO8073) → ISO Session → ISO Presentation
10
35
  → ACSE (AARQ/AARE) → MMS (Initiate + confirmed services)
11
36
  ```
12
37
 
38
+ It is also a **61850-6 (SCL) toolkit**: `py61850.scl` reads an `.scd`/`.cid`/
39
+ `.icd` into a general object model, **changes** it through an edit layer whose
40
+ every operation is invertible, and writes it back **byte for byte** where it did
41
+ not touch. That half needs no relay and no network at all.
42
+
13
43
  See [ROADMAP.md](ROADMAP.md) for what's next (an MMS sniffer, MMS simulation
14
- from SCL, and a GOOSE sniff/publish interface).
44
+ from SCL, and a GOOSE sniff/publish interface), and
45
+ [CHANGELOG.md](CHANGELOG.md) for what changed in each release.
15
46
 
16
47
  ## What this library is for
17
48
 
@@ -40,11 +71,12 @@ Concretely, that means three things for any change here:
40
71
  pip install -e . # from a checkout (editable)
41
72
  # or, once published/tagged:
42
73
  # pip install py61850
43
- # pip install "git+https://github.com/OWNER/py61850@v0.1.0"
74
+ # pip install "git+https://github.com/OWNER/py61850@v0.5.0"
44
75
  ```
45
76
 
46
- Requires Python ≥ 3.9. Installs `py61850` (subcommands) plus `mms-scan` and
47
- `mms-files` for the two MMS drivers directly.
77
+ Requires **Python ≥ 3.13** (raised from 3.9 in 0.5.0 — see
78
+ [CHANGELOG.md](CHANGELOG.md) for both reasons). Installs `py61850`
79
+ (subcommands) plus `mms-scan` and `mms-files` for the two MMS drivers directly.
48
80
 
49
81
  ## Use as a library
50
82
 
@@ -108,7 +140,7 @@ with FileTransfer("192.0.2.22") as ft:
108
140
  progress=lambda got, total: print(f"{got}/{total}"))
109
141
  ```
110
142
 
111
- ### Reading an SCL file
143
+ ### Reading an SCL file — and writing it back unchanged
112
144
 
113
145
  ```python
114
146
  from py61850.scl import SclDocument
@@ -126,6 +158,74 @@ for ln in ied.logical_nodes():
126
158
  print(attr.reference(), attr.mms_item(), attr.fc, attr.btype)
127
159
  ```
128
160
 
161
+ A parsed document goes back out as the file it came from:
162
+
163
+ ```python
164
+ doc.write("station.scd") # atomic: temp file beside it, then os.replace
165
+ raw = doc.to_bytes() # or the bytes, to hand somewhere else
166
+ ```
167
+
168
+ **Byte for byte**, with no edit applied — comments, indentation, attribute
169
+ order, namespace prefixes, `xmlns` declarations nothing uses, the line ending
170
+ the file was written with and the XML declaration as it was spelled all
171
+ survive. Five differences are permitted, none of them observable through an
172
+ XML parser and every one a limit of the standard library's serialiser:
173
+ attribute quote style, empty-element spacing and form, CDATA boundaries, and
174
+ the spelling of a numeric character reference. `SclDocument.to_bytes` states
175
+ all five; the round-trip test holds them against 44 MB of real station
176
+ exports from three vendors.
177
+
178
+ That guarantee is the point of the write side. The file goes back into DIGSI
179
+ and SEL Architect, and a library that reformats the 99 % of an SCD it did not
180
+ touch turns every save into a whole-file diff.
181
+
182
+ ### Changing it — the edit layer
183
+
184
+ Since 0.5.0 a document can also be **edited**, and every edit is invertible.
185
+ Four primitives (`Insert`, `Remove`, `SetAttributes`, `SetTextContent`) each
186
+ compute their inverse *before* they apply; a list of them is itself an edit,
187
+ applied in order and inverted in reverse. On top of those sit the IEC 61850-6
188
+ rules that decide what may be created where, what a rename has to drag along,
189
+ and where in a parent a new element belongs.
190
+
191
+ ```python
192
+ from py61850.scl import SclDocument, Connection, subscribe
193
+
194
+ doc = SclDocument.parse("station.scd")
195
+
196
+ sink = doc.ied("QMA1_MU1").ext_refs()[2] # an input awaiting a source
197
+ block = doc.ied("TR01_2414").control_blocks()[3] # the GOOSE that publishes it
198
+ fcda = block.logical_node.data_sets[block.dat_set].fcdas[12]
199
+
200
+ edits = subscribe(doc, [Connection(sink=sink.element,
201
+ fcda=fcda.element,
202
+ control_block=block.element)])
203
+
204
+ undo = doc.apply_edit(edits) # ONE history entry, however many edits it is
205
+ doc.write("station.scd")
206
+ doc.apply_edit(undo) # restores the document exactly
207
+ ```
208
+
209
+ Three things hold the layer together:
210
+
211
+ - **Every function returns everything you should apply, input edit first.** One
212
+ rule, one `apply_edit` call, one undo entry — so "subscribe this ExtRef",
213
+ which may touch an `ExtRef`, an `Inputs` and a supervision logical node in
214
+ three places, is one step in a caller's history.
215
+ - **The edit functions take `xml.etree` elements, not the model objects.** The
216
+ model is how you find things and `.element` is what you pass; that is what
217
+ lets an edit reach a part of the file the read model does not cover.
218
+ - **A rejected edit changes nothing.** Guards raise `EditRejected` before the
219
+ tree is touched, so there is no half-applied state — the same rule the round
220
+ trip depends on.
221
+
222
+ `py61850.scl.__all__` is 132 names across the areas it can edit: control blocks
223
+ and datasets, GSE/SMV addresses, report and sampled-value control, IEDs and the
224
+ rename fan-out, subscription supervision, `DataTypeTemplates` import and merge,
225
+ the `Substation` section, and the allocators for MAC addresses, APPIDs and
226
+ instance numbers. `CHANGELOG.md` lists them by area, says where this diverges
227
+ from OpenSCD and — more usefully — what it does **not** implement.
228
+
129
229
  `py61850.scl` is imported explicitly and is never pulled in by `import
130
230
  py61850`, so the MMS client keeps installing and running unprivileged.
131
231
 
@@ -157,6 +257,9 @@ use one client per thread.
157
257
  | `Iec61850Error` | base of `TransportError`, `MmsError`, and the reserved `LinkError` / `GooseError` / `SvError` / `SclError` |
158
258
  | `decode_read_response`, `decode_data_definition`, `decode_service_error` | TLV decoders |
159
259
 
260
+ `py61850.scl` has a public surface of its own — 132 names, imported from
261
+ `py61850.scl` rather than from `py61850` — and the two do not overlap.
262
+
160
263
  Everything else — `core.ber`, `mms.pdu`, `osi.*`, `CotpTransport` — is internal
161
264
  and may change between releases.
162
265
 
@@ -248,7 +351,14 @@ src/py61850/
248
351
  ├── link/ # packet-capture sources — planned (ROADMAP 0.2, 2.0)
249
352
  ├── goose/ # GOOSE pub/sub — planned (ROADMAP 2.0)
250
353
  ├── sv/ # Sampled Values — planned (after GOOSE)
251
- ├── scl/ # SCL parsing — planned (ROADMAP 1.0)
354
+ ├── scl/ # IEC 61850-6: read, edit, write. The largest package
355
+ │ │ # here, and never imported by `py61850` itself
356
+ │ ├── document.py # SclDocument: parse, the byte-faithful write, edits
357
+ │ ├── model.py # the instance tree, built per IED on demand
358
+ │ ├── templates.py # DataTypeTemplates resolved once per document
359
+ │ ├── edit.py # the four primitives, each inverted before it applies
360
+ │ ├── ordering.py # where a new element goes, from the content models
361
+ │ └── … # one module per 61850-6 area it can edit
252
362
  └── cli/
253
363
  ├── main.py # `py61850` — subcommand dispatcher
254
364
  ├── scan.py # `mms-scan` — data-model scan driver