From 5dd0361de29a2fab6aa798c97be86d18614a912a Mon Sep 17 00:00:00 2001 From: Sebastian Brix Date: Mon, 22 Jan 2018 12:31:56 +0100 Subject: [PATCH] initial commit --- .gitignore | 2 + Makefile | 6 + Pipfile | 13 + Pipfile.lock | 430 +++ clock-test.py | 62 + doc-pdoc/css.mako | 283 ++ doc-pdoc/html.mako | 513 ++++ doc-pdoc/text.mako | 162 ++ doc/clone.m.md | 679 +++++ doc/core.m.md | 1988 +++++++++++++ doc/index.md | 648 +++++ doc/intf.m.md | 6687 ++++++++++++++++++++++++++++++++++++++++++++ doc/keyed.m.md | 753 +++++ doc/store.m.md | 851 ++++++ doc/switch.m.md | 1491 ++++++++++ doc/util.m.md | 1346 +++++++++ listen.py | 30 + setup.py | 39 + supcon/__init__.py | 94 + supcon/clone.py | 89 + supcon/core.py | 813 ++++++ supcon/graph.py | 144 + supcon/intf.py | 742 +++++ supcon/keyed.py | 80 + supcon/store.py | 90 + supcon/switch.py | 208 ++ supcon/util.py | 175 ++ 27 files changed, 18418 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 Pipfile create mode 100644 Pipfile.lock create mode 100755 clock-test.py create mode 100644 doc-pdoc/css.mako create mode 100644 doc-pdoc/html.mako create mode 100644 doc-pdoc/text.mako create mode 100644 doc/clone.m.md create mode 100644 doc/core.m.md create mode 100644 doc/index.md create mode 100644 doc/intf.m.md create mode 100644 doc/keyed.m.md create mode 100644 doc/store.m.md create mode 100644 doc/switch.m.md create mode 100644 doc/util.m.md create mode 100755 listen.py create mode 100644 setup.py create mode 100644 supcon/__init__.py create mode 100644 supcon/clone.py create mode 100644 supcon/core.py create mode 100644 supcon/graph.py create mode 100644 supcon/intf.py create mode 100644 supcon/keyed.py create mode 100644 supcon/store.py create mode 100644 supcon/switch.py create mode 100644 supcon/util.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e07a6e8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.directory +__pycache__ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ca6f291 --- /dev/null +++ b/Makefile @@ -0,0 +1,6 @@ +doku: + rm -rf ./doc/* + PYTHONPATH=. pipenv run pdoc --template-dir doc-pdoc --html --html-dir ./doc --all-submodules supcon + mv ./doc/supcon/* ./doc/ + rm -rf ./doc/supcon + for f in ./doc/*.html; do mv $${f} `dirname $${f}`/`basename $${f} html`md; done diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..89d61f2 --- /dev/null +++ b/Pipfile @@ -0,0 +1,13 @@ +[[source]] +url = "https://pypi.python.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +"e1839a8" = {path = ".", editable = true} + +[dev-packages] +"e1839a8" = {path = ".", extras = ["e"], editable = true} + +[requires] +python_version = "3" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..203345b --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,430 @@ +{ + "_meta": { + "hash": { + "sha256": "5222cbe0571531c1844f56e5766605155e243efab356f8b635f0ad9956893afa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "asn1crypto": { + "hashes": [ + "sha256:2f1adbb7546ed199e3c90ef23ec95c5cf3585bac7d11fb7eb562a3fe89c64e87", + "sha256:9d5c20441baf0cb60a4ac34cc447c6c189024b6b4c6cd7877034f4965c464e49" + ], + "version": "==0.24.0" + }, + "attrs": { + "hashes": [ + "sha256:1c7960ccfd6a005cd9f7ba884e6316b5e430a3f1a6c37c5f87d8b43f83b54ec9", + "sha256:a17a9573a6f475c99b551c0e0a812707ddda1ec9653bed04c13841404ed6f450" + ], + "version": "==17.4.0" + }, + "automat": { + "hashes": [ + "sha256:2140297df155f7990f6f4c73b2ab0583bd8150db9ed2a1b48122abe66e9908c1", + "sha256:3c1fd04ecf08ac87b4dd3feae409542e9bf7827257097b2b6ed5692f69d6f6a8" + ], + "version": "==0.6.0" + }, + "cffi": { + "hashes": [ + "sha256:151b7eefd035c56b2b2e1eb9963c90c6302dc15fbd8c1c0a83a163ff2c7d7743", + "sha256:1553d1e99f035ace1c0544050622b7bc963374a00c467edafac50ad7bd276aef", + "sha256:1b0493c091a1898f1136e3f4f991a784437fac3673780ff9de3bcf46c80b6b50", + "sha256:2ba8a45822b7aee805ab49abfe7eec16b90587f7f26df20c71dd89e45a97076f", + "sha256:3c85641778460581c42924384f5e68076d724ceac0f267d66c757f7535069c93", + "sha256:3eb6434197633b7748cea30bf0ba9f66727cdce45117a712b29a443943733257", + "sha256:4c91af6e967c2015729d3e69c2e51d92f9898c330d6a851bf8f121236f3defd3", + "sha256:770f3782b31f50b68627e22f91cb182c48c47c02eb405fd689472aa7b7aa16dc", + "sha256:79f9b6f7c46ae1f8ded75f68cf8ad50e5729ed4d590c74840471fc2823457d04", + "sha256:7a33145e04d44ce95bcd71e522b478d282ad0eafaf34fe1ec5bbd73e662f22b6", + "sha256:857959354ae3a6fa3da6651b966d13b0a8bed6bbc87a0de7b38a549db1d2a359", + "sha256:87f37fe5130574ff76c17cab61e7d2538a16f843bb7bca8ebbc4b12de3078596", + "sha256:95d5251e4b5ca00061f9d9f3d6fe537247e145a8524ae9fd30a2f8fbce993b5b", + "sha256:9d1d3e63a4afdc29bd76ce6aa9d58c771cd1599fbba8cf5057e7860b203710dd", + "sha256:a36c5c154f9d42ec176e6e620cb0dd275744aa1d804786a71ac37dc3661a5e95", + "sha256:ae5e35a2c189d397b91034642cb0eab0e346f776ec2eb44a49a459e6615d6e2e", + "sha256:b0f7d4a3df8f06cf49f9f121bead236e328074de6449866515cea4907bbc63d6", + "sha256:b75110fb114fa366b29a027d0c9be3709579602ae111ff61674d28c93606acca", + "sha256:ba5e697569f84b13640c9e193170e89c13c6244c24400fc57e88724ef610cd31", + "sha256:be2a9b390f77fd7676d80bc3cdc4f8edb940d8c198ed2d8c0be1319018c778e1", + "sha256:d5d8555d9bfc3f02385c1c37e9f998e2011f0db4f90e250e5bc0c0a85a813085", + "sha256:e55e22ac0a30023426564b1059b035973ec82186ddddbac867078435801c7801", + "sha256:e90f17980e6ab0f3c2f3730e56d1fe9bcba1891eeea58966e89d352492cc74f4", + "sha256:ecbb7b01409e9b782df5ded849c178a0aa7c906cf8c5a67368047daab282b184", + "sha256:ed01918d545a38998bfa5902c7c00e0fee90e957ce036a4000a88e3fe2264917", + "sha256:edabd457cd23a02965166026fd9bfd196f4324fe6032e866d0f3bd0301cd486f", + "sha256:fdf1c1dc5bafc32bc5d08b054f94d659422b05aba244d6be4ddc1c72d9aa70fb" + ], + "markers": "platform_python_implementation != 'pypy'", + "version": "==1.11.5" + }, + "constantly": { + "hashes": [ + "sha256:586372eb92059873e29eba4f9dec8381541b4d3834660707faf8ba59146dfc35", + "sha256:dd2fa9d6b1a51a83f0d7dd76293d734046aa176e384bf6e33b7e44880eb37c5d" + ], + "version": "==15.1.0" + }, + "cryptography": { + "hashes": [ + "sha256:3f3b65d5a16e6b52fba63dc860b62ca9832f51f1a2ae5083c78b6840275f12dd", + "sha256:551a3abfe0c8c6833df4192a63371aa2ff43afd8f570ed345d31f251d78e7e04", + "sha256:5cb990056b7cadcca26813311187ad751ea644712022a3976443691168781b6f", + "sha256:60bda7f12ecb828358be53095fc9c6edda7de8f1ef571f96c00b2363643fa3cd", + "sha256:6fef51ec447fe9f8351894024e94736862900d3a9aa2961528e602eb65c92bdb", + "sha256:77d0ad229d47a6e0272d00f6bf8ac06ce14715a9fd02c9a97f5a2869aab3ccb2", + "sha256:808fe471b1a6b777f026f7dc7bd9a4959da4bfab64972f2bbe91e22527c1c037", + "sha256:9b62fb4d18529c84b961efd9187fecbb48e89aa1a0f9f4161c61b7fc42a101bd", + "sha256:9e5bed45ec6b4f828866ac6a6bedf08388ffcfa68abe9e94b34bb40977aba531", + "sha256:9fc295bf69130a342e7a19a39d7bbeb15c0bcaabc7382ec33ef3b2b7d18d2f63", + "sha256:abd070b5849ed64e6d349199bef955ee0ad99aefbad792f0c587f8effa681a5e", + "sha256:ba6a774749b6e510cffc2fb98535f717e0e5fd91c7c99a61d223293df79ab351", + "sha256:c332118647f084c983c6a3e1dba0f3bcb051f69d12baccac68db8d62d177eb8a", + "sha256:d6f46e862ee36df81e6342c2177ba84e70f722d9dc9c6c394f9f1f434c4a5563", + "sha256:db6013746f73bf8edd9c3d1d3f94db635b9422f503db3fc5ef105233d4c011ab", + "sha256:f57008eaff597c69cf692c3518f6d4800f0309253bb138b526a37fe9ef0c7471", + "sha256:f6c821ac253c19f2ad4c8691633ae1d1a17f120d5b01ea1d256d7b602bc59887" + ], + "version": "==2.2.2" + }, + "e1839a8": { + "editable": true, + "path": "." + }, + "hyperlink": { + "hashes": [ + "sha256:98da4218a56b448c7ec7d2655cb339af1f7d751cf541469bb4fc28c4a4245b34", + "sha256:f01b4ff744f14bc5d0a22a6b9f1525ab7d6312cb0ff967f59414bbac52f0a306" + ], + "version": "==18.0.0" + }, + "idna": { + "hashes": [ + "sha256:2c6a5de3089009e3da7c5dde64a141dbc8551d5b7f6cf4ed7c2568d0cc520a8f", + "sha256:8c7309c718f94b3a625cb648ace320157ad16ff131ae0af362c9f21b80ef6ec4" + ], + "version": "==2.6" + }, + "incremental": { + "hashes": [ + "sha256:717e12246dddf231a349175f48d74d93e2897244939173b01974ab6661406b9f", + "sha256:7b751696aaf36eebfab537e458929e194460051ccad279c72b755a167eebd4b3" + ], + "version": "==17.5.0" + }, + "msgpack-python": { + "hashes": [ + "sha256:378cc8a6d3545b532dfd149da715abae4fda2a3adb6d74e525d0d5e51f46909b" + ], + "version": "==0.5.6" + }, + "pigpio": { + "hashes": [ + "sha256:38bdc1b4d58898e7b4a69de3b0d90e8e8ccd851ac7d03e30791769475ffd941f", + "sha256:be1480aecf7c8aba9c92d2846ae153ad4c7e89ae188b28e4bcc579c3bc30e36b" + ], + "version": "==1.40.post1" + }, + "pyasn1": { + "hashes": [ + "sha256:0d7f6e959fe53f3960a23d73f35e1fce61348b30915b6664309ca756de7c1f89", + "sha256:5a0db897b311d265cde49615cf783f1c78613138605cdd0f907ecfa5b2aba3ee", + "sha256:758cb50abddc03e4563fd9e7f03db56e3e87b58c0bd01247360326e5c0c7ffa5", + "sha256:7d626683e3d792cccc608da02498aff37ab4f3dafd8905d6bf755d11f9b26b43", + "sha256:a7efe807c4b83a859e2735c692b92ed7b567cfddc4163763412920041d876c2b", + "sha256:b5a9ca48055b9a20f6d1b3d68e38692e5431c86a0f99ea602e61294e891fee5b", + "sha256:c07d6e587b2f928366b1f67c09bda026a3e6fcc99e80a744dc67f8fca3895626", + "sha256:d258b0a71994f7770599835249cece1caef3c70def868c4915e6e5ca49b67d15", + "sha256:d5cd6ed995dba16fad0c521cfe31cd2d68400b53fcc2bce93326829be73ab6d1", + "sha256:d84c2aea3cf43780e9e6a19f4e4dddee9f6976519020e64e47c57e5c7a8c3dd2", + "sha256:e85895087905c65b5b594eb91f7522664c85545b147d5f4d4e7b1b07da8dcbdc", + "sha256:f81c96761fca60d64b1c9b79ec2e40cf9495a745cf570613079ef324aeb9672b" + ], + "version": "==0.4.2" + }, + "pyasn1-modules": { + "hashes": [ + "sha256:041e9fbafac548d095f5b6c3b328b80792f006196e15a232b731a83c93d59493", + "sha256:0cdca76a68dcb701fff58c397de0ef9922b472b1cb3ea9695ca19d03f1869787", + "sha256:0cea139045c38f84abaa803bcb4b5e8775ea12a42af10019d942f227acc426c3", + "sha256:0f2e50d20bc670be170966638fa0ae603f0bc9ed6ebe8e97a6d1d4cef30cc889", + "sha256:47fb6757ab78fe966e7c58b2030b546854f78416d653163f0ce9290cf2278e8b", + "sha256:598a6004ec26a8ab40a39ea955068cf2a3949ad9c0030da970f2e1ca4c9f1cc9", + "sha256:72fd8b0c11191da088147c6e4678ec53e573923ecf60b57eeac9e97433e09fc2", + "sha256:854700bbdd01394e2ada9c1bfbd0ed9f5d0c551350dbbd023e88b11d2771ae06", + "sha256:af00ea8f2022b6287dc375b2c70f31ab5af83989fc6fe9eacd4976ce26cd7ccc", + "sha256:b1f395cae2d669e0830cb023aa86f9f283b7a9aa32317d7f80d8e78aa2745812", + "sha256:c6747146e95d2b14cc2a8399b2b0bde3f93778f8f9ec704690d2b589c376c137", + "sha256:f53fe5bcebdf318f51399b250fe8325ef3a26d927f012cc0c8e0f9e9af7f9deb" + ], + "version": "==0.2.1" + }, + "pycparser": { + "hashes": [ + "sha256:99a8ca03e29851d96616ad0404b4aad7d9ee16f25c9f9708a11faf2810f7b226" + ], + "version": "==2.18" + }, + "pyopenssl": { + "hashes": [ + "sha256:07a2de1a54de07448732a81e38a55df7da109b2f47f599f8bb35b0cbec69d4bd", + "sha256:2c10cfba46a52c0b0950118981d61e72c1e5b1aac451ca1bc77de1a679456773" + ], + "version": "==17.5.0" + }, + "service-identity": { + "hashes": [ + "sha256:0e76f3c042cc0f5c7e6da002cf646f59dc4023962d1d1166343ce53bdad39e17", + "sha256:4001fbb3da19e0df22c47a06d29681a398473af4aa9d745eca525b3b2c2302ab" + ], + "version": "==17.0.0" + }, + "six": { + "hashes": [ + "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", + "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" + ], + "version": "==1.11.0" + }, + "twisted": { + "hashes": [ + "sha256:0da1a7e35d5fcae37bc9c7978970b5feb3bc82822155b8654ec63925c05af75c", + "sha256:716805e624f9396fcc1f47e8aef68e629fd31599a74855b6e1636122c042458d", + "sha256:7bc3cdfd1ca5e5b84c7936db3c2cb2feb7d5b77410e713fd346da095a3b6a1d2" + ], + "version": "==17.9.0" + }, + "zope.interface": { + "hashes": [ + "sha256:21506674d30c009271fe68a242d330c83b1b9d76d62d03d87e1e9528c61beea6", + "sha256:3d184aff0756c44fff7de69eb4cd5b5311b6f452d4de28cb08343b3f21993763", + "sha256:467d364b24cb398f76ad5e90398d71b9325eb4232be9e8a50d6a3b3c7a1c8789", + "sha256:57c38470d9f57e37afb460c399eb254e7193ac7fb8042bd09bdc001981a9c74c", + "sha256:9ada83f4384bbb12dedc152bcdd46a3ac9f5f7720d43ac3ce3e8e8b91d733c10", + "sha256:a1daf9c5120f3cc6f2b5fef8e1d2a3fb7bbbb20ed4bfdc25bc8364bc62dcf54b", + "sha256:e6b77ae84f2b8502d99a7855fa33334a1eb6159de45626905cb3e454c023f339", + "sha256:e881ef610ff48aece2f4ee2af03d2db1a146dc7c705561bd6089b2356f61641f", + "sha256:f41037260deaacb875db250021fe883bf536bf6414a4fd25b25059b02e31b120" + ], + "version": "==4.5.0" + } + }, + "develop": { + "asn1crypto": { + "hashes": [ + "sha256:2f1adbb7546ed199e3c90ef23ec95c5cf3585bac7d11fb7eb562a3fe89c64e87", + "sha256:9d5c20441baf0cb60a4ac34cc447c6c189024b6b4c6cd7877034f4965c464e49" + ], + "version": "==0.24.0" + }, + "attrs": { + "hashes": [ + "sha256:1c7960ccfd6a005cd9f7ba884e6316b5e430a3f1a6c37c5f87d8b43f83b54ec9", + "sha256:a17a9573a6f475c99b551c0e0a812707ddda1ec9653bed04c13841404ed6f450" + ], + "version": "==17.4.0" + }, + "automat": { + "hashes": [ + "sha256:2140297df155f7990f6f4c73b2ab0583bd8150db9ed2a1b48122abe66e9908c1", + "sha256:3c1fd04ecf08ac87b4dd3feae409542e9bf7827257097b2b6ed5692f69d6f6a8" + ], + "version": "==0.6.0" + }, + "cffi": { + "hashes": [ + "sha256:151b7eefd035c56b2b2e1eb9963c90c6302dc15fbd8c1c0a83a163ff2c7d7743", + "sha256:1553d1e99f035ace1c0544050622b7bc963374a00c467edafac50ad7bd276aef", + "sha256:1b0493c091a1898f1136e3f4f991a784437fac3673780ff9de3bcf46c80b6b50", + "sha256:2ba8a45822b7aee805ab49abfe7eec16b90587f7f26df20c71dd89e45a97076f", + "sha256:3c85641778460581c42924384f5e68076d724ceac0f267d66c757f7535069c93", + "sha256:3eb6434197633b7748cea30bf0ba9f66727cdce45117a712b29a443943733257", + "sha256:4c91af6e967c2015729d3e69c2e51d92f9898c330d6a851bf8f121236f3defd3", + "sha256:770f3782b31f50b68627e22f91cb182c48c47c02eb405fd689472aa7b7aa16dc", + "sha256:79f9b6f7c46ae1f8ded75f68cf8ad50e5729ed4d590c74840471fc2823457d04", + "sha256:7a33145e04d44ce95bcd71e522b478d282ad0eafaf34fe1ec5bbd73e662f22b6", + "sha256:857959354ae3a6fa3da6651b966d13b0a8bed6bbc87a0de7b38a549db1d2a359", + "sha256:87f37fe5130574ff76c17cab61e7d2538a16f843bb7bca8ebbc4b12de3078596", + "sha256:95d5251e4b5ca00061f9d9f3d6fe537247e145a8524ae9fd30a2f8fbce993b5b", + "sha256:9d1d3e63a4afdc29bd76ce6aa9d58c771cd1599fbba8cf5057e7860b203710dd", + "sha256:a36c5c154f9d42ec176e6e620cb0dd275744aa1d804786a71ac37dc3661a5e95", + "sha256:ae5e35a2c189d397b91034642cb0eab0e346f776ec2eb44a49a459e6615d6e2e", + "sha256:b0f7d4a3df8f06cf49f9f121bead236e328074de6449866515cea4907bbc63d6", + "sha256:b75110fb114fa366b29a027d0c9be3709579602ae111ff61674d28c93606acca", + "sha256:ba5e697569f84b13640c9e193170e89c13c6244c24400fc57e88724ef610cd31", + "sha256:be2a9b390f77fd7676d80bc3cdc4f8edb940d8c198ed2d8c0be1319018c778e1", + "sha256:d5d8555d9bfc3f02385c1c37e9f998e2011f0db4f90e250e5bc0c0a85a813085", + "sha256:e55e22ac0a30023426564b1059b035973ec82186ddddbac867078435801c7801", + "sha256:e90f17980e6ab0f3c2f3730e56d1fe9bcba1891eeea58966e89d352492cc74f4", + "sha256:ecbb7b01409e9b782df5ded849c178a0aa7c906cf8c5a67368047daab282b184", + "sha256:ed01918d545a38998bfa5902c7c00e0fee90e957ce036a4000a88e3fe2264917", + "sha256:edabd457cd23a02965166026fd9bfd196f4324fe6032e866d0f3bd0301cd486f", + "sha256:fdf1c1dc5bafc32bc5d08b054f94d659422b05aba244d6be4ddc1c72d9aa70fb" + ], + "markers": "platform_python_implementation != 'pypy'", + "version": "==1.11.5" + }, + "constantly": { + "hashes": [ + "sha256:586372eb92059873e29eba4f9dec8381541b4d3834660707faf8ba59146dfc35", + "sha256:dd2fa9d6b1a51a83f0d7dd76293d734046aa176e384bf6e33b7e44880eb37c5d" + ], + "version": "==15.1.0" + }, + "cryptography": { + "hashes": [ + "sha256:3f3b65d5a16e6b52fba63dc860b62ca9832f51f1a2ae5083c78b6840275f12dd", + "sha256:551a3abfe0c8c6833df4192a63371aa2ff43afd8f570ed345d31f251d78e7e04", + "sha256:5cb990056b7cadcca26813311187ad751ea644712022a3976443691168781b6f", + "sha256:60bda7f12ecb828358be53095fc9c6edda7de8f1ef571f96c00b2363643fa3cd", + "sha256:6fef51ec447fe9f8351894024e94736862900d3a9aa2961528e602eb65c92bdb", + "sha256:77d0ad229d47a6e0272d00f6bf8ac06ce14715a9fd02c9a97f5a2869aab3ccb2", + "sha256:808fe471b1a6b777f026f7dc7bd9a4959da4bfab64972f2bbe91e22527c1c037", + "sha256:9b62fb4d18529c84b961efd9187fecbb48e89aa1a0f9f4161c61b7fc42a101bd", + "sha256:9e5bed45ec6b4f828866ac6a6bedf08388ffcfa68abe9e94b34bb40977aba531", + "sha256:9fc295bf69130a342e7a19a39d7bbeb15c0bcaabc7382ec33ef3b2b7d18d2f63", + "sha256:abd070b5849ed64e6d349199bef955ee0ad99aefbad792f0c587f8effa681a5e", + "sha256:ba6a774749b6e510cffc2fb98535f717e0e5fd91c7c99a61d223293df79ab351", + "sha256:c332118647f084c983c6a3e1dba0f3bcb051f69d12baccac68db8d62d177eb8a", + "sha256:d6f46e862ee36df81e6342c2177ba84e70f722d9dc9c6c394f9f1f434c4a5563", + "sha256:db6013746f73bf8edd9c3d1d3f94db635b9422f503db3fc5ef105233d4c011ab", + "sha256:f57008eaff597c69cf692c3518f6d4800f0309253bb138b526a37fe9ef0c7471", + "sha256:f6c821ac253c19f2ad4c8691633ae1d1a17f120d5b01ea1d256d7b602bc59887" + ], + "version": "==2.2.2" + }, + "e1839a8": { + "editable": true, + "path": "." + }, + "hyperlink": { + "hashes": [ + "sha256:98da4218a56b448c7ec7d2655cb339af1f7d751cf541469bb4fc28c4a4245b34", + "sha256:f01b4ff744f14bc5d0a22a6b9f1525ab7d6312cb0ff967f59414bbac52f0a306" + ], + "version": "==18.0.0" + }, + "idna": { + "hashes": [ + "sha256:2c6a5de3089009e3da7c5dde64a141dbc8551d5b7f6cf4ed7c2568d0cc520a8f", + "sha256:8c7309c718f94b3a625cb648ace320157ad16ff131ae0af362c9f21b80ef6ec4" + ], + "version": "==2.6" + }, + "incremental": { + "hashes": [ + "sha256:717e12246dddf231a349175f48d74d93e2897244939173b01974ab6661406b9f", + "sha256:7b751696aaf36eebfab537e458929e194460051ccad279c72b755a167eebd4b3" + ], + "version": "==17.5.0" + }, + "msgpack-python": { + "hashes": [ + "sha256:378cc8a6d3545b532dfd149da715abae4fda2a3adb6d74e525d0d5e51f46909b" + ], + "version": "==0.5.6" + }, + "pigpio": { + "hashes": [ + "sha256:38bdc1b4d58898e7b4a69de3b0d90e8e8ccd851ac7d03e30791769475ffd941f", + "sha256:be1480aecf7c8aba9c92d2846ae153ad4c7e89ae188b28e4bcc579c3bc30e36b" + ], + "version": "==1.40.post1" + }, + "pyasn1": { + "hashes": [ + "sha256:0d7f6e959fe53f3960a23d73f35e1fce61348b30915b6664309ca756de7c1f89", + "sha256:5a0db897b311d265cde49615cf783f1c78613138605cdd0f907ecfa5b2aba3ee", + "sha256:758cb50abddc03e4563fd9e7f03db56e3e87b58c0bd01247360326e5c0c7ffa5", + "sha256:7d626683e3d792cccc608da02498aff37ab4f3dafd8905d6bf755d11f9b26b43", + "sha256:a7efe807c4b83a859e2735c692b92ed7b567cfddc4163763412920041d876c2b", + "sha256:b5a9ca48055b9a20f6d1b3d68e38692e5431c86a0f99ea602e61294e891fee5b", + "sha256:c07d6e587b2f928366b1f67c09bda026a3e6fcc99e80a744dc67f8fca3895626", + "sha256:d258b0a71994f7770599835249cece1caef3c70def868c4915e6e5ca49b67d15", + "sha256:d5cd6ed995dba16fad0c521cfe31cd2d68400b53fcc2bce93326829be73ab6d1", + "sha256:d84c2aea3cf43780e9e6a19f4e4dddee9f6976519020e64e47c57e5c7a8c3dd2", + "sha256:e85895087905c65b5b594eb91f7522664c85545b147d5f4d4e7b1b07da8dcbdc", + "sha256:f81c96761fca60d64b1c9b79ec2e40cf9495a745cf570613079ef324aeb9672b" + ], + "version": "==0.4.2" + }, + "pyasn1-modules": { + "hashes": [ + "sha256:041e9fbafac548d095f5b6c3b328b80792f006196e15a232b731a83c93d59493", + "sha256:0cdca76a68dcb701fff58c397de0ef9922b472b1cb3ea9695ca19d03f1869787", + "sha256:0cea139045c38f84abaa803bcb4b5e8775ea12a42af10019d942f227acc426c3", + "sha256:0f2e50d20bc670be170966638fa0ae603f0bc9ed6ebe8e97a6d1d4cef30cc889", + "sha256:47fb6757ab78fe966e7c58b2030b546854f78416d653163f0ce9290cf2278e8b", + "sha256:598a6004ec26a8ab40a39ea955068cf2a3949ad9c0030da970f2e1ca4c9f1cc9", + "sha256:72fd8b0c11191da088147c6e4678ec53e573923ecf60b57eeac9e97433e09fc2", + "sha256:854700bbdd01394e2ada9c1bfbd0ed9f5d0c551350dbbd023e88b11d2771ae06", + "sha256:af00ea8f2022b6287dc375b2c70f31ab5af83989fc6fe9eacd4976ce26cd7ccc", + "sha256:b1f395cae2d669e0830cb023aa86f9f283b7a9aa32317d7f80d8e78aa2745812", + "sha256:c6747146e95d2b14cc2a8399b2b0bde3f93778f8f9ec704690d2b589c376c137", + "sha256:f53fe5bcebdf318f51399b250fe8325ef3a26d927f012cc0c8e0f9e9af7f9deb" + ], + "version": "==0.2.1" + }, + "pycparser": { + "hashes": [ + "sha256:99a8ca03e29851d96616ad0404b4aad7d9ee16f25c9f9708a11faf2810f7b226" + ], + "version": "==2.18" + }, + "pyopenssl": { + "hashes": [ + "sha256:07a2de1a54de07448732a81e38a55df7da109b2f47f599f8bb35b0cbec69d4bd", + "sha256:2c10cfba46a52c0b0950118981d61e72c1e5b1aac451ca1bc77de1a679456773" + ], + "version": "==17.5.0" + }, + "service-identity": { + "hashes": [ + "sha256:0e76f3c042cc0f5c7e6da002cf646f59dc4023962d1d1166343ce53bdad39e17", + "sha256:4001fbb3da19e0df22c47a06d29681a398473af4aa9d745eca525b3b2c2302ab" + ], + "version": "==17.0.0" + }, + "six": { + "hashes": [ + "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", + "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" + ], + "version": "==1.11.0" + }, + "twisted": { + "hashes": [ + "sha256:0da1a7e35d5fcae37bc9c7978970b5feb3bc82822155b8654ec63925c05af75c", + "sha256:716805e624f9396fcc1f47e8aef68e629fd31599a74855b6e1636122c042458d", + "sha256:7bc3cdfd1ca5e5b84c7936db3c2cb2feb7d5b77410e713fd346da095a3b6a1d2" + ], + "version": "==17.9.0" + }, + "zope.interface": { + "hashes": [ + "sha256:21506674d30c009271fe68a242d330c83b1b9d76d62d03d87e1e9528c61beea6", + "sha256:3d184aff0756c44fff7de69eb4cd5b5311b6f452d4de28cb08343b3f21993763", + "sha256:467d364b24cb398f76ad5e90398d71b9325eb4232be9e8a50d6a3b3c7a1c8789", + "sha256:57c38470d9f57e37afb460c399eb254e7193ac7fb8042bd09bdc001981a9c74c", + "sha256:9ada83f4384bbb12dedc152bcdd46a3ac9f5f7720d43ac3ce3e8e8b91d733c10", + "sha256:a1daf9c5120f3cc6f2b5fef8e1d2a3fb7bbbb20ed4bfdc25bc8364bc62dcf54b", + "sha256:e6b77ae84f2b8502d99a7855fa33334a1eb6159de45626905cb3e454c023f339", + "sha256:e881ef610ff48aece2f4ee2af03d2db1a146dc7c705561bd6089b2356f61641f", + "sha256:f41037260deaacb875db250021fe883bf536bf6414a4fd25b25059b02e31b120" + ], + "version": "==4.5.0" + } + } +} diff --git a/clock-test.py b/clock-test.py new file mode 100755 index 0000000..0945424 --- /dev/null +++ b/clock-test.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 + +import time +import supcon.intf +import supcon.core +import supcon.clone + +from twisted.internet import reactor +from twisted.internet.endpoints import TCP4ServerEndpoint + +""" the interface com.example.Clock """ +ComExampleClockIntf = supcon.intf.DInterface.load({ + 'name': 'com.example.Clock', + 'events': [ + { + 'name': 'tick', + 'args': [ + { 'name': 'time', 'description': 'The current time' } + ], + 'description': 'Fires every second' + } + ] +}) + +""" an implementation of the interface com.example.Clock """ +class ComExampleClockImpl(supcon.intf.Implementation): + + def __init__(self, reactor): + super().__init__(ComExampleClockIntf) + + self.__reactor = reactor + reactor.callLater(1, self.__tick) + + def __tick(self): + t = time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime()) + self.fire('tick', { 'time': t }) + self.__reactor.callLater(1, self.__tick) + +""" create the implementation instanz """ +clockImpl = ComExampleClockImpl(reactor) + +""" create the local node """ +local = supcon.core.Node('clock-test') + +""" register the clock instance """ +def register(): + if register.registered: + local.unregister('/clock5', clockImpl) + else: + local.register('/clock5', clockImpl) + register.registered = not register.registered + reactor.callLater(5, register) +register.registered = False + +register() +local.register('/clock', clockImpl) + +supcon.clone.Cloner(local, '/clone5', local.name, '/clock5', 'com.example.Clock') + +local.listen(TCP4ServerEndpoint(reactor, 8123)) + +reactor.run() diff --git a/doc-pdoc/css.mako b/doc-pdoc/css.mako new file mode 100644 index 0000000..cda0e7b --- /dev/null +++ b/doc-pdoc/css.mako @@ -0,0 +1,283 @@ +<%def name="pdoc()"> + .CONTENT { + padding: 0; + } + + #container { + display: flex; + flex-directtion: column; + } + + #content { + padding: 30px; + border-left: 1px solid #ddd; + } + #sidebar { + padding: 30px; + } + + #footer { + font-size: .75em; + padding: 5px 30px; + border-top: 1px solid #ddd; + text-align: right; + } + #footer p { + margin: 0 0 0 30px; + display: inline-block; + } + + h1, h2, h3, h4, h5 { + font-weight: 300; + } + h1 { + font-size: 2.5em; + line-height: 1.1em; + margin: 0 0 .50em 0; + } + + h2 { + font-size: 1.75em; + margin: 1em 0 .50em 0; + } + + h3 { + margin: 25px 0 10px 0; + } + + h4 { + margin: 0; + font-size: 105%; + } + + a { + color: #058; + text-decoration: none; + transition: color .3s ease-in-out; + } + + a:hover { + color: #e08524; + transition: color .3s ease-in-out; + } + + pre, code, .mono, .name { + font-family: "Ubuntu Mono", "Cousine", "DejaVu Sans Mono", monospace; + } + + .title .name { + font-weight: bold; + } + .section-title { + margin-top: 2em; + } + .ident { + color: #900; + } + + code { + background: #f9f9f9; + } + + pre { + background: #fefefe; + border: 1px solid #ddd; + box-shadow: 2px 2px 0 #f3f3f3; + margin: 0 30px; + padding: 15px 30px; + } + + .codehilite { + margin: 0 30px 10px 30px; + } + + .codehilite pre { + margin: 0; + } + .codehilite .err { background: #ff3300; color: #fff !important; } + + table#module-list { + font-size: 110%; + } + + table#module-list tr td:first-child { + padding-right: 10px; + white-space: nowrap; + } + + table#module-list td { + vertical-align: top; + padding-bottom: 8px; + } + + table#module-list td p { + margin: 0 0 7px 0; + } + + .def { + display: table; + } + + .def p { + display: table-cell; + vertical-align: top; + text-align: left; + } + + .def p:first-child { + white-space: nowrap; + } + + .def p:last-child { + width: 100%; + } + + + #index { + list-style-type: none; + margin: 0; + padding: 0; + } + ul#index .class_name { + /* font-size: 110%; */ + font-weight: bold; + } + #index ul { + margin: 0; + } + + .item { + margin: 0 0 15px 0; + } + + .item .class { + margin: 0 0 25px 30px; + } + + .item .class ul.class_list { + margin: 0 0 20px 0; + } + + .item .name { + background: #fafafa; + margin: 0; + font-weight: bold; + padding: 5px 10px; + border-radius: 3px; + display: inline-block; + min-width: 40%; + } + .item .name:hover { + background: #f6f6f6; + } + + .item .empty_desc { + margin: 0 0 5px 0; + padding: 0; + } + + .item .inheritance { + margin: 3px 0 0 30px; + } + + .item .inherited { + color: #666; + } + + .item .desc { + padding: 0 8px; + margin: 0; + } + + .item .desc p { + margin: 0 0 10px 0; + } + + .source_cont { + margin: 0; + padding: 0; + } + + .source_link a { + background: #ffc300; + font-weight: 400; + font-size: .75em; + text-transform: uppercase; + color: #fff; + text-shadow: 1px 1px 0 #f4b700; + + padding: 3px 8px; + border-radius: 2px; + transition: background .3s ease-in-out; + } + .source_link a:hover { + background: #FF7200; + text-shadow: none; + transition: background .3s ease-in-out; + } + + .source { + display: none; + max-height: 600px; + overflow-y: scroll; + margin-bottom: 15px; + } + + .source .codehilite { + margin: 0; + } + + .desc h1, .desc h2, .desc h3 { + font-size: 100% !important; + } + .clear { + clear: both; + } + + @media all and (max-width: 950px) { + #sidebar { + width: 35%; + } + #content { + width: 65%; + } + } + @media all and (max-width: 650px) { + #top { + display: none; + } + #sidebar { + float: none; + width: auto; + } + #content { + float: none; + width: auto; + padding: 30px; + } + + #index ul { + padding: 0; + margin-bottom: 15px; + } + #index ul li { + display: inline-block; + margin-right: 30px; + } + #footer { + text-align: left; + } + #footer p { + display: block; + margin: inherit; + } + } + + /*****************************/ + + +<%def name="pre()"> + + +<%def name="post()"> + diff --git a/doc-pdoc/html.mako b/doc-pdoc/html.mako new file mode 100644 index 0000000..ee7fffe --- /dev/null +++ b/doc-pdoc/html.mako @@ -0,0 +1,513 @@ +<% + import re + import sys + + import markdown + try: + import pygments + import pygments.formatters + import pygments.lexers + use_pygments = True + except ImportError: + use_pygments = False + + import pdoc + + # From language reference, but adds '.' to allow fully qualified names. + pyident = re.compile('^[a-zA-Z_][a-zA-Z0-9_.]+$') + indent = re.compile('^\s*') + + # Whether we're showing the module list or a single module. + module_list = 'modules' in context.keys() + + pdoc.html_module_suffix = '.m.md' + pdoc.html_package_name = 'index.md' + + link_prefix = '/screwerk/' + + def decode(s): + if sys.version_info[0] < 3 and isinstance(s, str): + return s.decode('utf-8', 'ignore') + return s + + def ident(s): + return '%s' % s + + def sourceid(dobj): + return 'source-%s' % dobj.refname + + def clean_source_lines(lines): + """ + Cleans the source code so that pygments can render it well. + + Returns one string with all of the source code. + """ + base_indent = len(indent.match(lines[0]).group(0)) + base_indent = 0 + for line in lines: + if len(line.strip()) > 0: + base_indent = len(indent.match(lines[0]).group(0)) + break + lines = [line[base_indent:] for line in lines] + if not use_pygments: # :-( + return '
%s
' % (''.join(lines)) + + pylex = pygments.lexers.PythonLexer() + htmlform = pygments.formatters.HtmlFormatter(cssclass='codehilite') + return pygments.highlight(''.join(lines), pylex, htmlform) + + def linkify(match): + matched = match.group(0) + ident = matched[1:-1] + name, url = lookup(ident) + if name is None: + return matched + return '[`%s`](%s)' % (name, url) + + def mark(s, linky=True): + if linky: + s, _ = re.subn('\b\n\b', ' ', s) + if not module_list: + s, _ = re.subn('`[^`]+`', linkify, s) + + extensions = [] + if use_pygments: + extensions = ['markdown.extensions.codehilite(linenums=False)'] + s = markdown.markdown(s.strip(), extensions=extensions) + return s + + def glimpse(s, length=100): + if len(s) < length: + return s + return s[0:length] + '...' + + def module_url(m): + """ + Returns a URL for `m`, which must be an instance of `Module`. + Also, `m` must be a submodule of the module being documented. + + Namely, '.' import separators are replaced with '/' URL + separators. Also, packages are translated as directories + containing `index.html` corresponding to the `__init__` module, + while modules are translated as regular HTML files with an + `.m.html` suffix. (Given default values of + `pdoc.html_module_suffix` and `pdoc.html_package_name`.) + """ + #if module.name == m.name: + # return '' + + if len(link_prefix) > 0: + base = m.name + else: + base = m.name[len(module.name)+1:] + url = base.replace('.', '/') + if m.is_package(): + url += '/%s' % pdoc.html_package_name + else: + url += pdoc.html_module_suffix + return link_prefix + url + + def external_url(refname): + """ + Attempts to guess an absolute URL for the external identifier + given. + + Note that this just returns the refname with an ".ext" suffix. + It will be up to whatever is interpreting the URLs to map it + to an appropriate documentation page. + """ + return '/%s.ext' % refname + + def is_external_linkable(name): + return external_links and pyident.match(name) and '.' in name + + def lookup(refname): + """ + Given a fully qualified identifier name, return its refname + with respect to the current module and a value for a `href` + attribute. If `refname` is not in the public interface of + this module or its submodules, then `None` is returned for + both return values. (Unless this module has enabled external + linking.) + + In particular, this takes into account sub-modules and external + identifiers. If `refname` is in the public API of the current + module, then a local anchor link is given. If `refname` is in the + public API of a sub-module, then a link to a different page with + the appropriate anchor is given. Otherwise, `refname` is + considered external and no link is used. + """ + d = module.find_ident(refname) + if isinstance(d, pdoc.External): + if is_external_linkable(refname): + return d.refname, external_url(d.refname) + else: + return None, None + if isinstance(d, pdoc.Module): + return d.refname, module_url(d) + if module.is_public(d.refname): + #return d.name, '#%s' % d.refname + return d.name, '%s#%s' % (module_url(d.module), d.refname) + return d.refname, '%s#%s' % (module_url(d.module), d.refname) + + def link(refname): + """ + A convenience wrapper around `href` to produce the full + `a` tag if `refname` is found. Otherwise, plain text of + `refname` is returned. + """ + name, url = lookup(refname) + if name is None: + return refname + return '%s' % (url, name) + + def lookupObj(cls, name): + for pCls in cls.__mro__: + pVars = vars(pCls) + if name in pVars: + return pVars[name] + + def isInstMethod(o): + if not isinstance(o, pdoc.Function): + return False + if not o.cls or not o.cls.cls: + return False + obj = lookupObj(o.cls.cls, o.name) + if isinstance(obj, classmethod): + return False + if isinstance(obj, staticmethod): + return False + return True + + def isClassMethod(o): + if not isinstance(o, pdoc.Function): + return False + if not o.cls or not o.cls.cls: + return False + obj = lookupObj(o.cls.cls, o.name) + if not isinstance(obj, classmethod): + return False + return True + + def isStaticMethod(o): + if not isinstance(o, pdoc.Function): + return False + if not o.cls or not o.cls.cls: + return False + obj = lookupObj(o.cls.cls, o.name) + if not isinstance(obj, staticmethod): + return False + return True + + def cInstMethods(c): + p = lambda o: isInstMethod(o) and c.module._docfilter(o) + return sorted(filter(p, c.doc.values())) + + def cClassMethods(c): + p = lambda o: isClassMethod(o) and c.module._docfilter(o) + return sorted(filter(p, c.doc.values())) + + def cStaticMethods(c): + p = lambda o: isStaticMethod(o) and c.module._docfilter(o) + return sorted(filter(p, c.doc.values())) + +%> +<%def name="show_source(d)"> + % if show_source_code and d.source is not None and len(d.source) > 0: + +
+ ${decode(clean_source_lines(d.source))} +
+ % endif + + +<%def name="show_desc(d, limit=None)"> + <% + inherits = (hasattr(d, 'inherits') + and (len(d.docstring) == 0 + or d.docstring == d.inherits.docstring)) + docstring = (d.inherits.docstring if inherits else d.docstring).strip() + if limit is not None: + docstring = glimpse(docstring, limit) + %> + % if len(docstring) > 0: + % if inherits: +
${docstring | mark}
+ % else: +
${docstring | mark}
+ % endif + % endif + % if not isinstance(d, pdoc.Module): +
${show_source(d)}
+ % endif + + +<%def name="show_inheritance(d)"> + % if hasattr(d, 'inherits'): +

+ Inheritance: + % if hasattr(d.inherits, 'cls'): + ${link(d.inherits.cls.refname)}.${link(d.inherits.refname)} + % else: + ${link(d.inherits.refname)} + % endif +

+ % endif + + +<%def name="show_module_list(modules)"> +

Python module list

+ +% if len(modules) == 0: +

No modules found.

+% else: + + % for name, desc in modules: + + + + + % endfor +
${name} + % if len(desc.strip()) > 0: +
${desc | mark}
+ % endif +
+% endif + + +<%def name="show_column_list(items, numcols=3)"> + + + +<%def name="show_module(module)"> + <% + variables = module.variables() + classes = module.classes() + functions = module.functions() + submodules = module.submodules() + %> + + <%def name="show_func(f)"> +
+
+

def ${ident(f.name)}(

${f.spec() | h})

+
+ ${show_inheritance(f)} + ${show_desc(f)} +
+ + + % if 'http_server' in context.keys() and http_server: + + % endif + +
+

${module.name} module

+ ${module.docstring | mark} + ${show_source(module)} +
+ +
+ % if len(variables) > 0: +

Module variables

+ % for v in variables: +
+

var ${ident(v.name)}

+ ${show_desc(v)} +
+ % endfor + % endif + + % if len(functions) > 0: +

Functions

+ % for f in functions: + ${show_func(f)} + % endfor + % endif + + % if len(classes) > 0: +

Classes

+ % for c in classes: + <% + class_vars = c.class_variables() + inst_vars = c.instance_variables() + instMethods = cInstMethods(c) + classMethods = cClassMethods(c) + staticMethods = cStaticMethods(c) + mro = c.module.mro(c) + %> +
+

class ${ident(c.name)}

+ ${show_desc(c)} + +
+ % if len(mro) > 0: +

Ancestors (in MRO)

+
    + % for cls in mro: +
  • ${link(cls.refname)}
  • + % endfor +
+ % endif + % if len(staticMethods) > 0: +

Static Methods

+ % for f in staticMethods: + ${show_func(f)} + % endfor + % endif + % if len(class_vars) > 0: +

Class variables

+ % for v in class_vars: +
+

var ${ident(v.name)}

+ ${show_inheritance(v)} + ${show_desc(v)} +
+ % endfor + % endif + % if len(classMethods) > 0: +

Class Methods

+ % for f in classMethods: + ${show_func(f)} + % endfor + % endif + % if len(inst_vars) > 0: +

Instance variables

+ % for v in inst_vars: +
+

var ${ident(v.name)}

+ ${show_inheritance(v)} + ${show_desc(v)} +
+ % endfor + % endif + % if len(instMethods) > 0: +

Methods

+ % for f in instMethods: + ${show_func(f)} + % endfor + % endif +
+
+ % endfor + % endif + + % if len(submodules) > 0: +

Sub-modules

+ % for m in submodules: +
+

${link(m.refname)}

+ ${show_desc(m, limit=300)} +
+ % endfor + % endif +
+ + +<%def name="module_index(module)"> + <% + variables = module.variables() + classes = module.classes() + functions = module.functions() + submodules = module.submodules() + %> + + + + +<%namespace name="css" file="css.mako" /> + + + +% if use_pygments: + +% endif + + + +
+ % if module_list: +
+ ${show_module_list(modules)} +
+ % else: + ${module_index(module)} +
+ ${show_module(module)} +
+ % endif +
diff --git a/doc-pdoc/text.mako b/doc-pdoc/text.mako new file mode 100644 index 0000000..1040c89 --- /dev/null +++ b/doc-pdoc/text.mako @@ -0,0 +1,162 @@ +## Define mini-templates for each portion of the doco. + +<% + import re + import pdoc + + def indent(s, spaces=4): + """ + Inserts `spaces` after each string of new lines in `s` + and before the start of the string. + """ + new = re.sub('(\n+)', '\\1%s' % (' ' * spaces), s) + return (' ' * spaces) + new.strip() + + def docstring(d): + if len(d.docstring) == 0 and hasattr(d, 'inherits'): + return d.inherits.docstring + else: + return d.docstring + + def submoduleRef(m): + ref = './{}' if m.is_package() else './{}.m.md' + return ref.format(m.name.split('.')[-1]) + + def header(n, header): + return '#' * n + ' {}'.format(header) + + def url(d, suffix = None): + if d.refname[0:len(module.refname)] != module.refname: + raise ValueError(d.refname) + url = '#' + d.refname[len(module.refname) + 1:] + url+= '-{}'.format(suffix) if suffix else '' + return url + + def target(d, suffix = None): + url = d.refname[len(module.refname) + 1:] + url+= '-{}'.format(suffix) if suffix else '' + return ''.format(url) + + def linkTo(label, d, suffix = None): + if isinstance(d, pdoc.External): + return label + return '[{}]({})'.format(label, url(d, suffix)) +%> + +<%def name="function(func)" filter="trim"> +${func.name}(${func.spec()}) +${docstring(func) | indent} + + +<%def name="variable(var)" filter="trim"> +${var.name} +${docstring(var) | indent} + + +<%def name="class_(cls)" filter="trim"> +${target(cls)} +${header(3, cls.name)} +% if len(cls.docstring) > 0: + +${cls.docstring} +% endif +<% + class_vars = cls.class_variables() + static_methods = cls.functions() + inst_vars = cls.instance_variables() + methods = cls.methods() + mro = cls.module.mro(cls) + descendents = cls.module.descendents(cls) +%> +% if len(mro) > 0: +${header(4, 'Ancestors (in MRO)')} +% for c in mro: +* ${linkTo(c.refname, c)} +% endfor + +% endif +% if len(descendents) > 0: +${header(4, 'Descendents')} +% for c in descendents: +* ${linkTo(c.refname, c)} +% endfor + +% endif +% if len(class_vars) > 0: +${header(4, 'Class variables')} + % for v in class_vars: +${capture(variable, v) | indent} + + % endfor +% endif +% if len(static_methods) > 0: +${header(4, 'Static methods')} + % for f in static_methods: +${capture(function, f) | indent} + + % endfor +% endif +% if len(inst_vars) > 0: +${header(4, 'Instance variables')} + % for v in inst_vars: +${variable(v)} + + % endfor +% endif +% if len(methods) > 0: +${header(4, 'Methods')} + % for m in methods: +${capture(function, m) | indent} + + % endfor +% endif + + +## Start the output logic for an entire module. + +<% + variables = module.variables() + classes = module.classes() + functions = module.functions() + submodules = module.submodules() +%> + +${header(1, 'Module '+module.name)} +% if not module._filtering: +${module.docstring} +% endif + + +% if len(variables) > 0: +${header(2, 'Variables')} + % for v in variables: +${variable(v)} + + % endfor +% endif + + +% if len(functions) > 0: +${header(2, 'Functions')} + % for f in functions: +${function(f)} + + % endfor +% endif + + +% if len(classes) > 0: +${header(2, 'Classes')} + % for c in classes: +${class_(c)} + + % endfor +% endif + + +% if len(submodules) > 0: +${header(2, 'Sub-modules')} +% for m in submodules: +[${m.name}](${submoduleRef(m)}) +% endfor +% endif diff --git a/doc/clone.m.md b/doc/clone.m.md new file mode 100644 index 0000000..4704df0 --- /dev/null +++ b/doc/clone.m.md @@ -0,0 +1,679 @@ + + + + + + + + + +
+ + + + +
+ + + + + + +
+

supcon.clone module

+ + + +
+
# -*- coding: utf-8 -*-
+
+import supcon.intf
+
+class _Clone(supcon.intf.Implementation):
+
+  def __init__(self, local, rnode, rpath, interface):
+    super().__init__(interface)
+
+    self.__local = local
+    self.__rnode = rnode
+    self.__rpath = rpath
+
+    for event in self.interface.events:
+      self.__local.on(self.__rnode, self.__rpath, self.intf, event, self.__on)
+
+  def __del__(self):
+    for event in self.interface.events:
+      self.__local.off(self.__rnode, self.__rpath, self.intf, event, self.__on)
+
+  def __on(self, args: dict, event):
+    (_Node, _Path, _Intf, eEvent) = event
+    self.fire(eEvent, args)
+
+  def call(self, method, inArgs):
+    #print('_Clone.call', method, inArgs)
+    return self.__local.call(self.__rnode, self.__rpath, self.intf, method, inArgs)
+
+class Cloner(object):
+
+  def __init__(self, local: supcon.intf.Node, lPath: str, rNode: str, rPath: str, rIntf: str):
+    #print('Cloner.__init__')
+    self.__local = local
+    self.__lPath = lPath
+
+    self.__rNode = rNode
+    self.__rPath = rPath
+    self.__rIntf = rIntf
+
+    self.__impl = None
+
+    self.__local.on(self.__local.name, '/', 'supcon.Local', 'intf', self.__onIntf)
+    self.__local.on(self.__local.name, '/', 'supcon.Local', 'intfLost', self.__onIntf)
+
+    nodes = local.nodes()
+    if nodes.hasIntf(rNode, rPath, rIntf):
+      self.__setupImpl(nodes[rNode][rPath][rIntf])
+
+  @property
+  def local(self) -> supcon.intf.Node:
+    return self.__local
+
+  @property
+  def lPath(self) -> str:
+    return self.__lPath
+
+  @property
+  def rNode(self) -> str:
+    return self.__rNode
+
+  @property
+  def rPath(self) -> str:
+    return self.__rPath
+
+  @property
+  def rIntf(self) -> str:
+    return self.__rIntf
+
+  def __onIntf(self, args, event):
+    if args['node'] != self.__rNode or args['path'] != self.__rPath:
+      return
+    aInterface = supcon.intf.DInterface.load(args['intf'])
+    if aInterface.name != self.__rIntf:
+      return
+
+    if event[3] == 'intf':
+      self.__setupImpl(aInterface)
+    else:
+      self.__teardownImpl()
+
+  def __setupImpl(self, interface):
+    #print('Cloner.__setupImpl')
+    self.__impl = _Clone(self.__local, self.__rNode, self.__rPath, interface)
+    self.__local.register(self.__lPath, self.__impl)
+
+  def __teardownImpl(self):
+    self.__local.unregister(self.__lPath, self.__impl)
+    self.__impl = None
+
+ +
+ +
+ +
+ + +

Classes

+ +
+

class Cloner

+ + +
+ +
+
class Cloner(object):
+
+  def __init__(self, local: supcon.intf.Node, lPath: str, rNode: str, rPath: str, rIntf: str):
+    #print('Cloner.__init__')
+    self.__local = local
+    self.__lPath = lPath
+
+    self.__rNode = rNode
+    self.__rPath = rPath
+    self.__rIntf = rIntf
+
+    self.__impl = None
+
+    self.__local.on(self.__local.name, '/', 'supcon.Local', 'intf', self.__onIntf)
+    self.__local.on(self.__local.name, '/', 'supcon.Local', 'intfLost', self.__onIntf)
+
+    nodes = local.nodes()
+    if nodes.hasIntf(rNode, rPath, rIntf):
+      self.__setupImpl(nodes[rNode][rPath][rIntf])
+
+  @property
+  def local(self) -> supcon.intf.Node:
+    return self.__local
+
+  @property
+  def lPath(self) -> str:
+    return self.__lPath
+
+  @property
+  def rNode(self) -> str:
+    return self.__rNode
+
+  @property
+  def rPath(self) -> str:
+    return self.__rPath
+
+  @property
+  def rIntf(self) -> str:
+    return self.__rIntf
+
+  def __onIntf(self, args, event):
+    if args['node'] != self.__rNode or args['path'] != self.__rPath:
+      return
+    aInterface = supcon.intf.DInterface.load(args['intf'])
+    if aInterface.name != self.__rIntf:
+      return
+
+    if event[3] == 'intf':
+      self.__setupImpl(aInterface)
+    else:
+      self.__teardownImpl()
+
+  def __setupImpl(self, interface):
+    #print('Cloner.__setupImpl')
+    self.__impl = _Clone(self.__local, self.__rNode, self.__rPath, interface)
+    self.__local.register(self.__lPath, self.__impl)
+
+  def __teardownImpl(self):
+    self.__local.unregister(self.__lPath, self.__impl)
+    self.__impl = None
+
+ +
+
+ + +
+

Ancestors (in MRO)

+
    +
  • Cloner
  • +
  • builtins.object
  • +
+

Instance variables

+
+

var lPath

+ + + + +
+
+ +
+
+

var local

+ + + + +
+
+ +
+
+

var rIntf

+ + + + +
+
+ +
+
+

var rNode

+ + + + +
+
+ +
+
+

var rPath

+ + + + +
+
+ +
+

Methods

+ +
+
+

def __init__(

self, local, lPath, rNode, rPath, rIntf)

+
+ + + + +

Initialize self. See help(type(self)) for accurate signature.

+
+ +
+
def __init__(self, local: supcon.intf.Node, lPath: str, rNode: str, rPath: str, rIntf: str):
+  #print('Cloner.__init__')
+  self.__local = local
+  self.__lPath = lPath
+  self.__rNode = rNode
+  self.__rPath = rPath
+  self.__rIntf = rIntf
+  self.__impl = None
+  self.__local.on(self.__local.name, '/', 'supcon.Local', 'intf', self.__onIntf)
+  self.__local.on(self.__local.name, '/', 'supcon.Local', 'intfLost', self.__onIntf)
+  nodes = local.nodes()
+  if nodes.hasIntf(rNode, rPath, rIntf):
+    self.__setupImpl(nodes[rNode][rPath][rIntf])
+
+ +
+
+ +
+ +
+
+ +
+ +
+
diff --git a/doc/core.m.md b/doc/core.m.md new file mode 100644 index 0000000..a851689 --- /dev/null +++ b/doc/core.m.md @@ -0,0 +1,1988 @@ + + + + + + + + + +
+ + + + +
+ + + + + + +
+

supcon.core module

+

This module provides the implementation of abstract class supcon.intf.Node.

+ + +
+
# -*- coding: utf-8 -*-
+"""
+This module provides the implementation of abstract class `supcon.intf.Node`.
+"""
+
+import sys
+import traceback
+
+import uuid
+import struct
+import msgpack
+
+import twisted.internet.defer as defer
+import twisted.internet.protocol
+import twisted.internet.endpoints
+
+import twisted.application.service
+import twisted.application.internet
+
+import supcon.intf
+
+_printArgs = lambda *args: print(', '.join(['{}'.format(i) for i in args]))
+
+LocalIntf = supcon.intf.DInterface.load({
+  'name': 'supcon.Local',
+  'events': [
+    {
+      'name': 'node',
+      'args': [
+        {'name': 'node', 'description': 'The node'},
+      ],
+    },
+    {
+      'name': 'nodeLost',
+      'args': [
+        {'name': 'node', 'description': 'The lost node'},
+      ],
+    },
+    {
+      'name': 'path',
+      'args': [
+        {'name': 'node', 'description': 'The node'},
+        {'name': 'path', 'description': 'The path'},
+      ],
+    },
+    {
+      'name': 'pathLost',
+      'args': [
+        {'name': 'node', 'description': 'The node'},
+        {'name': 'path', 'description': 'The path'},
+      ],
+    },
+    {
+      'name': 'intf',
+      'args': [
+        {'name': 'node', 'description': 'The node'},
+        {'name': 'path', 'description': 'The path'},
+        {'name': 'intf', 'description': 'The interface'},
+      ],
+    },
+    {
+      'name': 'intfLost',
+      'args': [
+        {'name': 'node', 'description': 'The node'},
+        {'name': 'path', 'description': 'The path'},
+        {'name': 'intf', 'description': 'The interface'},
+      ],
+    },
+  ],
+  'methods': [
+    {
+      'name': 'nodes',
+      'outArgs': [
+        {'name': 'nodes', 'description': 'A datastructure about all known nodes, paths and interfaces'},
+      ],
+    },
+  ],
+  'description': 'All methods and events are only locally available'
+})
+
+RemoteIntf = supcon.intf.DInterface.load({
+  'name': 'supcon.Remote',
+  'events': [
+    {
+      'name': 'intf',
+      'args': [
+        {'name': 'path', 'description': 'The path'},
+        {'name': 'intf', 'description': 'The interface'},
+      ],
+    },
+    {
+      'name': 'intfLost',
+      'args': [
+        {'name': 'path', 'description': 'The path'},
+        {'name': 'intf', 'description': 'The interface'},
+      ],
+    },
+  ],
+  'methods': [
+    {
+      'name': 'node',
+      'outArgs': [
+        {'name': 'node', 'description': 'A datastructure about all paths and interfaces of the node'},
+      ],
+    },
+    {
+      'name': 'on',
+      'inArgs': [
+        {'name': 'path', 'description': 'The path'},
+        {'name': 'intf', 'description': 'The interface'},
+        {'name': 'event', 'description': 'The event'},
+      ],
+    },
+    {
+      'name': 'off',
+      'inArgs': [
+        {'name': 'path', 'description': 'The path'},
+        {'name': 'intf', 'description': 'The interface'},
+        {'name': 'event', 'description': 'The event'},
+      ],
+    },
+  ],
+  'description': 'All methods and events are only used internally'
+})
+
+
+class _Protocol(twisted.internet.protocol.Protocol):
+  """This class handels handshake and message encoding between nodes on the bus.
+
+  This class is just an implementation detail of the class Node.
+  """
+  def __init__(self):
+    super().__init__()
+    self.factory = None
+
+    self.__data = bytearray()
+    self.__name = None
+
+  def connectionMade(self):
+    self.sendMesg({
+      'type': 'handshake',
+      'name': self.factory.name
+    })
+
+  def connectionLost(self, reason):
+    if not self.__name:
+      return
+    self.factory.delNode(self.__name)
+
+  def dataReceived(self, data):
+    self.__data.extend(data)
+
+    while len(self.__data) > 4:
+
+      size = struct.unpack_from('<L', self.__data)[0]
+      if len(self.__data) < size + 4:
+        break
+
+      mesg = self.__data[4:size + 4]
+      self.__data = self.__data[size + 4:]
+
+      try:
+        mesg = msgpack.unpackb(mesg, encoding='utf-8')
+      except BaseException as _:
+        self.transport.loseConnection()
+      else:
+        self.recvMesg(mesg)
+
+  def recvMesg(self, mesg):
+    if not self.__name:
+      if mesg['type'] != 'handshake':
+        self.transport.loseConnection()
+        return
+
+      self.__name = mesg['name']
+      self.factory.addNode(self.__name, self)
+    else:
+      self.factory.recvMesg(self.__name, mesg)
+
+  def sendMesg(self, mesg):
+    data = msgpack.packb(mesg, use_bin_type=True)
+    size = struct.pack('<L', len(data))
+
+    self.transport.write(size)
+    self.transport.write(data)
+
+
+class _Factory(twisted.internet.protocol.Factory):
+  """This class creates _Protocol instances for connections to remote nodes.
+
+  This class is just an implementation detail of the class Node and encapulates
+  methods to create the _Protocol instances resulting from calls to
+  Node.connect() and Node.listen().
+  """
+  protocol = _Protocol
+
+  def __init__(self, name, addNode, delNode, recvMesg):
+    self.name = name
+    self.addNode = addNode
+    self.delNode = delNode
+    self.recvMesg = recvMesg
+
+
+class _KnownMgr(object):
+
+  def __init__(self, fire):
+    self.__fire = fire
+
+    self.__nodes = supcon.intf.DNodes()
+
+  def addNode(self, node: str) -> bool:
+    if node in self.__nodes:
+      return True
+
+    self.__nodes = self.__nodes.addNode(node)
+
+    self.__fire('node', {'node': node})
+    return True
+
+  def delNode(self, node: str) -> bool:
+    if node not in self.__nodes:
+      return True
+
+    for path in self.__nodes[node]:
+      for intf in self.__nodes[node][path]:
+        self.delIntf(node, path, self.__nodes[node][path][intf])
+
+    self.__nodes = self.__nodes.delNode(node)
+    self.__fire('nodeLost', {'node': node})
+    return True
+
+  def addIntf(self, node: str, path: str, interface: supcon.intf.DInterface) -> bool:
+    #_printArgs('_KnownMgr.addIntf', node, path, interface)
+    if node not in self.__nodes:
+      return False
+
+    if path not in self.__nodes[node]:
+      self.__nodes = self.__nodes.addPath(node, path)
+      self.__fire('path', {'node': node, 'path': path})
+
+    if interface.name in self.__nodes[node][path]:
+      return True
+
+    self.__nodes = self.__nodes.addInterface(node, path, interface)
+    self.__fire('intf', {'node': node, 'path': path, 'intf': interface.dump()})
+    return True
+
+  def delIntf(self, node: str, path: str, interface: supcon.intf.DInterface) -> bool:
+    #_printArgs('_KnownMgr.delIntf', node, path, interface)
+    if not self.__nodes.hasIntf(node, path, interface.name):
+      return True
+
+    self.__nodes = self.__nodes.delInterface(node, path, interface)
+    self.__fire('intfLost', {'node': node, 'path': path, 'intf': interface.dump()})
+
+    if not self.__nodes[node][path]:
+      self.__nodes = self.__nodes.delPath(node, path)
+      self.__fire('pathLost', {'node': node, 'path': path})
+
+    return True
+
+  def getNode(self, node: str) -> supcon.intf.DNode:
+    if node in self.__nodes:
+      return self.__nodes[node]
+    return None
+
+  def setNode(self, newNode: supcon.intf.DNode):
+    #_printArgs('_KnownMgr.setNode', newNode.dump())
+    oldNode = self.getNode(newNode.name)
+
+    for oldPath in oldNode.values():
+      for oldIntf in oldPath.values():
+        if not newNode.hasIntf(oldPath.name, oldIntf.name):
+          self.delIntf(oldNode.name, oldPath.name, oldIntf)
+
+    for newPath in newNode.values():
+      for newIntf in newPath.values():
+        if not oldNode.hasIntf(newPath.name, newIntf.name):
+          self.addIntf(newNode.name, newPath.name, newIntf)
+
+  def getNodes(self) -> supcon.intf.DNodes:
+    return self.__nodes
+
+
+class _InformMgr(object):
+
+  def __init__(self):
+
+    self.__keys = {}
+    self.__nodes = {}
+
+  def on(self, path, intf, event, node):
+    key = (path, intf, event)
+
+    if key not in self.__nodes:
+      self.__nodes[key] = {}
+    self.__nodes[key][node] = True
+
+    if node not in self.__keys:
+      self.__keys[node] = {}
+    self.__keys[node][key] = True
+
+  def off(self, path, intf, event, node):
+    key = (path, intf, event)
+    if key not in self.__nodes or node not in self.__nodes[key]:
+      return
+
+    del self.__nodes[key][node]
+    if not self.__nodes[key]:
+      del self.__nodes[key]
+
+    del self.__keys[node][key]
+    if not self.__keys[node]:
+      del self.__keys[node]
+
+  def nodes(self, path, intf, event):
+    key = (path, intf, event)
+    if key not in self.__nodes:
+      return []
+    return self.__nodes[key].keys()
+
+  def delNode(self, node):
+    if node not in self.__keys:
+      return
+    for key in self.__keys[node]:
+      del self.__nodes[key][node]
+      if not self.__nodes[key]:
+        del self.__nodes[key]
+    del self.__keys[node]
+
+
+class _DeferredMgr(object):
+  """ This class manages Deferred """
+
+  def __init__(self):
+    self.__infos = {}
+
+  def create(self, data=None) -> (str, defer.Deferred):
+    """Creates a Deferred with an info.
+
+    Args:
+      data: Some additional data
+
+    Returns:
+      (str, defer.Deferred): A Tuple of an unique id and a Deferred
+    """
+    pid = str(uuid.uuid4())
+
+    def canceller(_d):
+      del self.__infos[pid]
+
+    d = defer.Deferred(canceller)
+    self.__infos[pid] = (d, data)
+
+    return (pid, d)
+
+  def succeed(self, value, pid):
+    """Succeeds the Deferred identified by the given unique id with the given
+    response.
+
+    Args:
+      pid (str): A unique id of a Deferred created with _DeferredMgr.create()
+      value (mixed): The value to succeed the Deferred with
+    """
+    if pid not in self.__infos:
+      return
+
+    d = self.__infos[pid][0]
+    del self.__infos[pid]
+    d.callback(value)
+
+  def fail(self, reason, pid):
+    """Fail the Deferred identified by the given unique id with the given
+    reason.
+
+    Args:
+      reason (Exception): The reason to fail the Deferred with
+      pid (str): A unique id of a Deferred created with _DeferredMgr.create()
+    """
+    if pid not in self.__infos:
+      return
+
+    d = self.__infos[pid][1]
+    del self.__infos[pid]
+    d.errback(reason)
+
+  def failAll(self, reason, predicate):
+    """Fail all Deferred for which predicate(data) returns true.
+
+    Args:
+      reason (Exception): The reason to fail the Deferred with
+      predicate (function): A predicate
+    """
+    for pid, info in self.__infos.copy().items():
+      if predicate(pid, info[1]):
+        del self.__infos[pid]
+        info[0].errback(reason)
+
+
+class _CallbackMgr(object):
+
+  def __init__(self):
+    self.__cbs = {}
+
+  def on(self, key, cb):
+    first = False
+    if key not in self.__cbs:
+      self.__cbs[key] = {}
+      first = True
+    self.__cbs[key][id(cb)] = cb
+    return first
+
+  def off(self, key, cb):
+    if key not in self.__cbs or id(cb) not in self.__cbs[key]:
+      return False
+    del self.__cbs[key][id(cb)]
+    if not self.__cbs[key]:
+      del self.__cbs[key]
+      return True
+    return False
+
+  def fire(self, key, args):
+    #_printArgs('_CallbackMgr.fire', key, args)
+    if key not in self.__cbs:
+      return
+    for cb in self.__cbs[key].values():
+      try:
+        cb(args, key)
+      except BaseException as _:
+        traceback.print_exc()
+
+  def keys(self, predicate):
+    return [key for key in self.__cbs if predicate(key)]
+
+
+class Node(supcon.intf.Node):
+
+  def __init__(self, name):
+    super().__init__(name)
+
+    self.__impls = {}
+    self.__protocols = {}
+
+    self.__factory = _Factory(self.name, self.__addNode, self.__delNode, self.__recvMesg)
+
+    self.__service = twisted.application.service.MultiService()
+    self.__service.startService()
+
+    fire = lambda event, args: \
+      self.__fireEventLocal(self.name, '/', LocalIntf.name, event, args)
+
+    self.__knownMgr = _KnownMgr(fire)
+    self.__informMgr = _InformMgr()
+    self.__callbackMgr = _CallbackMgr()
+    self.__deferredMgr = _DeferredMgr()
+
+    self.__knownMgr.addNode(self.name)
+
+  def connect(self, endpoint):
+    service = twisted.application.internet.ClientService(endpoint, self.__factory)
+    self.__service.addService(service)
+
+  def listen(self, endpoint):
+    endpoint.listen(self.__factory)
+
+  def __addNode(self, node: str, protocol: _Protocol):
+    if node in self.__protocols:
+      return False
+    self.__protocols[node] = protocol
+
+    self.__knownMgr.addNode(node)
+
+    # reestablish existing callbacks by calls to supcon.Remote.on
+    for key in self.__callbackMgr.keys(lambda key: key[0] == node):
+      self.__callRemote(node, '/', RemoteIntf.name, 'on', {
+        'path': key[1],
+        'intf': key[2],
+        'event': key[3]
+      })
+
+    self.__callRemote(node, '/', RemoteIntf.name, 'node', {}).addCallback(
+      lambda args: self.__knownMgr.setNode(supcon.intf.DNode.load(args['node']))
+    ).addErrback(_printArgs)
+
+    return True
+
+  def __delNode(self, node):
+    if node not in self.__protocols:
+      return False
+    del self.__protocols[node]
+
+    self.__knownMgr.delNode(node)
+    self.__informMgr.delNode(node)
+
+    reason = RuntimeError('node {} lost'.format(node))
+    predicate = lambda pid, data: data[0] == node
+    self.__deferredMgr.failAll(reason, predicate)
+
+    return True
+
+  def nodes(self):
+    return self.__knownMgr.getNodes()
+
+  def register(self, path: str, impl: supcon.intf.Implementation):
+    path = supcon.intf.DPath.toName(path)
+
+    if path not in self.__impls:
+      self.__impls[path] = {}
+
+    if impl.intf in self.__impls[path]:
+      raise ValueError('interface {} already registered at path {}'.format(impl.intf, path))
+
+    self.__impls[path][impl.intf] = {
+      'impl': impl,
+      'fire': lambda event, args: self.__fireImplEvent(path, impl.intf, event, args)
+    }
+    impl.addFireCb(self.__impls[path][impl.intf]['fire'])
+
+    self.__knownMgr.addIntf(self.name, path, impl.interface)
+    self.__broadcastEvent('intf', {'path': path, 'intf': impl.interface.dump()})
+
+  def unregister(self, path: str, impl: supcon.intf.Implementation):
+    path = supcon.intf.DPath.toName(path)
+
+    if path not in self.__impls:
+      raise ValueError('unknown path {}'.format(path))
+
+    if impl.intf not in self.__impls[path]:
+      raise ValueError('unknown interface {} at path {}'.format(impl.intf, path))
+
+    if impl != self.__impls[path][impl.intf]['impl']:
+      raise ValueError('unknown implementation for interface {} at path {}'.format(impl.intf, path))
+
+    impl.delFireCb(self.__impls[path][impl.intf]['fire'])
+    del self.__impls[path][impl.intf]
+
+    self.__knownMgr.delIntf(self.name, path, impl.interface)
+    self.__broadcastEvent('intfLost', {'path': path, 'intf': impl.interface.dump()})
+
+  def on(self, node, path, intf, event, cb):
+    if self.__callbackMgr.on((node, path, intf, event), cb):
+      if node != self.name and node in self.__protocols:
+        self.__callRemote(node, '/', RemoteIntf.name, 'on', {
+          'path': path, 'intf': intf, 'event': event
+        })
+
+  def off(self, node, path, intf, event, cb):
+    if self.__callbackMgr.off((node, path, intf, event), cb):
+      if node != self.name and node in self.__protocols:
+        self.__callRemote(node, '/', RemoteIntf.name, 'off', {
+          'path': path, 'intf': intf, 'event': event
+        })
+
+  def call(self, node, path, intf, method, args) -> defer.Deferred:
+    # TODO: don't raise Exceptions
+    assert path != '/' or intf != RemoteIntf.name, \
+      'unable to call: method {} of interface {} at path /'.format(method, RemoteIntf.name)
+
+    if node == self.name:
+      if path == '/' and intf == LocalIntf.name:
+        return self.__callLocal(method, args)
+      return self.__callImpl(path, intf, method, args)
+    return self.__callRemote(node, path, intf, method, args)
+
+  def __callImpl(self, path, intf, method, args) -> defer.Deferred:
+    # TODO: don't raise Exception
+    assert path != '/' or intf != LocalIntf.name, \
+      'unable to call impl: method {} of interface {} at path /'.format(method, LocalIntf.name)
+    assert path != '/' or intf != RemoteIntf.name, \
+      'unable to call impl: method {} of interface {} at path /'.format(method, RemoteIntf.name)
+
+    try:
+      if path not in self.__impls:
+        raise ValueError('unknown path') # TODO: nicer error message
+      if intf not in self.__impls[path]:
+        raise ValueError('unknown intf') # TODO: nicer error message
+      if method not in self.__impls[path][intf]['impl'].interface.methods:
+        raise ValueError('unknown method') # TODO: nicer error message
+
+      d = self.__impls[path][intf]['impl'].call(method, args)
+      if not isinstance(d, defer.Deferred):
+        d = defer.succeed(d)
+    except BaseException as e:
+      d = defer.fail(e)
+
+    return d
+
+  def __callLocal(self, method, args) -> defer.Deferred:
+    # TODO: don't raise Exception
+    assert method in LocalIntf.methods, \
+      '{} is not an method of interface {} at path /'.format(method, RemoteIntf.name)
+    LocalIntf.methods[method].validateInArgs(args)
+
+    if method == 'nodes':
+      return defer.succeed({'nodes': self.__knownMgr.getNodes().dump()})
+
+    return defer.fail(RuntimeError('method {} not yet implemented'.format(method)))
+
+  def __callRemote(self, node, path, intf, method, args) -> defer.Deferred:
+    # TODO: don't raise Exception
+    assert path != '/' or intf != LocalIntf.name, \
+      'unable to call remote: method {} of interface {} at path /'.format(method, LocalIntf.name)
+
+    # TODO: validate args
+
+    (cid, p) = self.__deferredMgr.create((node, path, intf, method))
+    self.__sendCall(node, path, intf, method, args, cid)
+    return p
+
+  def __fireImplEvent(self, path, intf, event, args):
+    assert path != '/' or intf != LocalIntf.name, \
+      'unable to fire impl: event {} of interface {} at path /'.format(event, RemoteIntf.name)
+    assert path != '/' or intf != RemoteIntf.name, \
+      'unable to fire impl: event {} of interface {} at path /'.format(event, RemoteIntf.name)
+
+    self.__fireEventLocal(self.name, path, intf, event, args)
+    self.__fireEventRemote(path, intf, event, args)
+
+  def __fireEventLocal(self, node: str, path: str, intf: str, event: str, args: dict):
+    assert path != '/' or intf != RemoteIntf.name, \
+      'unable to fire local: event {} of interface {} at path /'.format(event, RemoteIntf.name)
+    #_printArgs('Node.__fireEventLocal', node, path, intf, event, args)
+
+    # TODO: validate args
+
+    self.__callbackMgr.fire((node, path, intf, event), args)
+
+  def __fireEventRemote(self, path: str, intf: str, event: str, args: dict):
+    assert path != '/' or intf != LocalIntf.name, \
+      'unable to fire remote: event {} of interface {} at path /'.format(event, LocalIntf.name)
+
+    for node in self.__informMgr.nodes(path, intf, event):
+      self.__sendEvent(node, path, intf, event, args)
+
+  def __broadcastEvent(self, event: str, args: dict):
+    try:
+      assert event in RemoteIntf.events, \
+        '{} is not an event of interface {} at path /'.format(event, RemoteIntf.name)
+      RemoteIntf.events[event].validateArgs(args)
+    except BaseException as _:
+      traceback.print_exc()
+      return
+
+    for node in self.__protocols:
+      self.__sendEvent(node, '/', RemoteIntf.name, event, args)
+
+  def __recvMesg(self, node: str, mesg: dict):
+    """Gets called by _Protocol on new Message"""
+    if node not in self.__protocols:
+      return False
+
+    if mesg['type'] == 'call':
+      self.__recvCall(node, mesg['path'], mesg['intf'], mesg['name'], mesg['args'], mesg['id'])
+    elif mesg['type'] == 'error':
+      self.__recvError(node, mesg['args'], mesg['id'])
+    elif mesg['type'] == 'response':
+      self.__recvResult(node, mesg['args'], mesg['id'])
+    elif mesg['type'] == 'event':
+      self.__recvEvent(node, mesg['path'], mesg['intf'], mesg['name'], mesg['args'])
+    else:
+      return False
+
+    return True
+
+  def __recvCall(self, node: str, path: str, intf: str, method: str, args: dict, cid: str):
+    try:
+      assert path != '/' or intf != LocalIntf.name, \
+        'unable to recv call: method {} of interface {} at path / from node {}'.format(method, intf, node)
+
+      if path == '/' and intf == RemoteIntf.name:
+        assert method in RemoteIntf.methods, \
+          '{} is not an method of interface {} at path /'.format(method, RemoteIntf.name)
+        RemoteIntf.methods[method].validateInArgs(args)
+
+        if method == 'node':
+          d = defer.succeed({'node': self.__knownMgr.getNode(self.name).dump()})
+        elif method == 'on':
+          self.__informMgr.on(args['path'], args['intf'], args['event'], node)
+          d = defer.succeed({})
+        elif method == 'off':
+          self.__informMgr.on(args['path'], args['intf'], args['event'], node)
+          d = defer.succeed({})
+        else:
+          raise ValueError('method {} is not yet implemented'.format(method))
+      else:
+        d = self.__callImpl(path, intf, method, args)
+    except BaseException as e:
+      traceback.print_exc()
+      d = defer.fail(e)
+
+    d.addCallbacks(
+      lambda result: self.__sendResult(node, result, cid),
+      lambda reason: self.__sendError(node, repr(reason), cid)
+    ).addErrback(_printArgs)
+
+  def __recvError(self, _node: str, reason: str, cid: str):
+    try:
+      self.__deferredMgr.fail(RuntimeError(reason), cid)
+    except BaseException as _:
+      traceback.print_exc()
+
+  def __recvResult(self, _node: str, result: dict, cid: str):
+    try:
+      self.__deferredMgr.succeed(result, cid)
+    except BaseException as _:
+      traceback.print_exc()
+
+  def __recvEvent(self, node: str, path: str, intf: str, event: str, args: dict):
+    try:
+      assert path != '/' or intf != LocalIntf.name, \
+        'unable to recv event: event {} of interface {} at path /'.format(event, LocalIntf.name)
+
+      if path == '/' and intf == RemoteIntf.name:
+        assert event in RemoteIntf.events, \
+          '{} is not an event of interface {} at path /'.format(event, RemoteIntf.name)
+        RemoteIntf.events[event].validateArgs(args)
+
+        self.__recvEventRemote(node, event, args)
+      else:
+        self.__fireEventLocal(node, path, intf, event, args)
+    except BaseException as _:
+      traceback.print_exc()
+
+  def __recvEventRemote(self, node, event, args):
+    if event == 'intf':
+      path = args['path']
+      interface = supcon.intf.DInterface.load(args['intf'])
+
+      self.__knownMgr.addIntf(node, path, interface)
+
+    elif event == 'intfLost':
+      path = args['path']
+      interface = supcon.intf.DInterface.load(args['intf'])
+
+      self.__knownMgr.delIntf(node, path, interface)
+
+      reason = RuntimeError('interface {} at path {} on node {} lost'.format(node, path, interface.name))
+      predicate = lambda pid, data: data[0] == node and data[1] == path and data[2] == interface.name
+      self.__deferredMgr.failAll(reason, predicate)
+
+    else:
+      raise ValueError('event {} not yet implemented'.format(event))
+
+  def __sendCall(self, node: str, path: str, intf: str, method: str, args: dict, cid: str) -> bool:
+    try:
+      assert path != '/' or intf != LocalIntf.name, \
+        'unable to send call: method {} of interface {} at path /'.format(method, LocalIntf.name)
+      if node not in self.__protocols:
+        raise RuntimeError('unknown node {}'.format(node))
+
+      self.__protocols[node].sendMesg({
+        'type': 'call',
+        'path': path,
+        'intf': intf,
+        'name': method,
+        'args': args,
+        'id': cid
+      })
+      return True
+    except BaseException as _:
+      traceback.print_exc()
+      return False
+
+  def __sendError(self, node: str, reason: str, cid: str) -> bool:
+    try:
+      if node not in self.__protocols:
+        raise RuntimeError('unknown node {}'.format(node))
+
+      self.__protocols[node].sendMesg({
+        'type': 'error',
+        'args': reason,
+        'id': cid
+      })
+      return True
+    except BaseException as _:
+      traceback.print_exc()
+      return False
+
+  def __sendResult(self, node: str, result: dict, cid: str) -> bool:
+    try:
+      if node not in self.__protocols:
+        raise RuntimeError('unknown node {}'.format(node))
+
+      self.__protocols[node].sendMesg({
+        'type': 'response',
+        'args': result,
+        'id': cid
+      })
+      return True
+    except BaseException as _:
+      traceback.print_exc()
+      return False
+
+  def __sendEvent(self, node: str, path: str, intf: str, event: str, args: dict) -> bool:
+    try:
+      assert path != '/' or intf != LocalIntf.name, \
+        'unable to send event: event {} of interface {} at path /'.format(event, LocalIntf.name)
+      if node not in self.__protocols:
+        traceback.print_stack()
+        raise RuntimeError('unknown node {}'.format(node))
+
+      self.__protocols[node].sendMesg({
+        'type': 'event',
+        'path': path,
+        'intf': intf,
+        'name': event,
+        'args': args
+      })
+      return True
+    except BaseException as _:
+      exc_info = sys.exc_info()
+      traceback.print_exception(*exc_info)
+      return False
+
+ +
+ +
+ +
+

Module variables

+
+

var LocalIntf

+ + +
+
+ +
+
+

var RemoteIntf

+ + +
+
+ +
+ + +

Classes

+ +
+

class Node

+ + +

The Node Interface. This class defines the methods that participants can +use to access the supcon bus.

+
+ +
+
class Node(supcon.intf.Node):
+
+  def __init__(self, name):
+    super().__init__(name)
+
+    self.__impls = {}
+    self.__protocols = {}
+
+    self.__factory = _Factory(self.name, self.__addNode, self.__delNode, self.__recvMesg)
+
+    self.__service = twisted.application.service.MultiService()
+    self.__service.startService()
+
+    fire = lambda event, args: \
+      self.__fireEventLocal(self.name, '/', LocalIntf.name, event, args)
+
+    self.__knownMgr = _KnownMgr(fire)
+    self.__informMgr = _InformMgr()
+    self.__callbackMgr = _CallbackMgr()
+    self.__deferredMgr = _DeferredMgr()
+
+    self.__knownMgr.addNode(self.name)
+
+  def connect(self, endpoint):
+    service = twisted.application.internet.ClientService(endpoint, self.__factory)
+    self.__service.addService(service)
+
+  def listen(self, endpoint):
+    endpoint.listen(self.__factory)
+
+  def __addNode(self, node: str, protocol: _Protocol):
+    if node in self.__protocols:
+      return False
+    self.__protocols[node] = protocol
+
+    self.__knownMgr.addNode(node)
+
+    # reestablish existing callbacks by calls to supcon.Remote.on
+    for key in self.__callbackMgr.keys(lambda key: key[0] == node):
+      self.__callRemote(node, '/', RemoteIntf.name, 'on', {
+        'path': key[1],
+        'intf': key[2],
+        'event': key[3]
+      })
+
+    self.__callRemote(node, '/', RemoteIntf.name, 'node', {}).addCallback(
+      lambda args: self.__knownMgr.setNode(supcon.intf.DNode.load(args['node']))
+    ).addErrback(_printArgs)
+
+    return True
+
+  def __delNode(self, node):
+    if node not in self.__protocols:
+      return False
+    del self.__protocols[node]
+
+    self.__knownMgr.delNode(node)
+    self.__informMgr.delNode(node)
+
+    reason = RuntimeError('node {} lost'.format(node))
+    predicate = lambda pid, data: data[0] == node
+    self.__deferredMgr.failAll(reason, predicate)
+
+    return True
+
+  def nodes(self):
+    return self.__knownMgr.getNodes()
+
+  def register(self, path: str, impl: supcon.intf.Implementation):
+    path = supcon.intf.DPath.toName(path)
+
+    if path not in self.__impls:
+      self.__impls[path] = {}
+
+    if impl.intf in self.__impls[path]:
+      raise ValueError('interface {} already registered at path {}'.format(impl.intf, path))
+
+    self.__impls[path][impl.intf] = {
+      'impl': impl,
+      'fire': lambda event, args: self.__fireImplEvent(path, impl.intf, event, args)
+    }
+    impl.addFireCb(self.__impls[path][impl.intf]['fire'])
+
+    self.__knownMgr.addIntf(self.name, path, impl.interface)
+    self.__broadcastEvent('intf', {'path': path, 'intf': impl.interface.dump()})
+
+  def unregister(self, path: str, impl: supcon.intf.Implementation):
+    path = supcon.intf.DPath.toName(path)
+
+    if path not in self.__impls:
+      raise ValueError('unknown path {}'.format(path))
+
+    if impl.intf not in self.__impls[path]:
+      raise ValueError('unknown interface {} at path {}'.format(impl.intf, path))
+
+    if impl != self.__impls[path][impl.intf]['impl']:
+      raise ValueError('unknown implementation for interface {} at path {}'.format(impl.intf, path))
+
+    impl.delFireCb(self.__impls[path][impl.intf]['fire'])
+    del self.__impls[path][impl.intf]
+
+    self.__knownMgr.delIntf(self.name, path, impl.interface)
+    self.__broadcastEvent('intfLost', {'path': path, 'intf': impl.interface.dump()})
+
+  def on(self, node, path, intf, event, cb):
+    if self.__callbackMgr.on((node, path, intf, event), cb):
+      if node != self.name and node in self.__protocols:
+        self.__callRemote(node, '/', RemoteIntf.name, 'on', {
+          'path': path, 'intf': intf, 'event': event
+        })
+
+  def off(self, node, path, intf, event, cb):
+    if self.__callbackMgr.off((node, path, intf, event), cb):
+      if node != self.name and node in self.__protocols:
+        self.__callRemote(node, '/', RemoteIntf.name, 'off', {
+          'path': path, 'intf': intf, 'event': event
+        })
+
+  def call(self, node, path, intf, method, args) -> defer.Deferred:
+    # TODO: don't raise Exceptions
+    assert path != '/' or intf != RemoteIntf.name, \
+      'unable to call: method {} of interface {} at path /'.format(method, RemoteIntf.name)
+
+    if node == self.name:
+      if path == '/' and intf == LocalIntf.name:
+        return self.__callLocal(method, args)
+      return self.__callImpl(path, intf, method, args)
+    return self.__callRemote(node, path, intf, method, args)
+
+  def __callImpl(self, path, intf, method, args) -> defer.Deferred:
+    # TODO: don't raise Exception
+    assert path != '/' or intf != LocalIntf.name, \
+      'unable to call impl: method {} of interface {} at path /'.format(method, LocalIntf.name)
+    assert path != '/' or intf != RemoteIntf.name, \
+      'unable to call impl: method {} of interface {} at path /'.format(method, RemoteIntf.name)
+
+    try:
+      if path not in self.__impls:
+        raise ValueError('unknown path') # TODO: nicer error message
+      if intf not in self.__impls[path]:
+        raise ValueError('unknown intf') # TODO: nicer error message
+      if method not in self.__impls[path][intf]['impl'].interface.methods:
+        raise ValueError('unknown method') # TODO: nicer error message
+
+      d = self.__impls[path][intf]['impl'].call(method, args)
+      if not isinstance(d, defer.Deferred):
+        d = defer.succeed(d)
+    except BaseException as e:
+      d = defer.fail(e)
+
+    return d
+
+  def __callLocal(self, method, args) -> defer.Deferred:
+    # TODO: don't raise Exception
+    assert method in LocalIntf.methods, \
+      '{} is not an method of interface {} at path /'.format(method, RemoteIntf.name)
+    LocalIntf.methods[method].validateInArgs(args)
+
+    if method == 'nodes':
+      return defer.succeed({'nodes': self.__knownMgr.getNodes().dump()})
+
+    return defer.fail(RuntimeError('method {} not yet implemented'.format(method)))
+
+  def __callRemote(self, node, path, intf, method, args) -> defer.Deferred:
+    # TODO: don't raise Exception
+    assert path != '/' or intf != LocalIntf.name, \
+      'unable to call remote: method {} of interface {} at path /'.format(method, LocalIntf.name)
+
+    # TODO: validate args
+
+    (cid, p) = self.__deferredMgr.create((node, path, intf, method))
+    self.__sendCall(node, path, intf, method, args, cid)
+    return p
+
+  def __fireImplEvent(self, path, intf, event, args):
+    assert path != '/' or intf != LocalIntf.name, \
+      'unable to fire impl: event {} of interface {} at path /'.format(event, RemoteIntf.name)
+    assert path != '/' or intf != RemoteIntf.name, \
+      'unable to fire impl: event {} of interface {} at path /'.format(event, RemoteIntf.name)
+
+    self.__fireEventLocal(self.name, path, intf, event, args)
+    self.__fireEventRemote(path, intf, event, args)
+
+  def __fireEventLocal(self, node: str, path: str, intf: str, event: str, args: dict):
+    assert path != '/' or intf != RemoteIntf.name, \
+      'unable to fire local: event {} of interface {} at path /'.format(event, RemoteIntf.name)
+    #_printArgs('Node.__fireEventLocal', node, path, intf, event, args)
+
+    # TODO: validate args
+
+    self.__callbackMgr.fire((node, path, intf, event), args)
+
+  def __fireEventRemote(self, path: str, intf: str, event: str, args: dict):
+    assert path != '/' or intf != LocalIntf.name, \
+      'unable to fire remote: event {} of interface {} at path /'.format(event, LocalIntf.name)
+
+    for node in self.__informMgr.nodes(path, intf, event):
+      self.__sendEvent(node, path, intf, event, args)
+
+  def __broadcastEvent(self, event: str, args: dict):
+    try:
+      assert event in RemoteIntf.events, \
+        '{} is not an event of interface {} at path /'.format(event, RemoteIntf.name)
+      RemoteIntf.events[event].validateArgs(args)
+    except BaseException as _:
+      traceback.print_exc()
+      return
+
+    for node in self.__protocols:
+      self.__sendEvent(node, '/', RemoteIntf.name, event, args)
+
+  def __recvMesg(self, node: str, mesg: dict):
+    """Gets called by _Protocol on new Message"""
+    if node not in self.__protocols:
+      return False
+
+    if mesg['type'] == 'call':
+      self.__recvCall(node, mesg['path'], mesg['intf'], mesg['name'], mesg['args'], mesg['id'])
+    elif mesg['type'] == 'error':
+      self.__recvError(node, mesg['args'], mesg['id'])
+    elif mesg['type'] == 'response':
+      self.__recvResult(node, mesg['args'], mesg['id'])
+    elif mesg['type'] == 'event':
+      self.__recvEvent(node, mesg['path'], mesg['intf'], mesg['name'], mesg['args'])
+    else:
+      return False
+
+    return True
+
+  def __recvCall(self, node: str, path: str, intf: str, method: str, args: dict, cid: str):
+    try:
+      assert path != '/' or intf != LocalIntf.name, \
+        'unable to recv call: method {} of interface {} at path / from node {}'.format(method, intf, node)
+
+      if path == '/' and intf == RemoteIntf.name:
+        assert method in RemoteIntf.methods, \
+          '{} is not an method of interface {} at path /'.format(method, RemoteIntf.name)
+        RemoteIntf.methods[method].validateInArgs(args)
+
+        if method == 'node':
+          d = defer.succeed({'node': self.__knownMgr.getNode(self.name).dump()})
+        elif method == 'on':
+          self.__informMgr.on(args['path'], args['intf'], args['event'], node)
+          d = defer.succeed({})
+        elif method == 'off':
+          self.__informMgr.on(args['path'], args['intf'], args['event'], node)
+          d = defer.succeed({})
+        else:
+          raise ValueError('method {} is not yet implemented'.format(method))
+      else:
+        d = self.__callImpl(path, intf, method, args)
+    except BaseException as e:
+      traceback.print_exc()
+      d = defer.fail(e)
+
+    d.addCallbacks(
+      lambda result: self.__sendResult(node, result, cid),
+      lambda reason: self.__sendError(node, repr(reason), cid)
+    ).addErrback(_printArgs)
+
+  def __recvError(self, _node: str, reason: str, cid: str):
+    try:
+      self.__deferredMgr.fail(RuntimeError(reason), cid)
+    except BaseException as _:
+      traceback.print_exc()
+
+  def __recvResult(self, _node: str, result: dict, cid: str):
+    try:
+      self.__deferredMgr.succeed(result, cid)
+    except BaseException as _:
+      traceback.print_exc()
+
+  def __recvEvent(self, node: str, path: str, intf: str, event: str, args: dict):
+    try:
+      assert path != '/' or intf != LocalIntf.name, \
+        'unable to recv event: event {} of interface {} at path /'.format(event, LocalIntf.name)
+
+      if path == '/' and intf == RemoteIntf.name:
+        assert event in RemoteIntf.events, \
+          '{} is not an event of interface {} at path /'.format(event, RemoteIntf.name)
+        RemoteIntf.events[event].validateArgs(args)
+
+        self.__recvEventRemote(node, event, args)
+      else:
+        self.__fireEventLocal(node, path, intf, event, args)
+    except BaseException as _:
+      traceback.print_exc()
+
+  def __recvEventRemote(self, node, event, args):
+    if event == 'intf':
+      path = args['path']
+      interface = supcon.intf.DInterface.load(args['intf'])
+
+      self.__knownMgr.addIntf(node, path, interface)
+
+    elif event == 'intfLost':
+      path = args['path']
+      interface = supcon.intf.DInterface.load(args['intf'])
+
+      self.__knownMgr.delIntf(node, path, interface)
+
+      reason = RuntimeError('interface {} at path {} on node {} lost'.format(node, path, interface.name))
+      predicate = lambda pid, data: data[0] == node and data[1] == path and data[2] == interface.name
+      self.__deferredMgr.failAll(reason, predicate)
+
+    else:
+      raise ValueError('event {} not yet implemented'.format(event))
+
+  def __sendCall(self, node: str, path: str, intf: str, method: str, args: dict, cid: str) -> bool:
+    try:
+      assert path != '/' or intf != LocalIntf.name, \
+        'unable to send call: method {} of interface {} at path /'.format(method, LocalIntf.name)
+      if node not in self.__protocols:
+        raise RuntimeError('unknown node {}'.format(node))
+
+      self.__protocols[node].sendMesg({
+        'type': 'call',
+        'path': path,
+        'intf': intf,
+        'name': method,
+        'args': args,
+        'id': cid
+      })
+      return True
+    except BaseException as _:
+      traceback.print_exc()
+      return False
+
+  def __sendError(self, node: str, reason: str, cid: str) -> bool:
+    try:
+      if node not in self.__protocols:
+        raise RuntimeError('unknown node {}'.format(node))
+
+      self.__protocols[node].sendMesg({
+        'type': 'error',
+        'args': reason,
+        'id': cid
+      })
+      return True
+    except BaseException as _:
+      traceback.print_exc()
+      return False
+
+  def __sendResult(self, node: str, result: dict, cid: str) -> bool:
+    try:
+      if node not in self.__protocols:
+        raise RuntimeError('unknown node {}'.format(node))
+
+      self.__protocols[node].sendMesg({
+        'type': 'response',
+        'args': result,
+        'id': cid
+      })
+      return True
+    except BaseException as _:
+      traceback.print_exc()
+      return False
+
+  def __sendEvent(self, node: str, path: str, intf: str, event: str, args: dict) -> bool:
+    try:
+      assert path != '/' or intf != LocalIntf.name, \
+        'unable to send event: event {} of interface {} at path /'.format(event, LocalIntf.name)
+      if node not in self.__protocols:
+        traceback.print_stack()
+        raise RuntimeError('unknown node {}'.format(node))
+
+      self.__protocols[node].sendMesg({
+        'type': 'event',
+        'path': path,
+        'intf': intf,
+        'name': event,
+        'args': args
+      })
+      return True
+    except BaseException as _:
+      exc_info = sys.exc_info()
+      traceback.print_exception(*exc_info)
+      return False
+
+ +
+
+ + +
+

Ancestors (in MRO)

+
    +
  • Node
  • +
  • supcon.intf.Node
  • +
  • abc.ABC
  • +
  • builtins.object
  • +
+

Class Methods

+ +
+
+

def toName(

cls, value)

+
+ + + + +
+ +
+
@classmethod
+def toName(cls, value) -> str:
+  return DNode.toName(value)
+
+ +
+
+ +
+ +

Instance variables

+
+

var name

+ + + + +

str: The name of the node on the bus

+
+
+ +
+

Methods

+ +
+
+

def __init__(

self, name)

+
+ + + + +

Initialize self. See help(type(self)) for accurate signature.

+
+ +
+
def __init__(self, name):
+  super().__init__(name)
+  self.__impls = {}
+  self.__protocols = {}
+  self.__factory = _Factory(self.name, self.__addNode, self.__delNode, self.__recvMesg)
+  self.__service = twisted.application.service.MultiService()
+  self.__service.startService()
+  fire = lambda event, args: \
+    self.__fireEventLocal(self.name, '/', LocalIntf.name, event, args)
+  self.__knownMgr = _KnownMgr(fire)
+  self.__informMgr = _InformMgr()
+  self.__callbackMgr = _CallbackMgr()
+  self.__deferredMgr = _DeferredMgr()
+  self.__knownMgr.addNode(self.name)
+
+ +
+
+ +
+ + +
+
+

def call(

self, node, path, intf, method, args)

+
+ + + + +

Calls a method on the bus

+

Args: + node (str): a node on the bus + path (str): a path on the given node + intf (str): an interface at the given path + method (str): a method of the given interface + args (dict): a dict of method arguments

+

Returns: + defer.Deferred:

+
+ +
+
def call(self, node, path, intf, method, args) -> defer.Deferred:
+  # TODO: don't raise Exceptions
+  assert path != '/' or intf != RemoteIntf.name, \
+    'unable to call: method {} of interface {} at path /'.format(method, RemoteIntf.name)
+  if node == self.name:
+    if path == '/' and intf == LocalIntf.name:
+      return self.__callLocal(method, args)
+    return self.__callImpl(path, intf, method, args)
+  return self.__callRemote(node, path, intf, method, args)
+
+ +
+
+ +
+ + +
+
+

def connect(

self, endpoint)

+
+ + + + +

Connects the node to the given endpoint.

+

If the connection failes or closes the connection gets reestablished with an +exponential timeout up to two minutes.

+

Args: + endpoint (twisted.internet.interfaces.IStreamClientEndpoint):

+
+ +
+
def connect(self, endpoint):
+  service = twisted.application.internet.ClientService(endpoint, self.__factory)
+  self.__service.addService(service)
+
+ +
+
+ +
+ + +
+
+

def listen(

self, endpoint)

+
+ + + + +

Listens at the given endpoint for incoming connections

+

Args: + endpoint (twisted.internet.interfaces.IStreamServerEndpoint):

+
+ +
+
def listen(self, endpoint):
+  endpoint.listen(self.__factory)
+
+ +
+
+ +
+ + +
+
+

def nodes(

self)

+
+ + + + +

list[str]: The currently connected nodes

+
+ +
+
def nodes(self):
+  return self.__knownMgr.getNodes()
+
+ +
+
+ +
+ + +
+
+

def off(

self, node, path, intf, event, cb)

+
+ + + + +

Unregisters a callback for an event on the bus

+

Args: + node (str): a node on the bus + path (str): a path on the given node + intf (str): an interface at the given path + event (str): a method of the given interface + cb (callable): a callable that gets called with a dict of event arguments

+
+ +
+
def off(self, node, path, intf, event, cb):
+  if self.__callbackMgr.off((node, path, intf, event), cb):
+    if node != self.name and node in self.__protocols:
+      self.__callRemote(node, '/', RemoteIntf.name, 'off', {
+        'path': path, 'intf': intf, 'event': event
+      })
+
+ +
+
+ +
+ + +
+
+

def on(

self, node, path, intf, event, cb)

+
+ + + + +

Registers a callback for an event on the bus

+

Args: + node (str): a node on the bus + path (str): a path on the given node + intf (str): an interface at the given path + event (str): a method of the given interface + cb (callable): a callable that gets called with a dict of event arguments

+
+ +
+
def on(self, node, path, intf, event, cb):
+  if self.__callbackMgr.on((node, path, intf, event), cb):
+    if node != self.name and node in self.__protocols:
+      self.__callRemote(node, '/', RemoteIntf.name, 'on', {
+        'path': path, 'intf': intf, 'event': event
+      })
+
+ +
+
+ +
+ + +
+
+

def register(

self, path, impl)

+
+ + + + +

Registers an implementation with the node

+

Args: + impl (Implementation):

+
+ +
+
def register(self, path: str, impl: supcon.intf.Implementation):
+  path = supcon.intf.DPath.toName(path)
+  if path not in self.__impls:
+    self.__impls[path] = {}
+  if impl.intf in self.__impls[path]:
+    raise ValueError('interface {} already registered at path {}'.format(impl.intf, path))
+  self.__impls[path][impl.intf] = {
+    'impl': impl,
+    'fire': lambda event, args: self.__fireImplEvent(path, impl.intf, event, args)
+  }
+  impl.addFireCb(self.__impls[path][impl.intf]['fire'])
+  self.__knownMgr.addIntf(self.name, path, impl.interface)
+  self.__broadcastEvent('intf', {'path': path, 'intf': impl.interface.dump()})
+
+ +
+
+ +
+ + +
+
+

def unregister(

self, path, impl)

+
+ + + + +

Removes an implementation from the node

+

Args: + impl (supcon.intf.Implementation):

+
+ +
+
def unregister(self, path: str, impl: supcon.intf.Implementation):
+  path = supcon.intf.DPath.toName(path)
+  if path not in self.__impls:
+    raise ValueError('unknown path {}'.format(path))
+  if impl.intf not in self.__impls[path]:
+    raise ValueError('unknown interface {} at path {}'.format(impl.intf, path))
+  if impl != self.__impls[path][impl.intf]['impl']:
+    raise ValueError('unknown implementation for interface {} at path {}'.format(impl.intf, path))
+  impl.delFireCb(self.__impls[path][impl.intf]['fire'])
+  del self.__impls[path][impl.intf]
+  self.__knownMgr.delIntf(self.name, path, impl.interface)
+  self.__broadcastEvent('intfLost', {'path': path, 'intf': impl.interface.dump()})
+
+ +
+
+ +
+ +
+
+ +
+ +
+
diff --git a/doc/index.md b/doc/index.md new file mode 100644 index 0000000..829a807 --- /dev/null +++ b/doc/index.md @@ -0,0 +1,648 @@ + + + + + + + + + +
+ + + + +
+ + + + + + +
+

supcon module

+

The supcon Framework provides inter-process communication (IPC) and remote +procedure call (RPC) mechanisms, that allow communication between multiple +computer programs running on different hosts in a TCP/IP network.

+

Each participating program exposes one or more supcon.intf.Nodes to the +network. Each supcon.intf.Node has a name that must be unique among the +nodes of the communicating programs.

+
import supcon.core
+
+lServer = supcon.core.Node('my-unique-server')
+lClient = supcon.core.Node('my-unique-client')
+
+ + +

You can give a supcon.intf.Node one or more endpoints on the network to either +connect to or to listen for incomming connections.

+
from twisted.internet import reactor
+from twisted.internet.endpoints import TCP4ClientEndpoint
+from twisted.internet.endpoints import TCP4ServerEndpoint
+
+lServer.listen(TCP4ServerEndpoint(reactor, 7000))
+lClient.connect(TCP4ClientEndpoint(reactor, 'localhost', 7000))
+
+ + +

When a supcon.intf.Node is unable to create a connection to the given endpoint +or an established connection terminates, the node tries to reestablish the +connection.

+

Programs can expose events and provide methods via the node. These events and +methods are defined in supcon.intf.Interfaces. Lets create one:

+
import supcon.intf
+
+timerIntf = supcon.intf.Interface({
+  'name': 'com.example.Timer',
+  'events': [
+    {
+      'name': 'timeout',
+      'args': [
+        { 'name': 'timer', 'description': '(str) - The name of the timer' }
+      ]
+      'description': 'Gets triggerd, whenever a timer times out.'
+    }
+  ],
+  'methods': [
+    {
+      'name': 'add',
+      'inArgs': [
+        { 'name': 'timer', 'description': '(str) - A name for the timer' }
+        { 'name': 'seconds', 'description': '(float) - A duration' }
+      ],
+      'description': 'Adds a new timer.'
+    }
+  ]
+})
+
+ + +

Lets implement the interface and register the implementation at the local node:

+
timerImpl = supcon.intf.Implementation(timerIntf)
+
+def fireTimeout(timer):
+  timerImpl.fire('timeout', { 'timer': timer })
+
+def onAdd(timer, seconds):
+  reactor.callLater(seconds, lambda: fireTimeout(timer))
+
+timerImpl.setCallCb('add', onAdd)
+
+lServer.register('/myTimer', timerImpl)
+
+ + +

Now you can watch for timeout events and add a timer via the +lClient reference:

+
lClient.on('my-unique-server', '/myTimer', 'com.example.Timer', 'timeout', 
+  lambda *args: print(', '.join([ '{}'.format(i) for i in args ]))
+)
+
+lClient.call('my-unique-server', '/myTimer', 'com.example.Timer', 'add', {
+  'timer': '5Seconds', 'seconds': 5
+})
+
+ + +

And don't forget to start the reactor (aka event loop)

+
reactor.run()
+
+ + +
+
# -*- coding: utf-8 -*-
+"""
+The `supcon` Framework provides inter-process communication (IPC) and remote
+procedure call (RPC) mechanisms, that allow communication between multiple
+computer programs running on different hosts in a TCP/IP network.
+
+Each participating program exposes one or more `supcon.intf.Node`s to the
+network. Each `supcon.intf.Node` has a name that must be unique among the
+nodes of the communicating programs.
+
+    #!python
+    import supcon.core
+    
+    lServer = supcon.core.Node('my-unique-server')
+    lClient = supcon.core.Node('my-unique-client')
+
+You can give a `supcon.intf.Node` one or more endpoints on the network to either
+connect to or to listen for incomming connections.
+
+    #!python
+    from twisted.internet import reactor
+    from twisted.internet.endpoints import TCP4ClientEndpoint
+    from twisted.internet.endpoints import TCP4ServerEndpoint
+
+    lServer.listen(TCP4ServerEndpoint(reactor, 7000))
+    lClient.connect(TCP4ClientEndpoint(reactor, 'localhost', 7000))
+
+When a `supcon.intf.Node` is unable to create a connection to the given endpoint
+or an established connection terminates, the node tries to reestablish the
+connection.
+
+Programs can expose events and provide methods via the node. These events and
+methods are defined in `supcon.intf.Interface`s. Lets create one:
+
+    #!python
+    import supcon.intf
+
+    timerIntf = supcon.intf.Interface({
+      'name': 'com.example.Timer',
+      'events': [
+        {
+          'name': 'timeout',
+          'args': [
+            { 'name': 'timer', 'description': '(str) - The name of the timer' }
+          ]
+          'description': 'Gets triggerd, whenever a timer times out.'
+        }
+      ],
+      'methods': [
+        {
+          'name': 'add',
+          'inArgs': [
+            { 'name': 'timer', 'description': '(str) - A name for the timer' }
+            { 'name': 'seconds', 'description': '(float) - A duration' }
+          ],
+          'description': 'Adds a new timer.'
+        }
+      ]
+    })
+
+Lets implement the interface and register the implementation at the local node:
+
+    #!python
+
+    timerImpl = supcon.intf.Implementation(timerIntf)
+    
+    def fireTimeout(timer):
+      timerImpl.fire('timeout', { 'timer': timer })
+    
+    def onAdd(timer, seconds):
+      reactor.callLater(seconds, lambda: fireTimeout(timer))
+
+    timerImpl.setCallCb('add', onAdd)
+
+    lServer.register('/myTimer', timerImpl)
+
+Now you can watch for timeout events and add a timer via the
+<code>lClient</code> reference:
+
+    #!python
+    lClient.on('my-unique-server', '/myTimer', 'com.example.Timer', 'timeout', 
+      lambda *args: print(', '.join([ '{}'.format(i) for i in args ]))
+    )
+
+    lClient.call('my-unique-server', '/myTimer', 'com.example.Timer', 'add', {
+      'timer': '5Seconds', 'seconds': 5
+    })
+
+And don't forget to start the reactor (aka event loop)
+
+    #!python
+    reactor.run()
+
+"""
+
+ +
+ +
+ +
+ + + +

Sub-modules

+
+

supcon.clone

+ + + +
+
+

supcon.core

+ + +

This module provides the implementation of abstract class supcon.intf.Node.

+ +
+
+

supcon.intf

+ + +

This module contains the classes to describe interfaces on the supcon bus, +a base class supcon.intf.Implementation to implemet those interfaces and the +abstract class supcon.intf.Node which contains methods to interact with the +supcon bus.

+ +
+
+

supcon.keyed

+ + + +
+
+

supcon.store

+ + + +
+
+

supcon.switch

+ + + +
+
+

supcon.util

+ + + +
+
+ +
+
diff --git a/doc/intf.m.md b/doc/intf.m.md new file mode 100644 index 0000000..fdcc08b --- /dev/null +++ b/doc/intf.m.md @@ -0,0 +1,6687 @@ + + + + + + + + + +
+ + + + +
+ + + + + + +
+

supcon.intf module

+

This module contains the classes to describe interfaces on the supcon bus, +a base class Implementation to implemet those interfaces and the +abstract class Node which contains methods to interact with the +supcon bus.

+ + +
+
# -*- coding: utf-8 -*-
+"""
+This module contains the classes to describe interfaces on the supcon bus,
+a base class `supcon.intf.Implementation` to implemet those interfaces and the
+abstract class `supcon.intf.Node` which contains methods to interact with the
+supcon bus.
+"""
+
+import re
+import abc
+import itertools
+import traceback
+
+import twisted.internet.defer as defer
+
+import supcon.util
+
+class Named(supcon.util.Named):
+  """A base class for objects with a name.
+  """
+
+  regex = None
+  """re.RegexObject: a regular expression, the name must conform to"""
+
+  def __init__(self, name):
+    """Initializes the NamedAndDescribed instance
+
+    Args:
+      name (str): the name of the argument
+    """
+    self.__name = self.toName(name)
+
+  @property
+  def name(self) -> str:
+    """str: the name"""
+    return self.__name
+
+  @classmethod
+  def toName(cls, value) -> str:
+    """Converts the value into a name. If this is impossible a ValueError is
+    raised.
+
+    Args:
+      value (any): the value
+    Raises:
+      ValueError
+    """
+    value = str(value)
+    if not cls.regex.fullmatch(value):
+      raise ValueError("value {} must match {} fully".format(value, cls.regex.pattern))
+    return value
+
+
+class NamedAndDescribed(Named):
+  """A base class for the interface describing classes `supcon.intf.Argument`,
+  `supcon.intf.Event`, `supcon.intf.Method` and `supcon.intf.Interface` that
+  all have the properties name and description."""
+
+  def __init__(self, name, description=''):
+    """Initializes the NamedAndDescribed instance
+
+    Args:
+      name (str): the name of the argument
+      description (str): a description of the argument
+    """
+    Named.__init__(self, name)
+    self.__description = self.toDescription(description)
+
+  @property
+  def description(self) -> str:
+    """str: a description"""
+    return self.__description
+
+  @classmethod
+  def toDescription(cls, value) -> str:
+    """Converts the value into a description. If this is impossible a ValueError
+    is raised.
+
+    Args:
+      value (any): the value
+    Raises:
+      ValueError
+    """
+    return str(value)
+
+
+class DArgument(supcon.util.Dumpable, NamedAndDescribed):
+  """Describes an input or output argument of a `supcon.intf.DMethod` or an
+  argument of a `supcon.intf.DEvent`"""
+
+  regex = re.compile('[a-zA-Z0-9]+')
+
+  def validate(self, argument):
+    """Validates the given argument value. Raises an ValueError if validation
+    fails.
+    """
+    pass
+
+  def dump(self):
+    data = {'name': self.name}
+    if self.description != '':
+      data['description'] = self.description
+    return data
+
+  @classmethod
+  def load(cls, data):
+    return cls(**data)
+
+
+class DArguments(supcon.util.NamedList):
+  """A readonly map of `supcon.intf.DArgument` instances"""
+  vtype = DArgument
+
+
+class DEvent(supcon.util.Dumpable, NamedAndDescribed):
+  """Describes an event that can be emitted by an implementation on the bus"""
+
+  regex = re.compile('[a-zA-Z0-9]+')
+
+  def __init__(self, name, args=DArguments(), description=''):
+    """Initializes the Event
+
+    Args:
+      name (str): the name of the event
+      args (DArguments): the list of arguments of the event
+      description (str): a description of the event
+    """
+    NamedAndDescribed.__init__(self, name, description)
+    self.__args = DArguments.to(args)
+
+  @property
+  def args(self) -> DArguments:
+    """DArguments: the list of arguments of the event"""
+    return self.__args
+
+  def validateArgs(self, args):
+    """Validates the given argument map. Raises an ValueError if validation
+    fails.
+    """
+    for arg in args:
+      if arg not in self.__args:
+        raise ValueError('event {} has no argument {}'.format(self.name, arg))
+      self.__args[arg].validate(args[arg])
+    for arg in self.__args:
+      if arg not in args:
+        raise ValueError('event {} needs argument {}'.format(self.name, arg))
+
+  def dump(self):
+    data = {'name': self.name}
+    if self.args:
+      data['args'] = self.args.dump()
+    if self.description != '':
+      data['description'] = self.description
+    return data
+
+  @classmethod
+  def load(cls, data):
+    return cls(**data)
+
+
+class DEvents(supcon.util.NamedList):
+  """A readonly map of `supcon.intf.DEvent` instances"""
+  vtype = DEvent
+
+
+class DMethod(supcon.util.Dumpable, NamedAndDescribed):
+  """A DMethod that can be called on an Object on the Bus"""
+
+  regex = re.compile('[a-zA-Z0-9]+')
+
+  def __init__(self, name, inArgs=DArguments(), outArgs=DArguments(), description=''):
+    """Initializes the Event
+
+    Args:
+      name (str): the name of the method
+      inArgs (DArguments): the list of input arguments of the method
+      outArgs (DArguments): the list of output arguments of the method
+      description (str): a description of the method
+    """
+    NamedAndDescribed.__init__(self, name, description)
+    self.__inArgs = DArguments.to(inArgs)#
+    self.__outArgs = DArguments.to(outArgs)
+
+  @property
+  def inArgs(self) -> DArguments:
+    """DArguments: The input arguments of the method"""
+    return self.__inArgs
+
+  @property
+  def outArgs(self) -> DArguments:
+    """DArguments: The output arguments of the method"""
+    return self.__outArgs
+
+  def validateInArgs(self, inArgs):
+    """Validates the given argument Mapping. Raises an ValueError if validation fails
+    """
+    for arg in inArgs:
+      if arg not in self.__inArgs:
+        raise ValueError('method {} has no input argument {}'.format(self.name, arg))
+      self.__inArgs[arg].validate(inArgs[arg])
+    for arg in self.__inArgs:
+      if arg not in inArgs:
+        raise ValueError('method {} needs input argument {}'.format(self.name, arg))
+
+  def validateOutArgs(self, outArgs):
+    """Validates the given argument Mapping. Raises an ValueError if validation fails
+    """
+    for arg in outArgs:
+      if arg not in self.__outArgs:
+        raise ValueError('method {} has no output argument {}'.format(self.name, arg))
+      self.__outArgs[arg].validate(outArgs[arg])
+    for arg in self.__outArgs:
+      if arg not in outArgs:
+        raise ValueError('method {} needs output argument {}'.format(self.name, arg))
+
+  def dump(self):
+    data = {'name': self.name}
+    if self.inArgs:
+      data['inArgs'] = self.inArgs.dump()
+    if self.outArgs:
+      data['outArgs'] = self.outArgs.dump()
+    if self.description != '':
+      data['description'] = self.description
+    return data
+
+  @classmethod
+  def load(cls, data):
+    return cls(**data)
+
+
+class DMethods(supcon.util.NamedList):
+  """A readonly map of `supcon.intf.DMethod` instances"""
+  vtype = DMethod
+
+
+class DInterface(supcon.util.Dumpable, NamedAndDescribed):
+  """An Interface that is implemented by an Object on the Bus"""
+
+  regex = re.compile('([a-zA-Z0-9_]+\\.)*[a-zA-Z0-9_]+')
+
+  def __init__(self, name, events=DEvents(), methods=DMethods(), description=''):
+    """Initializes the Event
+
+    Args:
+      name (str): the name of the interface
+      events (DEvents): the list of events of the interface
+      methods (DMethods): the list of methods of the interface
+      description (str): a description of the interface
+    """
+    NamedAndDescribed.__init__(self, name, description)
+    self.__events = DEvents.to(events)
+    self.__methods = DMethods.to(methods)
+
+  @property
+  def events(self) -> DEvents:
+    """DEvent: the list of events this interface can emit"""
+    return self.__events
+
+  @property
+  def methods(self) -> DMethods:
+    """DMethods: the list of methods this interface provides"""
+    return self.__methods
+
+  def validateEvent(self, event, args):
+    """Validates that the given event is an event of this interface and that
+    the given arguments are arguments of the event
+
+    Args:
+      event (str): the event
+      args (dict): the arguments
+    Raises:
+      ValueError
+    """
+    event = str(event)
+    if event not in self.__events:
+      raise ValueError('event {} is unknown'.format(event))
+    self.__events[event].validateArgs(args)
+
+  def validateCall(self, method, inArgs):
+    """Asserts that the given method is a method of this interface and that
+    the given arguments are input arguments of the method
+
+    Args:
+      event (str): the event
+      inArgs (dict): the input arguments
+    Raises:
+      AssertationError
+    """
+    method = str(method)
+    if method not in self.__methods:
+      raise ValueError('method {} is unknown'.format(method))
+    self.__methods[method].validateInArgs(inArgs)
+
+  def validateReturn(self, method, outArgs):
+    """Asserts that the given method is a method of this interface and that
+    the given arguments are output arguments of the method
+
+    Args:
+      event (str): the event
+      outArgs (dict): the output arguments
+    Raises:
+      AssertationError
+    """
+    method = str(method)
+    if method not in self.__methods:
+      raise ValueError('method {} is unknown'.format(method))
+    self.__methods[method].validateOutArgs(outArgs)
+
+  def dump(self):
+    data = {'name': self.name}
+    if self.events:
+      data['events'] = self.events.dump()
+    if self.methods:
+      data['methods'] = self.methods.dump()
+    if self.description != '':
+      data['description'] = self.description
+    return data
+
+  @classmethod
+  def load(cls, data):
+    return cls(**data)
+
+
+class DInterfaces(supcon.util.NamedList):
+  """A readonly map of `supcon.intf.DInterface` instances"""
+  vtype = DInterface
+
+
+class DPath(Named, supcon.util.NamedList):
+  """A named readonly map of `supcon.intf.DInterface` instances"""
+
+  vtype = DInterface
+  regex = re.compile('/(([a-zA-Z0-9_]+/)*[a-zA-Z0-9_]+)?')
+
+  def __init__(self, name, interfaces=None):
+    """Initializes the Path instance
+
+    Args:
+      name (str): the name of the argument
+      interfaces (): ...
+    """
+    if interfaces is None:
+      interfaces = []
+    Named.__init__(self, name)
+    supcon.util.NamedList.__init__(self, interfaces)
+
+  def dump(self):
+    return {
+      'name': self.name,
+      'interfaces': [value.dump() for value in self.values()]
+    }
+
+  @classmethod
+  def load(cls, data) -> 'DPath':
+    return cls(**data)
+
+  def addInterface(self, interface: DInterface) -> 'DPath':
+    if interface.name in self:
+      raise ValueError('interface {} at path {} already exists'.format(interface.name, self.name))
+    interfaces = itertools.chain(self.values(), [interface])
+    return DPath(self.name, interfaces)
+
+  def delInterface(self, interface: DInterface) -> 'DPath':
+    if interface.name not in self:
+      raise ValueError('interface {} at path {} does not exist'.format(interface.name, self.name))
+    interfaces = (v for v in self.values() if v.name != interface.name)
+    return DPath(self.name, interfaces)
+
+
+class DNode(Named, supcon.util.NamedList):
+  """A named readonly map of `supcon.intf.DPath` instances"""
+
+  vtype = DPath
+  regex = re.compile('.+')
+
+  def __init__(self, name, paths=None):
+    """Initializes the Path instance
+
+    Args:
+      name (str): the name of the argument
+      paths (): ...
+    """
+    if paths is None:
+      paths = []
+
+    Named.__init__(self, name)
+    supcon.util.NamedList.__init__(self, paths)
+
+  def dump(self):
+    return {
+      'name': self.name,
+      'paths': [value.dump() for value in self.values()]
+    }
+
+  @classmethod
+  def load(cls, data) -> 'DPath':
+    return cls(**data)
+
+  def hasPath(self, path: str) -> bool:
+    return path in self
+
+  def hasIntf(self, path: str, intf: str) -> bool:
+    return path in self and intf in self[path]
+
+  def addPath(self, path: str) -> 'DNode':
+    if path in self:
+      raise ValueError('path {} on node {} already exists'.format(path, self.name))
+    paths = itertools.chain(self.values(), [DPath(path)])
+    return DNode(self.name, paths)
+
+  def delPath(self, path: str) -> 'DNode':
+    if path not in self:
+      raise ValueError('path {} on node {} does not exist'.format(path, self.name))
+    if self[path]:
+      raise ValueError('path {} on node {} is not empty'.format(path, self.name))
+    paths = (v for v in self.values() if v.name != path)
+    return DNode(self.name, paths)
+
+  def addInterface(self, path: str, interface: DInterface) -> 'DNode':
+    if path not in self:
+      raise ValueError('path {} on node {} does not exist'.format(path, self.name))
+    if interface.name in self[path]:
+      raise ValueError('interface {} at path {} on node {} already exists'.format(interface.name, path, self.name))
+    paths = (v for v in self.values() if v.name != path)
+    paths = itertools.chain(paths, [self[path].addInterface(interface)])
+    return DNode(self.name, paths)
+
+  def delInterface(self, path: str, interface: DInterface) -> 'DNode':
+    if path not in self:
+      raise ValueError('path {} on node {} does not exist'.format(path, self.name))
+    if interface.name not in self[path]:
+      raise ValueError('interface {} at path {} on node {} does not exist'.format(interface.name, path, self.name))
+    paths = (v for v in self.values() if v.name != path)
+    paths = itertools.chain(paths, [self[path].delInterface(interface)])
+    return DNode(self.name, paths)
+
+
+class DNodes(supcon.util.NamedList):
+  """A readonly map of `supcon.intf.DNode` instances"""
+  vtype = DNode
+
+  def hasNode(self, node: str) -> bool:
+    return node in self
+
+  def hasPath(self, node: str, path: str) -> bool:
+    return node in self and path in self[node]
+
+  def hasIntf(self, node: str, path: str, intf: str) -> bool:
+    return node in self and path in self[node] and intf in self[node][path]
+
+  def addNode(self, node: str) -> 'DNodes':
+    if node in self:
+      raise ValueError('node {} already exists'.format(node))
+    nodes = itertools.chain(self.values(), [DNode(node)])
+    return DNodes(nodes)
+
+  def delNode(self, node: str) -> 'DNodes':
+    if node not in self:
+      raise ValueError('node {} does not exist'.format(node))
+    if self[node]:
+      raise ValueError('node {} is not empty'.format(node))
+    nodes = (v for v in self.values() if v.name != node)
+    return DNodes(nodes)
+
+  def addPath(self, node: str, path: str) -> 'DNodes':
+    if node not in self:
+      raise ValueError('node {} does not exist'.format(node))
+    nodes = (v for v in self.values() if v.name != node)
+    nodes = itertools.chain(nodes, [self[node].addPath(path)])
+    return DNodes(nodes)
+
+  def delPath(self, node: str, path: str) -> 'DNodes':
+    if node not in self:
+      raise ValueError('node {} does not exist'.format(node))
+    nodes = (v for v in self.values() if v.name != node)
+    nodes = itertools.chain(nodes, [self[node].delPath(path)])
+    return DNodes(nodes)
+
+  def addInterface(self, node: str, path: str, interface: DInterface) -> 'DNodes':
+    if node not in self:
+      raise ValueError('node {} does not exist'.format(node))
+    nodes = (v for v in self.values() if v.name != node)
+    nodes = itertools.chain(nodes, [self[node].addInterface(path, interface)])
+    return DNodes(nodes)
+
+  def delInterface(self, node: str, path: str, interface: DInterface) -> 'DNodes':
+    if node not in self:
+      raise ValueError('node {} does not exist'.format(node))
+    nodes = (v for v in self.values() if v.name != node)
+    nodes = itertools.chain(nodes, [self[node].delInterface(path, interface)])
+    return DNodes(nodes)
+
+
+class Implementation(object):
+  """The base class for interface implementations"""
+
+  def __init__(self, interface: DInterface):
+    """
+    Args:
+      interface (DInterface): the interface that is implemented by this Implementation
+    """
+    if not isinstance(interface, DInterface):
+      raise ValueError('interface must be an instance of {}'.format(DInterface))
+    self.__interface = interface
+
+    self.__callCbs = {}
+    self.__fireCbs = []
+
+  @property
+  def intf(self) -> str:
+    """str: The name of the implemented interface"""
+    return self.__interface.name
+
+  @property
+  def interface(self) -> DInterface:
+    """Interface: The implemented interface"""
+    return self.__interface
+
+  def setCallCb(self, method: str, cb):
+    """Sets a callback for the given method. The method must be a method of the
+    interface this Implementation implements. The callback must implement the
+    given method. The callback gets called by calls to Implementation.call()
+
+    Args:
+      method (str): the interface method that the callback implements
+      cb (callable): the callback
+    """
+    method = DMethod.toName(method)
+    if method in self.__callCbs:
+      raise ValueError('Callback for method {} is already set!'.format(method))
+    if method not in self.__interface.methods:
+      raise ValueError('Interface has no method {}!'.format(method))
+    self.__callCbs[method] = cb
+
+  def call(self, method: str, inArgs) -> defer.Deferred:
+    """Calls the given interface method with the given arguments. This method
+    calls the callback set by Implementation.setCallCb()
+
+    Args:
+      method (str): the called interface method
+      inArgs (Mapping): a map of input arguments
+    Returns:
+      defer.Deferred: Resolves with the result of the called method
+    """
+    def validateReturn(outArgs):
+      self.__interface.validateReturn(method, outArgs)
+      return outArgs
+
+    try:
+      self.__interface.validateCall(method, inArgs)
+      if method not in self.__callCbs:
+        raise ValueError('Callback for method {} is not set!'.format(method))
+
+      d = self.__callCbs[method](**inArgs)
+      if not isinstance(d, defer.Deferred):
+        d = defer.succeed(d)
+      d.addCallback(validateReturn)
+    except BaseException as e:
+      traceback.print_exc()
+      d = defer.fail(e)
+
+    return d
+
+  def addFireCb(self, cb):
+    """Adds a callback that gets called, when this Implementation fires an
+    event.
+
+    Args:
+      cb (callable): the callback
+    """
+    self.__fireCbs.append(cb)
+
+  def delFireCb(self, cb):
+    """Removes a callback that gets called, when this Implementation fires an
+    event.
+
+    Args:
+      cb (callable): the callback
+    """
+    self.__fireCbs.remove(cb)
+
+  def fire(self, event: str, args):
+    """Fires the given event with the given arguments.
+
+    Args:
+      event (str): the event name
+      args (collecion.Mapping): the event arguments
+    """
+    self.__interface.validateEvent(event, args)
+
+    for cb in self.__fireCbs:
+      cb(event, args)
+
+
+
+class Object(object):
+
+  def __init__(self, interfaces=None):
+    """
+    Args:
+      interfaces (DInterfaces): the interfaces that are implemented by this Object
+    """
+    if interfaces is None:
+      interfaces = []
+
+    self.__intfs = {}
+    self.__impls = {}
+
+    def getCallCb(intf):
+      return lambda method, inArgs: self.__call(intf, method, inArgs)
+
+    for interface in interfaces:
+      if not isinstance(interface, DInterface):
+        raise ValueError('interface must be an instance of {}'.format(DInterface))
+      self.__intfs[interface.name] = interface
+
+      implementation = Implementation(interface)
+      callCb = getCallCb(implementation.intf)
+      for method in interface.methods:
+        implementation.setCallCb(method, callCb)
+      self.__impls[implementation.intf] = implementation
+
+  @property
+  def interfaces(self):
+    return self.__intfs.values()
+
+  @property
+  def implementations(self):
+    return self.__impls.values()
+
+  def interface(self, intf):
+    return self.__intfs[intf]
+
+  def implementation(self, intf):
+    return self.__impls[intf]
+
+  def __call(self, intf: str, method: str, inArgs) -> defer.Deferred:
+    pass
+
+
+class Node(abc.ABC):
+  """The Node Interface. This class defines the methods that participants can
+  use to access the supcon bus."""
+
+  def __init__(self, name):
+    super().__init__()
+    self.__name = self.toName(name)
+
+  @property
+  def name(self) -> str:
+    """str: The name of the node on the bus"""
+    return self.__name
+
+  @classmethod
+  def toName(cls, value) -> str:
+    return DNode.toName(value)
+
+  @abc.abstractmethod
+  def nodes(self) -> DNodes:
+    """list[str]: The currently connected nodes"""
+    raise NotImplementedError()
+
+  @abc.abstractmethod
+  def connect(self, endpoint):
+    """Connects the node to the given endpoint.
+
+    If the connection failes or closes the connection gets reestablished with an
+    exponential timeout up to two minutes.
+
+    Args:
+      endpoint (twisted.internet.interfaces.IStreamClientEndpoint):
+    """
+    raise NotImplementedError()
+
+  @abc.abstractmethod
+  def listen(self, endpoint):
+    """Listens at the given endpoint for incoming connections
+
+    Args:
+      endpoint (twisted.internet.interfaces.IStreamServerEndpoint):
+    """
+    raise NotImplementedError()
+
+  @abc.abstractmethod
+  def register(self, path: str, impl: Implementation):
+    """Registers an implementation with the node
+
+    Args:
+      impl (Implementation):
+    """
+    raise NotImplementedError()
+
+  @abc.abstractmethod
+  def unregister(self, path: str, impl: Implementation):
+    """Removes an implementation from the node
+
+    Args:
+      impl (supcon.intf.Implementation):
+    """
+    raise NotImplementedError()
+
+  @abc.abstractmethod
+  def call(self, node: str, path: str, intf: str, method: str, args: dict) -> defer.Deferred:
+    """Calls a method on the bus
+
+    Args:
+      node (str): a node on the bus
+      path (str): a path on the given node
+      intf (str): an interface at the given path
+      method (str): a method of the given interface
+      args (dict): a dict of method arguments
+
+    Returns:
+      defer.Deferred:
+    """
+    raise NotImplementedError()
+
+  @abc.abstractmethod
+  def on(self, node: str, path: str, intf: str, event: str, cb):
+    """Registers a callback for an event on the bus
+
+    Args:
+      node (str): a node on the bus
+      path (str): a path on the given node
+      intf (str): an interface at the given path
+      event (str): a method of the given interface
+      cb (callable): a callable that gets called with a dict of event arguments
+    """
+    raise NotImplementedError()
+
+  @abc.abstractmethod
+  def off(self, node: str, path: str, intf: str, event: str, cb):
+    """Unregisters a callback for an event on the bus
+
+    Args:
+      node (str): a node on the bus
+      path (str): a path on the given node
+      intf (str): an interface at the given path
+      event (str): a method of the given interface
+      cb (callable): a callable that gets called with a dict of event arguments
+    """
+    raise NotImplementedError()
+
+ +
+ +
+ +
+ + +

Classes

+ +
+

class DArgument

+ + +

Describes an input or output argument of a DMethod or an +argument of a DEvent

+
+ +
+
class DArgument(supcon.util.Dumpable, NamedAndDescribed):
+  """Describes an input or output argument of a `supcon.intf.DMethod` or an
+  argument of a `supcon.intf.DEvent`"""
+
+  regex = re.compile('[a-zA-Z0-9]+')
+
+  def validate(self, argument):
+    """Validates the given argument value. Raises an ValueError if validation
+    fails.
+    """
+    pass
+
+  def dump(self):
+    data = {'name': self.name}
+    if self.description != '':
+      data['description'] = self.description
+    return data
+
+  @classmethod
+  def load(cls, data):
+    return cls(**data)
+
+ +
+
+ + +
+

Ancestors (in MRO)

+ +

Class variables

+
+

var regex

+ + + + +
+
+ +
+

Class Methods

+ +
+
+

def load(

cls, data)

+
+ + + + +
+ +
+
@classmethod
+def load(cls, data):
+  return cls(**data)
+
+ +
+
+ +
+ + +
+
+

def toDescription(

cls, value)

+
+ + + + +

Converts the value into a description. If this is impossible a ValueError +is raised.

+

Args: + value (any): the value +Raises: + ValueError

+
+ +
+
@classmethod
+def toDescription(cls, value) -> str:
+  """Converts the value into a description. If this is impossible a ValueError
+  is raised.
+  Args:
+    value (any): the value
+  Raises:
+    ValueError
+  """
+  return str(value)
+
+ +
+
+ +
+ + +
+
+

def toName(

cls, value)

+
+ + + + +

Converts the value into a name. If this is impossible a ValueError is +raised.

+

Args: + value (any): the value +Raises: + ValueError

+
+ +
+
@classmethod
+def toName(cls, value) -> str:
+  """Converts the value into a name. If this is impossible a ValueError is
+  raised.
+  Args:
+    value (any): the value
+  Raises:
+    ValueError
+  """
+  value = str(value)
+  if not cls.regex.fullmatch(value):
+    raise ValueError("value {} must match {} fully".format(value, cls.regex.pattern))
+  return value
+
+ +
+
+ +
+ +

Instance variables

+
+

var description

+ +

+ Inheritance: + NamedAndDescribed.description +

+ + + +

str: a description

+
+
+ +
+
+

var name

+ +

+ Inheritance: + Named.name +

+ + + +

str: the name

+
+
+ +
+

Methods

+ +
+
+

def __init__(

self, name, description='')

+
+ + + + +

Initializes the NamedAndDescribed instance

+

Args: + name (str): the name of the argument + description (str): a description of the argument

+
+ +
+
def __init__(self, name, description=''):
+  """Initializes the NamedAndDescribed instance
+  Args:
+    name (str): the name of the argument
+    description (str): a description of the argument
+  """
+  Named.__init__(self, name)
+  self.__description = self.toDescription(description)
+
+ +
+
+ +
+ + +
+
+

def dump(

self)

+
+ + + + +

Converts the supcon.util.Dumpable into a 'simple' datastructure.

+
+ +
+
def dump(self):
+  data = {'name': self.name}
+  if self.description != '':
+    data['description'] = self.description
+  return data
+
+ +
+
+ +
+ + +
+
+

def validate(

self, argument)

+
+ + + + +

Validates the given argument value. Raises an ValueError if validation +fails.

+
+ +
+
def validate(self, argument):
+  """Validates the given argument value. Raises an ValueError if validation
+  fails.
+  """
+  pass
+
+ +
+
+ +
+ +
+
+ +
+

class DArguments

+ + +

A readonly map of DArgument instances

+
+ +
+
class DArguments(supcon.util.NamedList):
+  """A readonly map of `supcon.intf.DArgument` instances"""
+  vtype = DArgument
+
+ +
+
+ + +
+

Ancestors (in MRO)

+
    +
  • DArguments
  • +
  • supcon.util.NamedList
  • +
  • collections.abc.Mapping
  • +
  • collections.abc.Collection
  • +
  • collections.abc.Sized
  • +
  • collections.abc.Iterable
  • +
  • collections.abc.Container
  • +
  • supcon.util.Dumpable
  • +
  • abc.ABC
  • +
  • builtins.object
  • +
+

Class variables

+
+

var vtype

+ + + + +
+
+ +
+

Class Methods

+ +
+
+

def load(

cls, data)

+
+ + + + +

Converts the given list of 'simple' datastructures data into an instance +of cls, a subclass of supcon.util.NamedList.

+
+ +
+
@classmethod
+def load(cls, data):
+  """
+  Converts the given list of 'simple' datastructures **data** into an instance
+  of **cls**, a subclass of `supcon.util.NamedList`.
+  """
+  return cls(data)
+
+ +
+
+ +
+ + +
+
+

def to(

cls, value)

+
+ + + + +

If the the value is an instance of cls, a subclass of +supcon.util.NamedList, the value is returned. If value is not an instance +of cls the method tries to convert the value into an instance of +cls.

+
+ +
+
@classmethod
+def to(cls, value):
+  """
+  If the the **value** is an instance of **cls**, a subclass of
+  `supcon.util.NamedList`, the value is returned. If value is not an instance
+  of **cls** the method tries to convert the **value** into an instance of
+  **cls**.
+  """
+  return value if isinstance(value, cls) else cls.load(value)
+
+ +
+
+ +
+ +

Methods

+ +
+
+

def __init__(

self, values=None)

+
+ + + + +

Initializes the supcon.util.NamedList with the given list of values.

+

An element of values must be of type supcon.util.NamedList.vtype or a +'simple' datastructure that can be converted to an instance of +supcon.util.NamedList.vtype by supcon.util.NamedList.vtype.load().

+
+ +
+
def __init__(self, values=None):
+  """
+  Initializes the `supcon.util.NamedList` with the given list of **values**.
+  An element of **values** must be of type `supcon.util.NamedList.vtype` or a
+  'simple' datastructure that can be converted to an instance of
+  `supcon.util.NamedList.vtype` by `supcon.util.NamedList.vtype`.load().
+  """
+  if values is None:
+    values = []
+  if not issubclass(self.vtype, Named):
+    raise ValueError('the value type must be a subclass of Named')
+  if not issubclass(self.vtype, Dumpable):
+    raise ValueError('the value type must be a subclass of Dumpable')
+  self.__values = collections.OrderedDict()
+  for value in values:
+    if not isinstance(value, self.vtype):
+      value = self.vtype.load(value)
+    if value.name in self.__values:
+      raise ValueError('value.name must be unique in the given list of values')
+    self.__values[value.name] = value
+
+ +
+
+ +
+ + +
+
+

def dump(

self)

+
+ + + + +

Converts the supcon.util.NamedList into a list of 'simple' datastructures.

+
+ +
+
def dump(self):
+  """
+  Converts the `supcon.util.NamedList` into a list of 'simple' datastructures.
+  """
+  return [value.dump() for value in self.values()]
+
+ +
+
+ +
+ + +
+
+

def get(

self, key, default=None)

+
+ + + + +

D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None.

+
+ +
+
def get(self, key, default=None):
+    'D.get(k[,d]) -> D[k] if k in D, else d.  d defaults to None.'
+    try:
+        return self[key]
+    except KeyError:
+        return default
+
+ +
+
+ +
+ + +
+
+

def items(

self)

+
+ + + + +

D.items() -> a set-like object providing a view on D's items

+
+ +
+
def items(self):
+    "D.items() -> a set-like object providing a view on D's items"
+    return ItemsView(self)
+
+ +
+
+ +
+ + +
+
+

def keys(

self)

+
+ + + + +

D.keys() -> a set-like object providing a view on D's keys

+
+ +
+
def keys(self):
+    "D.keys() -> a set-like object providing a view on D's keys"
+    return KeysView(self)
+
+ +
+
+ +
+ + +
+
+

def values(

self)

+
+ + + + +

D.values() -> an object providing a view on D's values

+
+ +
+
def values(self):
+    "D.values() -> an object providing a view on D's values"
+    return ValuesView(self)
+
+ +
+
+ +
+ +
+
+ +
+

class DEvent

+ + +

Describes an event that can be emitted by an implementation on the bus

+
+ +
+
class DEvent(supcon.util.Dumpable, NamedAndDescribed):
+  """Describes an event that can be emitted by an implementation on the bus"""
+
+  regex = re.compile('[a-zA-Z0-9]+')
+
+  def __init__(self, name, args=DArguments(), description=''):
+    """Initializes the Event
+
+    Args:
+      name (str): the name of the event
+      args (DArguments): the list of arguments of the event
+      description (str): a description of the event
+    """
+    NamedAndDescribed.__init__(self, name, description)
+    self.__args = DArguments.to(args)
+
+  @property
+  def args(self) -> DArguments:
+    """DArguments: the list of arguments of the event"""
+    return self.__args
+
+  def validateArgs(self, args):
+    """Validates the given argument map. Raises an ValueError if validation
+    fails.
+    """
+    for arg in args:
+      if arg not in self.__args:
+        raise ValueError('event {} has no argument {}'.format(self.name, arg))
+      self.__args[arg].validate(args[arg])
+    for arg in self.__args:
+      if arg not in args:
+        raise ValueError('event {} needs argument {}'.format(self.name, arg))
+
+  def dump(self):
+    data = {'name': self.name}
+    if self.args:
+      data['args'] = self.args.dump()
+    if self.description != '':
+      data['description'] = self.description
+    return data
+
+  @classmethod
+  def load(cls, data):
+    return cls(**data)
+
+ +
+
+ + +
+

Ancestors (in MRO)

+ +

Class variables

+
+

var regex

+ + + + +
+
+ +
+

Class Methods

+ +
+
+

def load(

cls, data)

+
+ + + + +
+ +
+
@classmethod
+def load(cls, data):
+  return cls(**data)
+
+ +
+
+ +
+ + +
+
+

def toDescription(

cls, value)

+
+ + + + +

Converts the value into a description. If this is impossible a ValueError +is raised.

+

Args: + value (any): the value +Raises: + ValueError

+
+ +
+
@classmethod
+def toDescription(cls, value) -> str:
+  """Converts the value into a description. If this is impossible a ValueError
+  is raised.
+  Args:
+    value (any): the value
+  Raises:
+    ValueError
+  """
+  return str(value)
+
+ +
+
+ +
+ + +
+
+

def toName(

cls, value)

+
+ + + + +

Converts the value into a name. If this is impossible a ValueError is +raised.

+

Args: + value (any): the value +Raises: + ValueError

+
+ +
+
@classmethod
+def toName(cls, value) -> str:
+  """Converts the value into a name. If this is impossible a ValueError is
+  raised.
+  Args:
+    value (any): the value
+  Raises:
+    ValueError
+  """
+  value = str(value)
+  if not cls.regex.fullmatch(value):
+    raise ValueError("value {} must match {} fully".format(value, cls.regex.pattern))
+  return value
+
+ +
+
+ +
+ +

Instance variables

+
+

var args

+ + + + +

DArguments: the list of arguments of the event

+
+
+ +
+
+

var description

+ + + + +

str: a description

+
+
+ +
+
+

var name

+ + + + +

str: the name

+
+
+ +
+

Methods

+ +
+
+

def __init__(

self, name, args=supcon.intf.DArguments(), description='')

+
+ + + + +

Initializes the Event

+

Args: + name (str): the name of the event + args (DArguments): the list of arguments of the event + description (str): a description of the event

+
+ +
+
def __init__(self, name, args=DArguments(), description=''):
+  """Initializes the Event
+  Args:
+    name (str): the name of the event
+    args (DArguments): the list of arguments of the event
+    description (str): a description of the event
+  """
+  NamedAndDescribed.__init__(self, name, description)
+  self.__args = DArguments.to(args)
+
+ +
+
+ +
+ + +
+
+

def dump(

self)

+
+ + + + +

Converts the supcon.util.Dumpable into a 'simple' datastructure.

+
+ +
+
def dump(self):
+  data = {'name': self.name}
+  if self.args:
+    data['args'] = self.args.dump()
+  if self.description != '':
+    data['description'] = self.description
+  return data
+
+ +
+
+ +
+ + +
+
+

def validateArgs(

self, args)

+
+ + + + +

Validates the given argument map. Raises an ValueError if validation +fails.

+
+ +
+
def validateArgs(self, args):
+  """Validates the given argument map. Raises an ValueError if validation
+  fails.
+  """
+  for arg in args:
+    if arg not in self.__args:
+      raise ValueError('event {} has no argument {}'.format(self.name, arg))
+    self.__args[arg].validate(args[arg])
+  for arg in self.__args:
+    if arg not in args:
+      raise ValueError('event {} needs argument {}'.format(self.name, arg))
+
+ +
+
+ +
+ +
+
+ +
+

class DEvents

+ + +

A readonly map of DEvent instances

+
+ +
+
class DEvents(supcon.util.NamedList):
+  """A readonly map of `supcon.intf.DEvent` instances"""
+  vtype = DEvent
+
+ +
+
+ + +
+

Ancestors (in MRO)

+
    +
  • DEvents
  • +
  • supcon.util.NamedList
  • +
  • collections.abc.Mapping
  • +
  • collections.abc.Collection
  • +
  • collections.abc.Sized
  • +
  • collections.abc.Iterable
  • +
  • collections.abc.Container
  • +
  • supcon.util.Dumpable
  • +
  • abc.ABC
  • +
  • builtins.object
  • +
+

Class variables

+
+

var vtype

+ + + + +
+
+ +
+

Class Methods

+ +
+
+

def load(

cls, data)

+
+ + + + +

Converts the given list of 'simple' datastructures data into an instance +of cls, a subclass of supcon.util.NamedList.

+
+ +
+
@classmethod
+def load(cls, data):
+  """
+  Converts the given list of 'simple' datastructures **data** into an instance
+  of **cls**, a subclass of `supcon.util.NamedList`.
+  """
+  return cls(data)
+
+ +
+
+ +
+ + +
+
+

def to(

cls, value)

+
+ + + + +

If the the value is an instance of cls, a subclass of +supcon.util.NamedList, the value is returned. If value is not an instance +of cls the method tries to convert the value into an instance of +cls.

+
+ +
+
@classmethod
+def to(cls, value):
+  """
+  If the the **value** is an instance of **cls**, a subclass of
+  `supcon.util.NamedList`, the value is returned. If value is not an instance
+  of **cls** the method tries to convert the **value** into an instance of
+  **cls**.
+  """
+  return value if isinstance(value, cls) else cls.load(value)
+
+ +
+
+ +
+ +

Methods

+ +
+
+

def __init__(

self, values=None)

+
+ + + + +

Initializes the supcon.util.NamedList with the given list of values.

+

An element of values must be of type supcon.util.NamedList.vtype or a +'simple' datastructure that can be converted to an instance of +supcon.util.NamedList.vtype by supcon.util.NamedList.vtype.load().

+
+ +
+
def __init__(self, values=None):
+  """
+  Initializes the `supcon.util.NamedList` with the given list of **values**.
+  An element of **values** must be of type `supcon.util.NamedList.vtype` or a
+  'simple' datastructure that can be converted to an instance of
+  `supcon.util.NamedList.vtype` by `supcon.util.NamedList.vtype`.load().
+  """
+  if values is None:
+    values = []
+  if not issubclass(self.vtype, Named):
+    raise ValueError('the value type must be a subclass of Named')
+  if not issubclass(self.vtype, Dumpable):
+    raise ValueError('the value type must be a subclass of Dumpable')
+  self.__values = collections.OrderedDict()
+  for value in values:
+    if not isinstance(value, self.vtype):
+      value = self.vtype.load(value)
+    if value.name in self.__values:
+      raise ValueError('value.name must be unique in the given list of values')
+    self.__values[value.name] = value
+
+ +
+
+ +
+ + +
+
+

def dump(

self)

+
+ + + + +

Converts the supcon.util.NamedList into a list of 'simple' datastructures.

+
+ +
+
def dump(self):
+  """
+  Converts the `supcon.util.NamedList` into a list of 'simple' datastructures.
+  """
+  return [value.dump() for value in self.values()]
+
+ +
+
+ +
+ + +
+
+

def get(

self, key, default=None)

+
+ + + + +

D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None.

+
+ +
+
def get(self, key, default=None):
+    'D.get(k[,d]) -> D[k] if k in D, else d.  d defaults to None.'
+    try:
+        return self[key]
+    except KeyError:
+        return default
+
+ +
+
+ +
+ + +
+
+

def items(

self)

+
+ + + + +

D.items() -> a set-like object providing a view on D's items

+
+ +
+
def items(self):
+    "D.items() -> a set-like object providing a view on D's items"
+    return ItemsView(self)
+
+ +
+
+ +
+ + +
+
+

def keys(

self)

+
+ + + + +

D.keys() -> a set-like object providing a view on D's keys

+
+ +
+
def keys(self):
+    "D.keys() -> a set-like object providing a view on D's keys"
+    return KeysView(self)
+
+ +
+
+ +
+ + +
+
+

def values(

self)

+
+ + + + +

D.values() -> an object providing a view on D's values

+
+ +
+
def values(self):
+    "D.values() -> an object providing a view on D's values"
+    return ValuesView(self)
+
+ +
+
+ +
+ +
+
+ +
+

class DInterface

+ + +

An Interface that is implemented by an Object on the Bus

+
+ +
+
class DInterface(supcon.util.Dumpable, NamedAndDescribed):
+  """An Interface that is implemented by an Object on the Bus"""
+
+  regex = re.compile('([a-zA-Z0-9_]+\\.)*[a-zA-Z0-9_]+')
+
+  def __init__(self, name, events=DEvents(), methods=DMethods(), description=''):
+    """Initializes the Event
+
+    Args:
+      name (str): the name of the interface
+      events (DEvents): the list of events of the interface
+      methods (DMethods): the list of methods of the interface
+      description (str): a description of the interface
+    """
+    NamedAndDescribed.__init__(self, name, description)
+    self.__events = DEvents.to(events)
+    self.__methods = DMethods.to(methods)
+
+  @property
+  def events(self) -> DEvents:
+    """DEvent: the list of events this interface can emit"""
+    return self.__events
+
+  @property
+  def methods(self) -> DMethods:
+    """DMethods: the list of methods this interface provides"""
+    return self.__methods
+
+  def validateEvent(self, event, args):
+    """Validates that the given event is an event of this interface and that
+    the given arguments are arguments of the event
+
+    Args:
+      event (str): the event
+      args (dict): the arguments
+    Raises:
+      ValueError
+    """
+    event = str(event)
+    if event not in self.__events:
+      raise ValueError('event {} is unknown'.format(event))
+    self.__events[event].validateArgs(args)
+
+  def validateCall(self, method, inArgs):
+    """Asserts that the given method is a method of this interface and that
+    the given arguments are input arguments of the method
+
+    Args:
+      event (str): the event
+      inArgs (dict): the input arguments
+    Raises:
+      AssertationError
+    """
+    method = str(method)
+    if method not in self.__methods:
+      raise ValueError('method {} is unknown'.format(method))
+    self.__methods[method].validateInArgs(inArgs)
+
+  def validateReturn(self, method, outArgs):
+    """Asserts that the given method is a method of this interface and that
+    the given arguments are output arguments of the method
+
+    Args:
+      event (str): the event
+      outArgs (dict): the output arguments
+    Raises:
+      AssertationError
+    """
+    method = str(method)
+    if method not in self.__methods:
+      raise ValueError('method {} is unknown'.format(method))
+    self.__methods[method].validateOutArgs(outArgs)
+
+  def dump(self):
+    data = {'name': self.name}
+    if self.events:
+      data['events'] = self.events.dump()
+    if self.methods:
+      data['methods'] = self.methods.dump()
+    if self.description != '':
+      data['description'] = self.description
+    return data
+
+  @classmethod
+  def load(cls, data):
+    return cls(**data)
+
+ +
+
+ + +
+

Ancestors (in MRO)

+ +

Class variables

+
+

var regex

+ + + + +
+
+ +
+

Class Methods

+ +
+
+

def load(

cls, data)

+
+ + + + +
+ +
+
@classmethod
+def load(cls, data):
+  return cls(**data)
+
+ +
+
+ +
+ + +
+
+

def toDescription(

cls, value)

+
+ + + + +

Converts the value into a description. If this is impossible a ValueError +is raised.

+

Args: + value (any): the value +Raises: + ValueError

+
+ +
+
@classmethod
+def toDescription(cls, value) -> str:
+  """Converts the value into a description. If this is impossible a ValueError
+  is raised.
+  Args:
+    value (any): the value
+  Raises:
+    ValueError
+  """
+  return str(value)
+
+ +
+
+ +
+ + +
+
+

def toName(

cls, value)

+
+ + + + +

Converts the value into a name. If this is impossible a ValueError is +raised.

+

Args: + value (any): the value +Raises: + ValueError

+
+ +
+
@classmethod
+def toName(cls, value) -> str:
+  """Converts the value into a name. If this is impossible a ValueError is
+  raised.
+  Args:
+    value (any): the value
+  Raises:
+    ValueError
+  """
+  value = str(value)
+  if not cls.regex.fullmatch(value):
+    raise ValueError("value {} must match {} fully".format(value, cls.regex.pattern))
+  return value
+
+ +
+
+ +
+ +

Instance variables

+
+

var description

+ +

+ Inheritance: + NamedAndDescribed.description +

+ + + +

str: a description

+
+
+ +
+
+

var events

+ + + + +

DEvent: the list of events this interface can emit

+
+
+ +
+
+

var methods

+ + + + +

DMethods: the list of methods this interface provides

+
+
+ +
+
+

var name

+ + + + +

str: the name

+
+
+ +
+

Methods

+ +
+
+

def __init__(

self, name, events=supcon.intf.DEvents(), methods=supcon.intf.DMethods(), description='')

+
+ + + + +

Initializes the Event

+

Args: + name (str): the name of the interface + events (DEvents): the list of events of the interface + methods (DMethods): the list of methods of the interface + description (str): a description of the interface

+
+ +
+
def __init__(self, name, events=DEvents(), methods=DMethods(), description=''):
+  """Initializes the Event
+  Args:
+    name (str): the name of the interface
+    events (DEvents): the list of events of the interface
+    methods (DMethods): the list of methods of the interface
+    description (str): a description of the interface
+  """
+  NamedAndDescribed.__init__(self, name, description)
+  self.__events = DEvents.to(events)
+  self.__methods = DMethods.to(methods)
+
+ +
+
+ +
+ + +
+
+

def dump(

self)

+
+ + + + +

Converts the supcon.util.Dumpable into a 'simple' datastructure.

+
+ +
+
def dump(self):
+  data = {'name': self.name}
+  if self.events:
+    data['events'] = self.events.dump()
+  if self.methods:
+    data['methods'] = self.methods.dump()
+  if self.description != '':
+    data['description'] = self.description
+  return data
+
+ +
+
+ +
+ + +
+
+

def validateCall(

self, method, inArgs)

+
+ + + + +

Asserts that the given method is a method of this interface and that +the given arguments are input arguments of the method

+

Args: + event (str): the event + inArgs (dict): the input arguments +Raises: + AssertationError

+
+ +
+
def validateCall(self, method, inArgs):
+  """Asserts that the given method is a method of this interface and that
+  the given arguments are input arguments of the method
+  Args:
+    event (str): the event
+    inArgs (dict): the input arguments
+  Raises:
+    AssertationError
+  """
+  method = str(method)
+  if method not in self.__methods:
+    raise ValueError('method {} is unknown'.format(method))
+  self.__methods[method].validateInArgs(inArgs)
+
+ +
+
+ +
+ + +
+
+

def validateEvent(

self, event, args)

+
+ + + + +

Validates that the given event is an event of this interface and that +the given arguments are arguments of the event

+

Args: + event (str): the event + args (dict): the arguments +Raises: + ValueError

+
+ +
+
def validateEvent(self, event, args):
+  """Validates that the given event is an event of this interface and that
+  the given arguments are arguments of the event
+  Args:
+    event (str): the event
+    args (dict): the arguments
+  Raises:
+    ValueError
+  """
+  event = str(event)
+  if event not in self.__events:
+    raise ValueError('event {} is unknown'.format(event))
+  self.__events[event].validateArgs(args)
+
+ +
+
+ +
+ + +
+
+

def validateReturn(

self, method, outArgs)

+
+ + + + +

Asserts that the given method is a method of this interface and that +the given arguments are output arguments of the method

+

Args: + event (str): the event + outArgs (dict): the output arguments +Raises: + AssertationError

+
+ +
+
def validateReturn(self, method, outArgs):
+  """Asserts that the given method is a method of this interface and that
+  the given arguments are output arguments of the method
+  Args:
+    event (str): the event
+    outArgs (dict): the output arguments
+  Raises:
+    AssertationError
+  """
+  method = str(method)
+  if method not in self.__methods:
+    raise ValueError('method {} is unknown'.format(method))
+  self.__methods[method].validateOutArgs(outArgs)
+
+ +
+
+ +
+ +
+
+ +
+

class DInterfaces

+ + +

A readonly map of DInterface instances

+
+ +
+
class DInterfaces(supcon.util.NamedList):
+  """A readonly map of `supcon.intf.DInterface` instances"""
+  vtype = DInterface
+
+ +
+
+ + +
+

Ancestors (in MRO)

+
    +
  • DInterfaces
  • +
  • supcon.util.NamedList
  • +
  • collections.abc.Mapping
  • +
  • collections.abc.Collection
  • +
  • collections.abc.Sized
  • +
  • collections.abc.Iterable
  • +
  • collections.abc.Container
  • +
  • supcon.util.Dumpable
  • +
  • abc.ABC
  • +
  • builtins.object
  • +
+

Class variables

+
+

var vtype

+ + + + +
+
+ +
+

Class Methods

+ +
+
+

def load(

cls, data)

+
+ + + + +

Converts the given list of 'simple' datastructures data into an instance +of cls, a subclass of supcon.util.NamedList.

+
+ +
+
@classmethod
+def load(cls, data):
+  """
+  Converts the given list of 'simple' datastructures **data** into an instance
+  of **cls**, a subclass of `supcon.util.NamedList`.
+  """
+  return cls(data)
+
+ +
+
+ +
+ + +
+
+

def to(

cls, value)

+
+ + + + +

If the the value is an instance of cls, a subclass of +supcon.util.NamedList, the value is returned. If value is not an instance +of cls the method tries to convert the value into an instance of +cls.

+
+ +
+
@classmethod
+def to(cls, value):
+  """
+  If the the **value** is an instance of **cls**, a subclass of
+  `supcon.util.NamedList`, the value is returned. If value is not an instance
+  of **cls** the method tries to convert the **value** into an instance of
+  **cls**.
+  """
+  return value if isinstance(value, cls) else cls.load(value)
+
+ +
+
+ +
+ +

Methods

+ +
+
+

def __init__(

self, values=None)

+
+ + + + +

Initializes the supcon.util.NamedList with the given list of values.

+

An element of values must be of type supcon.util.NamedList.vtype or a +'simple' datastructure that can be converted to an instance of +supcon.util.NamedList.vtype by supcon.util.NamedList.vtype.load().

+
+ +
+
def __init__(self, values=None):
+  """
+  Initializes the `supcon.util.NamedList` with the given list of **values**.
+  An element of **values** must be of type `supcon.util.NamedList.vtype` or a
+  'simple' datastructure that can be converted to an instance of
+  `supcon.util.NamedList.vtype` by `supcon.util.NamedList.vtype`.load().
+  """
+  if values is None:
+    values = []
+  if not issubclass(self.vtype, Named):
+    raise ValueError('the value type must be a subclass of Named')
+  if not issubclass(self.vtype, Dumpable):
+    raise ValueError('the value type must be a subclass of Dumpable')
+  self.__values = collections.OrderedDict()
+  for value in values:
+    if not isinstance(value, self.vtype):
+      value = self.vtype.load(value)
+    if value.name in self.__values:
+      raise ValueError('value.name must be unique in the given list of values')
+    self.__values[value.name] = value
+
+ +
+
+ +
+ + +
+
+

def dump(

self)

+
+ + + + +

Converts the supcon.util.NamedList into a list of 'simple' datastructures.

+
+ +
+
def dump(self):
+  """
+  Converts the `supcon.util.NamedList` into a list of 'simple' datastructures.
+  """
+  return [value.dump() for value in self.values()]
+
+ +
+
+ +
+ + +
+
+

def get(

self, key, default=None)

+
+ + + + +

D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None.

+
+ +
+
def get(self, key, default=None):
+    'D.get(k[,d]) -> D[k] if k in D, else d.  d defaults to None.'
+    try:
+        return self[key]
+    except KeyError:
+        return default
+
+ +
+
+ +
+ + +
+
+

def items(

self)

+
+ + + + +

D.items() -> a set-like object providing a view on D's items

+
+ +
+
def items(self):
+    "D.items() -> a set-like object providing a view on D's items"
+    return ItemsView(self)
+
+ +
+
+ +
+ + +
+
+

def keys(

self)

+
+ + + + +

D.keys() -> a set-like object providing a view on D's keys

+
+ +
+
def keys(self):
+    "D.keys() -> a set-like object providing a view on D's keys"
+    return KeysView(self)
+
+ +
+
+ +
+ + +
+
+

def values(

self)

+
+ + + + +

D.values() -> an object providing a view on D's values

+
+ +
+
def values(self):
+    "D.values() -> an object providing a view on D's values"
+    return ValuesView(self)
+
+ +
+
+ +
+ +
+
+ +
+

class DMethod

+ + +

A DMethod that can be called on an Object on the Bus

+
+ +
+
class DMethod(supcon.util.Dumpable, NamedAndDescribed):
+  """A DMethod that can be called on an Object on the Bus"""
+
+  regex = re.compile('[a-zA-Z0-9]+')
+
+  def __init__(self, name, inArgs=DArguments(), outArgs=DArguments(), description=''):
+    """Initializes the Event
+
+    Args:
+      name (str): the name of the method
+      inArgs (DArguments): the list of input arguments of the method
+      outArgs (DArguments): the list of output arguments of the method
+      description (str): a description of the method
+    """
+    NamedAndDescribed.__init__(self, name, description)
+    self.__inArgs = DArguments.to(inArgs)#
+    self.__outArgs = DArguments.to(outArgs)
+
+  @property
+  def inArgs(self) -> DArguments:
+    """DArguments: The input arguments of the method"""
+    return self.__inArgs
+
+  @property
+  def outArgs(self) -> DArguments:
+    """DArguments: The output arguments of the method"""
+    return self.__outArgs
+
+  def validateInArgs(self, inArgs):
+    """Validates the given argument Mapping. Raises an ValueError if validation fails
+    """
+    for arg in inArgs:
+      if arg not in self.__inArgs:
+        raise ValueError('method {} has no input argument {}'.format(self.name, arg))
+      self.__inArgs[arg].validate(inArgs[arg])
+    for arg in self.__inArgs:
+      if arg not in inArgs:
+        raise ValueError('method {} needs input argument {}'.format(self.name, arg))
+
+  def validateOutArgs(self, outArgs):
+    """Validates the given argument Mapping. Raises an ValueError if validation fails
+    """
+    for arg in outArgs:
+      if arg not in self.__outArgs:
+        raise ValueError('method {} has no output argument {}'.format(self.name, arg))
+      self.__outArgs[arg].validate(outArgs[arg])
+    for arg in self.__outArgs:
+      if arg not in outArgs:
+        raise ValueError('method {} needs output argument {}'.format(self.name, arg))
+
+  def dump(self):
+    data = {'name': self.name}
+    if self.inArgs:
+      data['inArgs'] = self.inArgs.dump()
+    if self.outArgs:
+      data['outArgs'] = self.outArgs.dump()
+    if self.description != '':
+      data['description'] = self.description
+    return data
+
+  @classmethod
+  def load(cls, data):
+    return cls(**data)
+
+ +
+
+ + +
+

Ancestors (in MRO)

+ +

Class variables

+
+

var regex

+ + + + +
+
+ +
+

Class Methods

+ +
+
+

def load(

cls, data)

+
+ + + + +
+ +
+
@classmethod
+def load(cls, data):
+  return cls(**data)
+
+ +
+
+ +
+ + +
+
+

def toDescription(

cls, value)

+
+ + + + +

Converts the value into a description. If this is impossible a ValueError +is raised.

+

Args: + value (any): the value +Raises: + ValueError

+
+ +
+
@classmethod
+def toDescription(cls, value) -> str:
+  """Converts the value into a description. If this is impossible a ValueError
+  is raised.
+  Args:
+    value (any): the value
+  Raises:
+    ValueError
+  """
+  return str(value)
+
+ +
+
+ +
+ + +
+
+

def toName(

cls, value)

+
+ + + + +

Converts the value into a name. If this is impossible a ValueError is +raised.

+

Args: + value (any): the value +Raises: + ValueError

+
+ +
+
@classmethod
+def toName(cls, value) -> str:
+  """Converts the value into a name. If this is impossible a ValueError is
+  raised.
+  Args:
+    value (any): the value
+  Raises:
+    ValueError
+  """
+  value = str(value)
+  if not cls.regex.fullmatch(value):
+    raise ValueError("value {} must match {} fully".format(value, cls.regex.pattern))
+  return value
+
+ +
+
+ +
+ +

Instance variables

+
+

var description

+ +

+ Inheritance: + NamedAndDescribed.description +

+ + + +

str: a description

+
+
+ +
+
+

var inArgs

+ + + + +

DArguments: The input arguments of the method

+
+
+ +
+
+

var name

+ + + + +

str: the name

+
+
+ +
+
+

var outArgs

+ + + + +

DArguments: The output arguments of the method

+
+
+ +
+

Methods

+ +
+
+

def __init__(

self, name, inArgs=supcon.intf.DArguments(), outArgs=supcon.intf.DArguments(), description='')

+
+ + + + +

Initializes the Event

+

Args: + name (str): the name of the method + inArgs (DArguments): the list of input arguments of the method + outArgs (DArguments): the list of output arguments of the method + description (str): a description of the method

+
+ +
+
def __init__(self, name, inArgs=DArguments(), outArgs=DArguments(), description=''):
+  """Initializes the Event
+  Args:
+    name (str): the name of the method
+    inArgs (DArguments): the list of input arguments of the method
+    outArgs (DArguments): the list of output arguments of the method
+    description (str): a description of the method
+  """
+  NamedAndDescribed.__init__(self, name, description)
+  self.__inArgs = DArguments.to(inArgs)#
+  self.__outArgs = DArguments.to(outArgs)
+
+ +
+
+ +
+ + +
+
+

def dump(

self)

+
+ + + + +

Converts the supcon.util.Dumpable into a 'simple' datastructure.

+
+ +
+
def dump(self):
+  data = {'name': self.name}
+  if self.inArgs:
+    data['inArgs'] = self.inArgs.dump()
+  if self.outArgs:
+    data['outArgs'] = self.outArgs.dump()
+  if self.description != '':
+    data['description'] = self.description
+  return data
+
+ +
+
+ +
+ + +
+
+

def validateInArgs(

self, inArgs)

+
+ + + + +

Validates the given argument Mapping. Raises an ValueError if validation fails

+
+ +
+
def validateInArgs(self, inArgs):
+  """Validates the given argument Mapping. Raises an ValueError if validation fails
+  """
+  for arg in inArgs:
+    if arg not in self.__inArgs:
+      raise ValueError('method {} has no input argument {}'.format(self.name, arg))
+    self.__inArgs[arg].validate(inArgs[arg])
+  for arg in self.__inArgs:
+    if arg not in inArgs:
+      raise ValueError('method {} needs input argument {}'.format(self.name, arg))
+
+ +
+
+ +
+ + +
+
+

def validateOutArgs(

self, outArgs)

+
+ + + + +

Validates the given argument Mapping. Raises an ValueError if validation fails

+
+ +
+
def validateOutArgs(self, outArgs):
+  """Validates the given argument Mapping. Raises an ValueError if validation fails
+  """
+  for arg in outArgs:
+    if arg not in self.__outArgs:
+      raise ValueError('method {} has no output argument {}'.format(self.name, arg))
+    self.__outArgs[arg].validate(outArgs[arg])
+  for arg in self.__outArgs:
+    if arg not in outArgs:
+      raise ValueError('method {} needs output argument {}'.format(self.name, arg))
+
+ +
+
+ +
+ +
+
+ +
+

class DMethods

+ + +

A readonly map of DMethod instances

+
+ +
+
class DMethods(supcon.util.NamedList):
+  """A readonly map of `supcon.intf.DMethod` instances"""
+  vtype = DMethod
+
+ +
+
+ + +
+

Ancestors (in MRO)

+
    +
  • DMethods
  • +
  • supcon.util.NamedList
  • +
  • collections.abc.Mapping
  • +
  • collections.abc.Collection
  • +
  • collections.abc.Sized
  • +
  • collections.abc.Iterable
  • +
  • collections.abc.Container
  • +
  • supcon.util.Dumpable
  • +
  • abc.ABC
  • +
  • builtins.object
  • +
+

Class variables

+
+

var vtype

+ + + + +
+
+ +
+

Class Methods

+ +
+
+

def load(

cls, data)

+
+ + + + +

Converts the given list of 'simple' datastructures data into an instance +of cls, a subclass of supcon.util.NamedList.

+
+ +
+
@classmethod
+def load(cls, data):
+  """
+  Converts the given list of 'simple' datastructures **data** into an instance
+  of **cls**, a subclass of `supcon.util.NamedList`.
+  """
+  return cls(data)
+
+ +
+
+ +
+ + +
+
+

def to(

cls, value)

+
+ + + + +

If the the value is an instance of cls, a subclass of +supcon.util.NamedList, the value is returned. If value is not an instance +of cls the method tries to convert the value into an instance of +cls.

+
+ +
+
@classmethod
+def to(cls, value):
+  """
+  If the the **value** is an instance of **cls**, a subclass of
+  `supcon.util.NamedList`, the value is returned. If value is not an instance
+  of **cls** the method tries to convert the **value** into an instance of
+  **cls**.
+  """
+  return value if isinstance(value, cls) else cls.load(value)
+
+ +
+
+ +
+ +

Methods

+ +
+
+

def __init__(

self, values=None)

+
+ + + + +

Initializes the supcon.util.NamedList with the given list of values.

+

An element of values must be of type supcon.util.NamedList.vtype or a +'simple' datastructure that can be converted to an instance of +supcon.util.NamedList.vtype by supcon.util.NamedList.vtype.load().

+
+ +
+
def __init__(self, values=None):
+  """
+  Initializes the `supcon.util.NamedList` with the given list of **values**.
+  An element of **values** must be of type `supcon.util.NamedList.vtype` or a
+  'simple' datastructure that can be converted to an instance of
+  `supcon.util.NamedList.vtype` by `supcon.util.NamedList.vtype`.load().
+  """
+  if values is None:
+    values = []
+  if not issubclass(self.vtype, Named):
+    raise ValueError('the value type must be a subclass of Named')
+  if not issubclass(self.vtype, Dumpable):
+    raise ValueError('the value type must be a subclass of Dumpable')
+  self.__values = collections.OrderedDict()
+  for value in values:
+    if not isinstance(value, self.vtype):
+      value = self.vtype.load(value)
+    if value.name in self.__values:
+      raise ValueError('value.name must be unique in the given list of values')
+    self.__values[value.name] = value
+
+ +
+
+ +
+ + +
+
+

def dump(

self)

+
+ + + + +

Converts the supcon.util.NamedList into a list of 'simple' datastructures.

+
+ +
+
def dump(self):
+  """
+  Converts the `supcon.util.NamedList` into a list of 'simple' datastructures.
+  """
+  return [value.dump() for value in self.values()]
+
+ +
+
+ +
+ + +
+
+

def get(

self, key, default=None)

+
+ + + + +

D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None.

+
+ +
+
def get(self, key, default=None):
+    'D.get(k[,d]) -> D[k] if k in D, else d.  d defaults to None.'
+    try:
+        return self[key]
+    except KeyError:
+        return default
+
+ +
+
+ +
+ + +
+
+

def items(

self)

+
+ + + + +

D.items() -> a set-like object providing a view on D's items

+
+ +
+
def items(self):
+    "D.items() -> a set-like object providing a view on D's items"
+    return ItemsView(self)
+
+ +
+
+ +
+ + +
+
+

def keys(

self)

+
+ + + + +

D.keys() -> a set-like object providing a view on D's keys

+
+ +
+
def keys(self):
+    "D.keys() -> a set-like object providing a view on D's keys"
+    return KeysView(self)
+
+ +
+
+ +
+ + +
+
+

def values(

self)

+
+ + + + +

D.values() -> an object providing a view on D's values

+
+ +
+
def values(self):
+    "D.values() -> an object providing a view on D's values"
+    return ValuesView(self)
+
+ +
+
+ +
+ +
+
+ +
+

class DNode

+ + +

A named readonly map of DPath instances

+
+ +
+
class DNode(Named, supcon.util.NamedList):
+  """A named readonly map of `supcon.intf.DPath` instances"""
+
+  vtype = DPath
+  regex = re.compile('.+')
+
+  def __init__(self, name, paths=None):
+    """Initializes the Path instance
+
+    Args:
+      name (str): the name of the argument
+      paths (): ...
+    """
+    if paths is None:
+      paths = []
+
+    Named.__init__(self, name)
+    supcon.util.NamedList.__init__(self, paths)
+
+  def dump(self):
+    return {
+      'name': self.name,
+      'paths': [value.dump() for value in self.values()]
+    }
+
+  @classmethod
+  def load(cls, data) -> 'DPath':
+    return cls(**data)
+
+  def hasPath(self, path: str) -> bool:
+    return path in self
+
+  def hasIntf(self, path: str, intf: str) -> bool:
+    return path in self and intf in self[path]
+
+  def addPath(self, path: str) -> 'DNode':
+    if path in self:
+      raise ValueError('path {} on node {} already exists'.format(path, self.name))
+    paths = itertools.chain(self.values(), [DPath(path)])
+    return DNode(self.name, paths)
+
+  def delPath(self, path: str) -> 'DNode':
+    if path not in self:
+      raise ValueError('path {} on node {} does not exist'.format(path, self.name))
+    if self[path]:
+      raise ValueError('path {} on node {} is not empty'.format(path, self.name))
+    paths = (v for v in self.values() if v.name != path)
+    return DNode(self.name, paths)
+
+  def addInterface(self, path: str, interface: DInterface) -> 'DNode':
+    if path not in self:
+      raise ValueError('path {} on node {} does not exist'.format(path, self.name))
+    if interface.name in self[path]:
+      raise ValueError('interface {} at path {} on node {} already exists'.format(interface.name, path, self.name))
+    paths = (v for v in self.values() if v.name != path)
+    paths = itertools.chain(paths, [self[path].addInterface(interface)])
+    return DNode(self.name, paths)
+
+  def delInterface(self, path: str, interface: DInterface) -> 'DNode':
+    if path not in self:
+      raise ValueError('path {} on node {} does not exist'.format(path, self.name))
+    if interface.name not in self[path]:
+      raise ValueError('interface {} at path {} on node {} does not exist'.format(interface.name, path, self.name))
+    paths = (v for v in self.values() if v.name != path)
+    paths = itertools.chain(paths, [self[path].delInterface(interface)])
+    return DNode(self.name, paths)
+
+ +
+
+ + +
+

Ancestors (in MRO)

+
    +
  • DNode
  • +
  • Named
  • +
  • supcon.util.Named
  • +
  • supcon.util.NamedList
  • +
  • collections.abc.Mapping
  • +
  • collections.abc.Collection
  • +
  • collections.abc.Sized
  • +
  • collections.abc.Iterable
  • +
  • collections.abc.Container
  • +
  • supcon.util.Dumpable
  • +
  • abc.ABC
  • +
  • builtins.object
  • +
+

Class variables

+
+

var regex

+ + + + +
+
+ +
+
+

var vtype

+ + + + +
+
+ +
+

Class Methods

+ +
+
+

def load(

cls, data)

+
+ + + + +
+ +
+
@classmethod
+def load(cls, data) -> 'DPath':
+  return cls(**data)
+
+ +
+
+ +
+ + +
+
+

def to(

cls, value)

+
+ + + + +

If the the value is an instance of cls, a subclass of +supcon.util.NamedList, the value is returned. If value is not an instance +of cls the method tries to convert the value into an instance of +cls.

+
+ +
+
@classmethod
+def to(cls, value):
+  """
+  If the the **value** is an instance of **cls**, a subclass of
+  `supcon.util.NamedList`, the value is returned. If value is not an instance
+  of **cls** the method tries to convert the **value** into an instance of
+  **cls**.
+  """
+  return value if isinstance(value, cls) else cls.load(value)
+
+ +
+
+ +
+ + +
+
+

def toName(

cls, value)

+
+ + + + +

Converts the value into a name. If this is impossible a ValueError is +raised.

+

Args: + value (any): the value +Raises: + ValueError

+
+ +
+
@classmethod
+def toName(cls, value) -> str:
+  """Converts the value into a name. If this is impossible a ValueError is
+  raised.
+  Args:
+    value (any): the value
+  Raises:
+    ValueError
+  """
+  value = str(value)
+  if not cls.regex.fullmatch(value):
+    raise ValueError("value {} must match {} fully".format(value, cls.regex.pattern))
+  return value
+
+ +
+
+ +
+ +

Instance variables

+
+

var name

+ +

+ Inheritance: + Named.name +

+ + + +

str: the name

+
+
+ +
+

Methods

+ +
+
+

def __init__(

self, name, paths=None)

+
+ + + + +

Initializes the Path instance

+

Args: + name (str): the name of the argument + paths (): ...

+
+ +
+
def __init__(self, name, paths=None):
+  """Initializes the Path instance
+  Args:
+    name (str): the name of the argument
+    paths (): ...
+  """
+  if paths is None:
+    paths = []
+  Named.__init__(self, name)
+  supcon.util.NamedList.__init__(self, paths)
+
+ +
+
+ +
+ + +
+
+

def addInterface(

self, path, interface)

+
+ + + + +
+ +
+
def addInterface(self, path: str, interface: DInterface) -> 'DNode':
+  if path not in self:
+    raise ValueError('path {} on node {} does not exist'.format(path, self.name))
+  if interface.name in self[path]:
+    raise ValueError('interface {} at path {} on node {} already exists'.format(interface.name, path, self.name))
+  paths = (v for v in self.values() if v.name != path)
+  paths = itertools.chain(paths, [self[path].addInterface(interface)])
+  return DNode(self.name, paths)
+
+ +
+
+ +
+ + +
+
+

def addPath(

self, path)

+
+ + + + +
+ +
+
def addPath(self, path: str) -> 'DNode':
+  if path in self:
+    raise ValueError('path {} on node {} already exists'.format(path, self.name))
+  paths = itertools.chain(self.values(), [DPath(path)])
+  return DNode(self.name, paths)
+
+ +
+
+ +
+ + +
+
+

def delInterface(

self, path, interface)

+
+ + + + +
+ +
+
def delInterface(self, path: str, interface: DInterface) -> 'DNode':
+  if path not in self:
+    raise ValueError('path {} on node {} does not exist'.format(path, self.name))
+  if interface.name not in self[path]:
+    raise ValueError('interface {} at path {} on node {} does not exist'.format(interface.name, path, self.name))
+  paths = (v for v in self.values() if v.name != path)
+  paths = itertools.chain(paths, [self[path].delInterface(interface)])
+  return DNode(self.name, paths)
+
+ +
+
+ +
+ + +
+
+

def delPath(

self, path)

+
+ + + + +
+ +
+
def delPath(self, path: str) -> 'DNode':
+  if path not in self:
+    raise ValueError('path {} on node {} does not exist'.format(path, self.name))
+  if self[path]:
+    raise ValueError('path {} on node {} is not empty'.format(path, self.name))
+  paths = (v for v in self.values() if v.name != path)
+  return DNode(self.name, paths)
+
+ +
+
+ +
+ + +
+
+

def dump(

self)

+
+ + + + +

Converts the supcon.util.NamedList into a list of 'simple' datastructures.

+
+ +
+
def dump(self):
+  return {
+    'name': self.name,
+    'paths': [value.dump() for value in self.values()]
+  }
+
+ +
+
+ +
+ + +
+
+

def get(

self, key, default=None)

+
+ + + + +

D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None.

+
+ +
+
def get(self, key, default=None):
+    'D.get(k[,d]) -> D[k] if k in D, else d.  d defaults to None.'
+    try:
+        return self[key]
+    except KeyError:
+        return default
+
+ +
+
+ +
+ + +
+
+

def hasIntf(

self, path, intf)

+
+ + + + +
+ +
+
def hasIntf(self, path: str, intf: str) -> bool:
+  return path in self and intf in self[path]
+
+ +
+
+ +
+ + +
+
+

def hasPath(

self, path)

+
+ + + + +
+ +
+
def hasPath(self, path: str) -> bool:
+  return path in self
+
+ +
+
+ +
+ + +
+
+

def items(

self)

+
+ + + + +

D.items() -> a set-like object providing a view on D's items

+
+ +
+
def items(self):
+    "D.items() -> a set-like object providing a view on D's items"
+    return ItemsView(self)
+
+ +
+
+ +
+ + +
+
+

def keys(

self)

+
+ + + + +

D.keys() -> a set-like object providing a view on D's keys

+
+ +
+
def keys(self):
+    "D.keys() -> a set-like object providing a view on D's keys"
+    return KeysView(self)
+
+ +
+
+ +
+ + +
+
+

def values(

self)

+
+ + + + +

D.values() -> an object providing a view on D's values

+
+ +
+
def values(self):
+    "D.values() -> an object providing a view on D's values"
+    return ValuesView(self)
+
+ +
+
+ +
+ +
+
+ +
+

class DNodes

+ + +

A readonly map of DNode instances

+
+ +
+
class DNodes(supcon.util.NamedList):
+  """A readonly map of `supcon.intf.DNode` instances"""
+  vtype = DNode
+
+  def hasNode(self, node: str) -> bool:
+    return node in self
+
+  def hasPath(self, node: str, path: str) -> bool:
+    return node in self and path in self[node]
+
+  def hasIntf(self, node: str, path: str, intf: str) -> bool:
+    return node in self and path in self[node] and intf in self[node][path]
+
+  def addNode(self, node: str) -> 'DNodes':
+    if node in self:
+      raise ValueError('node {} already exists'.format(node))
+    nodes = itertools.chain(self.values(), [DNode(node)])
+    return DNodes(nodes)
+
+  def delNode(self, node: str) -> 'DNodes':
+    if node not in self:
+      raise ValueError('node {} does not exist'.format(node))
+    if self[node]:
+      raise ValueError('node {} is not empty'.format(node))
+    nodes = (v for v in self.values() if v.name != node)
+    return DNodes(nodes)
+
+  def addPath(self, node: str, path: str) -> 'DNodes':
+    if node not in self:
+      raise ValueError('node {} does not exist'.format(node))
+    nodes = (v for v in self.values() if v.name != node)
+    nodes = itertools.chain(nodes, [self[node].addPath(path)])
+    return DNodes(nodes)
+
+  def delPath(self, node: str, path: str) -> 'DNodes':
+    if node not in self:
+      raise ValueError('node {} does not exist'.format(node))
+    nodes = (v for v in self.values() if v.name != node)
+    nodes = itertools.chain(nodes, [self[node].delPath(path)])
+    return DNodes(nodes)
+
+  def addInterface(self, node: str, path: str, interface: DInterface) -> 'DNodes':
+    if node not in self:
+      raise ValueError('node {} does not exist'.format(node))
+    nodes = (v for v in self.values() if v.name != node)
+    nodes = itertools.chain(nodes, [self[node].addInterface(path, interface)])
+    return DNodes(nodes)
+
+  def delInterface(self, node: str, path: str, interface: DInterface) -> 'DNodes':
+    if node not in self:
+      raise ValueError('node {} does not exist'.format(node))
+    nodes = (v for v in self.values() if v.name != node)
+    nodes = itertools.chain(nodes, [self[node].delInterface(path, interface)])
+    return DNodes(nodes)
+
+ +
+
+ + +
+

Ancestors (in MRO)

+
    +
  • DNodes
  • +
  • supcon.util.NamedList
  • +
  • collections.abc.Mapping
  • +
  • collections.abc.Collection
  • +
  • collections.abc.Sized
  • +
  • collections.abc.Iterable
  • +
  • collections.abc.Container
  • +
  • supcon.util.Dumpable
  • +
  • abc.ABC
  • +
  • builtins.object
  • +
+

Class variables

+
+

var vtype

+ + + + +
+
+ +
+

Class Methods

+ +
+
+

def load(

cls, data)

+
+ + + + +

Converts the given list of 'simple' datastructures data into an instance +of cls, a subclass of supcon.util.NamedList.

+
+ +
+
@classmethod
+def load(cls, data):
+  """
+  Converts the given list of 'simple' datastructures **data** into an instance
+  of **cls**, a subclass of `supcon.util.NamedList`.
+  """
+  return cls(data)
+
+ +
+
+ +
+ + +
+
+

def to(

cls, value)

+
+ + + + +

If the the value is an instance of cls, a subclass of +supcon.util.NamedList, the value is returned. If value is not an instance +of cls the method tries to convert the value into an instance of +cls.

+
+ +
+
@classmethod
+def to(cls, value):
+  """
+  If the the **value** is an instance of **cls**, a subclass of
+  `supcon.util.NamedList`, the value is returned. If value is not an instance
+  of **cls** the method tries to convert the **value** into an instance of
+  **cls**.
+  """
+  return value if isinstance(value, cls) else cls.load(value)
+
+ +
+
+ +
+ +

Methods

+ +
+
+

def __init__(

self, values=None)

+
+ + + + +

Initializes the supcon.util.NamedList with the given list of values.

+

An element of values must be of type supcon.util.NamedList.vtype or a +'simple' datastructure that can be converted to an instance of +supcon.util.NamedList.vtype by supcon.util.NamedList.vtype.load().

+
+ +
+
def __init__(self, values=None):
+  """
+  Initializes the `supcon.util.NamedList` with the given list of **values**.
+  An element of **values** must be of type `supcon.util.NamedList.vtype` or a
+  'simple' datastructure that can be converted to an instance of
+  `supcon.util.NamedList.vtype` by `supcon.util.NamedList.vtype`.load().
+  """
+  if values is None:
+    values = []
+  if not issubclass(self.vtype, Named):
+    raise ValueError('the value type must be a subclass of Named')
+  if not issubclass(self.vtype, Dumpable):
+    raise ValueError('the value type must be a subclass of Dumpable')
+  self.__values = collections.OrderedDict()
+  for value in values:
+    if not isinstance(value, self.vtype):
+      value = self.vtype.load(value)
+    if value.name in self.__values:
+      raise ValueError('value.name must be unique in the given list of values')
+    self.__values[value.name] = value
+
+ +
+
+ +
+ + +
+
+

def addInterface(

self, node, path, interface)

+
+ + + + +
+ +
+
def addInterface(self, node: str, path: str, interface: DInterface) -> 'DNodes':
+  if node not in self:
+    raise ValueError('node {} does not exist'.format(node))
+  nodes = (v for v in self.values() if v.name != node)
+  nodes = itertools.chain(nodes, [self[node].addInterface(path, interface)])
+  return DNodes(nodes)
+
+ +
+
+ +
+ + +
+
+

def addNode(

self, node)

+
+ + + + +
+ +
+
def addNode(self, node: str) -> 'DNodes':
+  if node in self:
+    raise ValueError('node {} already exists'.format(node))
+  nodes = itertools.chain(self.values(), [DNode(node)])
+  return DNodes(nodes)
+
+ +
+
+ +
+ + +
+
+

def addPath(

self, node, path)

+
+ + + + +
+ +
+
def addPath(self, node: str, path: str) -> 'DNodes':
+  if node not in self:
+    raise ValueError('node {} does not exist'.format(node))
+  nodes = (v for v in self.values() if v.name != node)
+  nodes = itertools.chain(nodes, [self[node].addPath(path)])
+  return DNodes(nodes)
+
+ +
+
+ +
+ + +
+
+

def delInterface(

self, node, path, interface)

+
+ + + + +
+ +
+
def delInterface(self, node: str, path: str, interface: DInterface) -> 'DNodes':
+  if node not in self:
+    raise ValueError('node {} does not exist'.format(node))
+  nodes = (v for v in self.values() if v.name != node)
+  nodes = itertools.chain(nodes, [self[node].delInterface(path, interface)])
+  return DNodes(nodes)
+
+ +
+
+ +
+ + +
+
+

def delNode(

self, node)

+
+ + + + +
+ +
+
def delNode(self, node: str) -> 'DNodes':
+  if node not in self:
+    raise ValueError('node {} does not exist'.format(node))
+  if self[node]:
+    raise ValueError('node {} is not empty'.format(node))
+  nodes = (v for v in self.values() if v.name != node)
+  return DNodes(nodes)
+
+ +
+
+ +
+ + +
+
+

def delPath(

self, node, path)

+
+ + + + +
+ +
+
def delPath(self, node: str, path: str) -> 'DNodes':
+  if node not in self:
+    raise ValueError('node {} does not exist'.format(node))
+  nodes = (v for v in self.values() if v.name != node)
+  nodes = itertools.chain(nodes, [self[node].delPath(path)])
+  return DNodes(nodes)
+
+ +
+
+ +
+ + +
+
+

def dump(

self)

+
+ + + + +

Converts the supcon.util.NamedList into a list of 'simple' datastructures.

+
+ +
+
def dump(self):
+  """
+  Converts the `supcon.util.NamedList` into a list of 'simple' datastructures.
+  """
+  return [value.dump() for value in self.values()]
+
+ +
+
+ +
+ + +
+
+

def get(

self, key, default=None)

+
+ + + + +

D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None.

+
+ +
+
def get(self, key, default=None):
+    'D.get(k[,d]) -> D[k] if k in D, else d.  d defaults to None.'
+    try:
+        return self[key]
+    except KeyError:
+        return default
+
+ +
+
+ +
+ + +
+
+

def hasIntf(

self, node, path, intf)

+
+ + + + +
+ +
+
def hasIntf(self, node: str, path: str, intf: str) -> bool:
+  return node in self and path in self[node] and intf in self[node][path]
+
+ +
+
+ +
+ + +
+
+

def hasNode(

self, node)

+
+ + + + +
+ +
+
def hasNode(self, node: str) -> bool:
+  return node in self
+
+ +
+
+ +
+ + +
+
+

def hasPath(

self, node, path)

+
+ + + + +
+ +
+
def hasPath(self, node: str, path: str) -> bool:
+  return node in self and path in self[node]
+
+ +
+
+ +
+ + +
+
+

def items(

self)

+
+ + + + +

D.items() -> a set-like object providing a view on D's items

+
+ +
+
def items(self):
+    "D.items() -> a set-like object providing a view on D's items"
+    return ItemsView(self)
+
+ +
+
+ +
+ + +
+
+

def keys(

self)

+
+ + + + +

D.keys() -> a set-like object providing a view on D's keys

+
+ +
+
def keys(self):
+    "D.keys() -> a set-like object providing a view on D's keys"
+    return KeysView(self)
+
+ +
+
+ +
+ + +
+
+

def values(

self)

+
+ + + + +

D.values() -> an object providing a view on D's values

+
+ +
+
def values(self):
+    "D.values() -> an object providing a view on D's values"
+    return ValuesView(self)
+
+ +
+
+ +
+ +
+
+ +
+

class DPath

+ + +

A named readonly map of DInterface instances

+
+ +
+
class DPath(Named, supcon.util.NamedList):
+  """A named readonly map of `supcon.intf.DInterface` instances"""
+
+  vtype = DInterface
+  regex = re.compile('/(([a-zA-Z0-9_]+/)*[a-zA-Z0-9_]+)?')
+
+  def __init__(self, name, interfaces=None):
+    """Initializes the Path instance
+
+    Args:
+      name (str): the name of the argument
+      interfaces (): ...
+    """
+    if interfaces is None:
+      interfaces = []
+    Named.__init__(self, name)
+    supcon.util.NamedList.__init__(self, interfaces)
+
+  def dump(self):
+    return {
+      'name': self.name,
+      'interfaces': [value.dump() for value in self.values()]
+    }
+
+  @classmethod
+  def load(cls, data) -> 'DPath':
+    return cls(**data)
+
+  def addInterface(self, interface: DInterface) -> 'DPath':
+    if interface.name in self:
+      raise ValueError('interface {} at path {} already exists'.format(interface.name, self.name))
+    interfaces = itertools.chain(self.values(), [interface])
+    return DPath(self.name, interfaces)
+
+  def delInterface(self, interface: DInterface) -> 'DPath':
+    if interface.name not in self:
+      raise ValueError('interface {} at path {} does not exist'.format(interface.name, self.name))
+    interfaces = (v for v in self.values() if v.name != interface.name)
+    return DPath(self.name, interfaces)
+
+ +
+
+ + +
+

Ancestors (in MRO)

+
    +
  • DPath
  • +
  • Named
  • +
  • supcon.util.Named
  • +
  • supcon.util.NamedList
  • +
  • collections.abc.Mapping
  • +
  • collections.abc.Collection
  • +
  • collections.abc.Sized
  • +
  • collections.abc.Iterable
  • +
  • collections.abc.Container
  • +
  • supcon.util.Dumpable
  • +
  • abc.ABC
  • +
  • builtins.object
  • +
+

Class variables

+
+

var regex

+ + + + +
+
+ +
+
+

var vtype

+ + + + +
+
+ +
+

Class Methods

+ +
+
+

def load(

cls, data)

+
+ + + + +
+ +
+
@classmethod
+def load(cls, data) -> 'DPath':
+  return cls(**data)
+
+ +
+
+ +
+ + +
+
+

def to(

cls, value)

+
+ + + + +

If the the value is an instance of cls, a subclass of +supcon.util.NamedList, the value is returned. If value is not an instance +of cls the method tries to convert the value into an instance of +cls.

+
+ +
+
@classmethod
+def to(cls, value):
+  """
+  If the the **value** is an instance of **cls**, a subclass of
+  `supcon.util.NamedList`, the value is returned. If value is not an instance
+  of **cls** the method tries to convert the **value** into an instance of
+  **cls**.
+  """
+  return value if isinstance(value, cls) else cls.load(value)
+
+ +
+
+ +
+ + +
+
+

def toName(

cls, value)

+
+ + + + +

Converts the value into a name. If this is impossible a ValueError is +raised.

+

Args: + value (any): the value +Raises: + ValueError

+
+ +
+
@classmethod
+def toName(cls, value) -> str:
+  """Converts the value into a name. If this is impossible a ValueError is
+  raised.
+  Args:
+    value (any): the value
+  Raises:
+    ValueError
+  """
+  value = str(value)
+  if not cls.regex.fullmatch(value):
+    raise ValueError("value {} must match {} fully".format(value, cls.regex.pattern))
+  return value
+
+ +
+
+ +
+ +

Instance variables

+
+

var name

+ +

+ Inheritance: + Named.name +

+ + + +

str: the name

+
+
+ +
+

Methods

+ +
+
+

def __init__(

self, name, interfaces=None)

+
+ + + + +

Initializes the Path instance

+

Args: + name (str): the name of the argument + interfaces (): ...

+
+ +
+
def __init__(self, name, interfaces=None):
+  """Initializes the Path instance
+  Args:
+    name (str): the name of the argument
+    interfaces (): ...
+  """
+  if interfaces is None:
+    interfaces = []
+  Named.__init__(self, name)
+  supcon.util.NamedList.__init__(self, interfaces)
+
+ +
+
+ +
+ + +
+
+

def addInterface(

self, interface)

+
+ + + + +
+ +
+
def addInterface(self, interface: DInterface) -> 'DPath':
+  if interface.name in self:
+    raise ValueError('interface {} at path {} already exists'.format(interface.name, self.name))
+  interfaces = itertools.chain(self.values(), [interface])
+  return DPath(self.name, interfaces)
+
+ +
+
+ +
+ + +
+
+

def delInterface(

self, interface)

+
+ + + + +
+ +
+
def delInterface(self, interface: DInterface) -> 'DPath':
+  if interface.name not in self:
+    raise ValueError('interface {} at path {} does not exist'.format(interface.name, self.name))
+  interfaces = (v for v in self.values() if v.name != interface.name)
+  return DPath(self.name, interfaces)
+
+ +
+
+ +
+ + +
+
+

def dump(

self)

+
+ + + + +

Converts the supcon.util.NamedList into a list of 'simple' datastructures.

+
+ +
+
def dump(self):
+  return {
+    'name': self.name,
+    'interfaces': [value.dump() for value in self.values()]
+  }
+
+ +
+
+ +
+ + +
+
+

def get(

self, key, default=None)

+
+ + + + +

D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None.

+
+ +
+
def get(self, key, default=None):
+    'D.get(k[,d]) -> D[k] if k in D, else d.  d defaults to None.'
+    try:
+        return self[key]
+    except KeyError:
+        return default
+
+ +
+
+ +
+ + +
+
+

def items(

self)

+
+ + + + +

D.items() -> a set-like object providing a view on D's items

+
+ +
+
def items(self):
+    "D.items() -> a set-like object providing a view on D's items"
+    return ItemsView(self)
+
+ +
+
+ +
+ + +
+
+

def keys(

self)

+
+ + + + +

D.keys() -> a set-like object providing a view on D's keys

+
+ +
+
def keys(self):
+    "D.keys() -> a set-like object providing a view on D's keys"
+    return KeysView(self)
+
+ +
+
+ +
+ + +
+
+

def values(

self)

+
+ + + + +

D.values() -> an object providing a view on D's values

+
+ +
+
def values(self):
+    "D.values() -> an object providing a view on D's values"
+    return ValuesView(self)
+
+ +
+
+ +
+ +
+
+ +
+

class Implementation

+ + +

The base class for interface implementations

+
+ +
+
class Implementation(object):
+  """The base class for interface implementations"""
+
+  def __init__(self, interface: DInterface):
+    """
+    Args:
+      interface (DInterface): the interface that is implemented by this Implementation
+    """
+    if not isinstance(interface, DInterface):
+      raise ValueError('interface must be an instance of {}'.format(DInterface))
+    self.__interface = interface
+
+    self.__callCbs = {}
+    self.__fireCbs = []
+
+  @property
+  def intf(self) -> str:
+    """str: The name of the implemented interface"""
+    return self.__interface.name
+
+  @property
+  def interface(self) -> DInterface:
+    """Interface: The implemented interface"""
+    return self.__interface
+
+  def setCallCb(self, method: str, cb):
+    """Sets a callback for the given method. The method must be a method of the
+    interface this Implementation implements. The callback must implement the
+    given method. The callback gets called by calls to Implementation.call()
+
+    Args:
+      method (str): the interface method that the callback implements
+      cb (callable): the callback
+    """
+    method = DMethod.toName(method)
+    if method in self.__callCbs:
+      raise ValueError('Callback for method {} is already set!'.format(method))
+    if method not in self.__interface.methods:
+      raise ValueError('Interface has no method {}!'.format(method))
+    self.__callCbs[method] = cb
+
+  def call(self, method: str, inArgs) -> defer.Deferred:
+    """Calls the given interface method with the given arguments. This method
+    calls the callback set by Implementation.setCallCb()
+
+    Args:
+      method (str): the called interface method
+      inArgs (Mapping): a map of input arguments
+    Returns:
+      defer.Deferred: Resolves with the result of the called method
+    """
+    def validateReturn(outArgs):
+      self.__interface.validateReturn(method, outArgs)
+      return outArgs
+
+    try:
+      self.__interface.validateCall(method, inArgs)
+      if method not in self.__callCbs:
+        raise ValueError('Callback for method {} is not set!'.format(method))
+
+      d = self.__callCbs[method](**inArgs)
+      if not isinstance(d, defer.Deferred):
+        d = defer.succeed(d)
+      d.addCallback(validateReturn)
+    except BaseException as e:
+      traceback.print_exc()
+      d = defer.fail(e)
+
+    return d
+
+  def addFireCb(self, cb):
+    """Adds a callback that gets called, when this Implementation fires an
+    event.
+
+    Args:
+      cb (callable): the callback
+    """
+    self.__fireCbs.append(cb)
+
+  def delFireCb(self, cb):
+    """Removes a callback that gets called, when this Implementation fires an
+    event.
+
+    Args:
+      cb (callable): the callback
+    """
+    self.__fireCbs.remove(cb)
+
+  def fire(self, event: str, args):
+    """Fires the given event with the given arguments.
+
+    Args:
+      event (str): the event name
+      args (collecion.Mapping): the event arguments
+    """
+    self.__interface.validateEvent(event, args)
+
+    for cb in self.__fireCbs:
+      cb(event, args)
+
+ +
+
+ + +
+

Ancestors (in MRO)

+ +

Instance variables

+
+

var interface

+ + + + +

Interface: The implemented interface

+
+
+ +
+
+

var intf

+ + + + +

str: The name of the implemented interface

+
+
+ +
+

Methods

+ +
+
+

def __init__(

self, interface)

+
+ + + + +

Args: +interface (DInterface): the interface that is implemented by this Implementation

+
+ +
+
def __init__(self, interface: DInterface):
+  """
+  Args:
+    interface (DInterface): the interface that is implemented by this Implementation
+  """
+  if not isinstance(interface, DInterface):
+    raise ValueError('interface must be an instance of {}'.format(DInterface))
+  self.__interface = interface
+  self.__callCbs = {}
+  self.__fireCbs = []
+
+ +
+
+ +
+ + +
+
+

def addFireCb(

self, cb)

+
+ + + + +

Adds a callback that gets called, when this Implementation fires an +event.

+

Args: + cb (callable): the callback

+
+ +
+
def addFireCb(self, cb):
+  """Adds a callback that gets called, when this Implementation fires an
+  event.
+  Args:
+    cb (callable): the callback
+  """
+  self.__fireCbs.append(cb)
+
+ +
+
+ +
+ + +
+
+

def call(

self, method, inArgs)

+
+ + + + +

Calls the given interface method with the given arguments. This method +calls the callback set by Implementation.setCallCb()

+

Args: + method (str): the called interface method + inArgs (Mapping): a map of input arguments +Returns: + defer.Deferred: Resolves with the result of the called method

+
+ +
+
def call(self, method: str, inArgs) -> defer.Deferred:
+  """Calls the given interface method with the given arguments. This method
+  calls the callback set by Implementation.setCallCb()
+  Args:
+    method (str): the called interface method
+    inArgs (Mapping): a map of input arguments
+  Returns:
+    defer.Deferred: Resolves with the result of the called method
+  """
+  def validateReturn(outArgs):
+    self.__interface.validateReturn(method, outArgs)
+    return outArgs
+  try:
+    self.__interface.validateCall(method, inArgs)
+    if method not in self.__callCbs:
+      raise ValueError('Callback for method {} is not set!'.format(method))
+    d = self.__callCbs[method](**inArgs)
+    if not isinstance(d, defer.Deferred):
+      d = defer.succeed(d)
+    d.addCallback(validateReturn)
+  except BaseException as e:
+    traceback.print_exc()
+    d = defer.fail(e)
+  return d
+
+ +
+
+ +
+ + +
+
+

def delFireCb(

self, cb)

+
+ + + + +

Removes a callback that gets called, when this Implementation fires an +event.

+

Args: + cb (callable): the callback

+
+ +
+
def delFireCb(self, cb):
+  """Removes a callback that gets called, when this Implementation fires an
+  event.
+  Args:
+    cb (callable): the callback
+  """
+  self.__fireCbs.remove(cb)
+
+ +
+
+ +
+ + +
+
+

def fire(

self, event, args)

+
+ + + + +

Fires the given event with the given arguments.

+

Args: + event (str): the event name + args (collecion.Mapping): the event arguments

+
+ +
+
def fire(self, event: str, args):
+  """Fires the given event with the given arguments.
+  Args:
+    event (str): the event name
+    args (collecion.Mapping): the event arguments
+  """
+  self.__interface.validateEvent(event, args)
+  for cb in self.__fireCbs:
+    cb(event, args)
+
+ +
+
+ +
+ + +
+
+

def setCallCb(

self, method, cb)

+
+ + + + +

Sets a callback for the given method. The method must be a method of the +interface this Implementation implements. The callback must implement the +given method. The callback gets called by calls to Implementation.call()

+

Args: + method (str): the interface method that the callback implements + cb (callable): the callback

+
+ +
+
def setCallCb(self, method: str, cb):
+  """Sets a callback for the given method. The method must be a method of the
+  interface this Implementation implements. The callback must implement the
+  given method. The callback gets called by calls to Implementation.call()
+  Args:
+    method (str): the interface method that the callback implements
+    cb (callable): the callback
+  """
+  method = DMethod.toName(method)
+  if method in self.__callCbs:
+    raise ValueError('Callback for method {} is already set!'.format(method))
+  if method not in self.__interface.methods:
+    raise ValueError('Interface has no method {}!'.format(method))
+  self.__callCbs[method] = cb
+
+ +
+
+ +
+ +
+
+ +
+

class Named

+ + +

A base class for objects with a name.

+
+ +
+
class Named(supcon.util.Named):
+  """A base class for objects with a name.
+  """
+
+  regex = None
+  """re.RegexObject: a regular expression, the name must conform to"""
+
+  def __init__(self, name):
+    """Initializes the NamedAndDescribed instance
+
+    Args:
+      name (str): the name of the argument
+    """
+    self.__name = self.toName(name)
+
+  @property
+  def name(self) -> str:
+    """str: the name"""
+    return self.__name
+
+  @classmethod
+  def toName(cls, value) -> str:
+    """Converts the value into a name. If this is impossible a ValueError is
+    raised.
+
+    Args:
+      value (any): the value
+    Raises:
+      ValueError
+    """
+    value = str(value)
+    if not cls.regex.fullmatch(value):
+      raise ValueError("value {} must match {} fully".format(value, cls.regex.pattern))
+    return value
+
+ +
+
+ + +
+

Ancestors (in MRO)

+
    +
  • Named
  • +
  • supcon.util.Named
  • +
  • abc.ABC
  • +
  • builtins.object
  • +
+

Class variables

+
+

var regex

+ + + + +

re.RegexObject: a regular expression, the name must conform to

+
+
+ +
+

Class Methods

+ +
+
+

def toName(

cls, value)

+
+ + + + +

Converts the value into a name. If this is impossible a ValueError is +raised.

+

Args: + value (any): the value +Raises: + ValueError

+
+ +
+
@classmethod
+def toName(cls, value) -> str:
+  """Converts the value into a name. If this is impossible a ValueError is
+  raised.
+  Args:
+    value (any): the value
+  Raises:
+    ValueError
+  """
+  value = str(value)
+  if not cls.regex.fullmatch(value):
+    raise ValueError("value {} must match {} fully".format(value, cls.regex.pattern))
+  return value
+
+ +
+
+ +
+ +

Instance variables

+
+

var name

+ + + + +

str: the name

+
+
+ +
+

Methods

+ +
+
+

def __init__(

self, name)

+
+ + + + +

Initializes the NamedAndDescribed instance

+

Args: + name (str): the name of the argument

+
+ +
+
def __init__(self, name):
+  """Initializes the NamedAndDescribed instance
+  Args:
+    name (str): the name of the argument
+  """
+  self.__name = self.toName(name)
+
+ +
+
+ +
+ +
+
+ +
+

class NamedAndDescribed

+ + +

A base class for the interface describing classes supcon.intf.Argument, +supcon.intf.Event, supcon.intf.Method and supcon.intf.Interface that +all have the properties name and description.

+
+ +
+
class NamedAndDescribed(Named):
+  """A base class for the interface describing classes `supcon.intf.Argument`,
+  `supcon.intf.Event`, `supcon.intf.Method` and `supcon.intf.Interface` that
+  all have the properties name and description."""
+
+  def __init__(self, name, description=''):
+    """Initializes the NamedAndDescribed instance
+
+    Args:
+      name (str): the name of the argument
+      description (str): a description of the argument
+    """
+    Named.__init__(self, name)
+    self.__description = self.toDescription(description)
+
+  @property
+  def description(self) -> str:
+    """str: a description"""
+    return self.__description
+
+  @classmethod
+  def toDescription(cls, value) -> str:
+    """Converts the value into a description. If this is impossible a ValueError
+    is raised.
+
+    Args:
+      value (any): the value
+    Raises:
+      ValueError
+    """
+    return str(value)
+
+ +
+
+ + +
+

Ancestors (in MRO)

+ +

Class variables

+
+

var regex

+ + + + +
+
+ +
+

Class Methods

+ +
+
+

def toDescription(

cls, value)

+
+ + + + +

Converts the value into a description. If this is impossible a ValueError +is raised.

+

Args: + value (any): the value +Raises: + ValueError

+
+ +
+
@classmethod
+def toDescription(cls, value) -> str:
+  """Converts the value into a description. If this is impossible a ValueError
+  is raised.
+  Args:
+    value (any): the value
+  Raises:
+    ValueError
+  """
+  return str(value)
+
+ +
+
+ +
+ + +
+
+

def toName(

cls, value)

+
+ + + + +

Converts the value into a name. If this is impossible a ValueError is +raised.

+

Args: + value (any): the value +Raises: + ValueError

+
+ +
+
@classmethod
+def toName(cls, value) -> str:
+  """Converts the value into a name. If this is impossible a ValueError is
+  raised.
+  Args:
+    value (any): the value
+  Raises:
+    ValueError
+  """
+  value = str(value)
+  if not cls.regex.fullmatch(value):
+    raise ValueError("value {} must match {} fully".format(value, cls.regex.pattern))
+  return value
+
+ +
+
+ +
+ +

Instance variables

+
+

var description

+ + + + +

str: a description

+
+
+ +
+
+

var name

+ + + + +

str: the name

+
+
+ +
+

Methods

+ +
+
+

def __init__(

self, name, description='')

+
+ + + + +

Initializes the NamedAndDescribed instance

+

Args: + name (str): the name of the argument + description (str): a description of the argument

+
+ +
+
def __init__(self, name, description=''):
+  """Initializes the NamedAndDescribed instance
+  Args:
+    name (str): the name of the argument
+    description (str): a description of the argument
+  """
+  Named.__init__(self, name)
+  self.__description = self.toDescription(description)
+
+ +
+
+ +
+ +
+
+ +
+

class Node

+ + +

The Node Interface. This class defines the methods that participants can +use to access the supcon bus.

+
+ +
+
class Node(abc.ABC):
+  """The Node Interface. This class defines the methods that participants can
+  use to access the supcon bus."""
+
+  def __init__(self, name):
+    super().__init__()
+    self.__name = self.toName(name)
+
+  @property
+  def name(self) -> str:
+    """str: The name of the node on the bus"""
+    return self.__name
+
+  @classmethod
+  def toName(cls, value) -> str:
+    return DNode.toName(value)
+
+  @abc.abstractmethod
+  def nodes(self) -> DNodes:
+    """list[str]: The currently connected nodes"""
+    raise NotImplementedError()
+
+  @abc.abstractmethod
+  def connect(self, endpoint):
+    """Connects the node to the given endpoint.
+
+    If the connection failes or closes the connection gets reestablished with an
+    exponential timeout up to two minutes.
+
+    Args:
+      endpoint (twisted.internet.interfaces.IStreamClientEndpoint):
+    """
+    raise NotImplementedError()
+
+  @abc.abstractmethod
+  def listen(self, endpoint):
+    """Listens at the given endpoint for incoming connections
+
+    Args:
+      endpoint (twisted.internet.interfaces.IStreamServerEndpoint):
+    """
+    raise NotImplementedError()
+
+  @abc.abstractmethod
+  def register(self, path: str, impl: Implementation):
+    """Registers an implementation with the node
+
+    Args:
+      impl (Implementation):
+    """
+    raise NotImplementedError()
+
+  @abc.abstractmethod
+  def unregister(self, path: str, impl: Implementation):
+    """Removes an implementation from the node
+
+    Args:
+      impl (supcon.intf.Implementation):
+    """
+    raise NotImplementedError()
+
+  @abc.abstractmethod
+  def call(self, node: str, path: str, intf: str, method: str, args: dict) -> defer.Deferred:
+    """Calls a method on the bus
+
+    Args:
+      node (str): a node on the bus
+      path (str): a path on the given node
+      intf (str): an interface at the given path
+      method (str): a method of the given interface
+      args (dict): a dict of method arguments
+
+    Returns:
+      defer.Deferred:
+    """
+    raise NotImplementedError()
+
+  @abc.abstractmethod
+  def on(self, node: str, path: str, intf: str, event: str, cb):
+    """Registers a callback for an event on the bus
+
+    Args:
+      node (str): a node on the bus
+      path (str): a path on the given node
+      intf (str): an interface at the given path
+      event (str): a method of the given interface
+      cb (callable): a callable that gets called with a dict of event arguments
+    """
+    raise NotImplementedError()
+
+  @abc.abstractmethod
+  def off(self, node: str, path: str, intf: str, event: str, cb):
+    """Unregisters a callback for an event on the bus
+
+    Args:
+      node (str): a node on the bus
+      path (str): a path on the given node
+      intf (str): an interface at the given path
+      event (str): a method of the given interface
+      cb (callable): a callable that gets called with a dict of event arguments
+    """
+    raise NotImplementedError()
+
+ +
+
+ + +
+

Ancestors (in MRO)

+
    +
  • Node
  • +
  • abc.ABC
  • +
  • builtins.object
  • +
+

Class Methods

+ +
+
+

def toName(

cls, value)

+
+ + + + +
+ +
+
@classmethod
+def toName(cls, value) -> str:
+  return DNode.toName(value)
+
+ +
+
+ +
+ +

Instance variables

+
+

var name

+ + + + +

str: The name of the node on the bus

+
+
+ +
+

Methods

+ +
+
+

def __init__(

self, name)

+
+ + + + +

Initialize self. See help(type(self)) for accurate signature.

+
+ +
+
def __init__(self, name):
+  super().__init__()
+  self.__name = self.toName(name)
+
+ +
+
+ +
+ + +
+
+

def call(

self, node, path, intf, method, args)

+
+ + + + +

Calls a method on the bus

+

Args: + node (str): a node on the bus + path (str): a path on the given node + intf (str): an interface at the given path + method (str): a method of the given interface + args (dict): a dict of method arguments

+

Returns: + defer.Deferred:

+
+ +
+
@abc.abstractmethod
+def call(self, node: str, path: str, intf: str, method: str, args: dict) -> defer.Deferred:
+  """Calls a method on the bus
+  Args:
+    node (str): a node on the bus
+    path (str): a path on the given node
+    intf (str): an interface at the given path
+    method (str): a method of the given interface
+    args (dict): a dict of method arguments
+  Returns:
+    defer.Deferred:
+  """
+  raise NotImplementedError()
+
+ +
+
+ +
+ + +
+
+

def connect(

self, endpoint)

+
+ + + + +

Connects the node to the given endpoint.

+

If the connection failes or closes the connection gets reestablished with an +exponential timeout up to two minutes.

+

Args: + endpoint (twisted.internet.interfaces.IStreamClientEndpoint):

+
+ +
+
@abc.abstractmethod
+def connect(self, endpoint):
+  """Connects the node to the given endpoint.
+  If the connection failes or closes the connection gets reestablished with an
+  exponential timeout up to two minutes.
+  Args:
+    endpoint (twisted.internet.interfaces.IStreamClientEndpoint):
+  """
+  raise NotImplementedError()
+
+ +
+
+ +
+ + +
+
+

def listen(

self, endpoint)

+
+ + + + +

Listens at the given endpoint for incoming connections

+

Args: + endpoint (twisted.internet.interfaces.IStreamServerEndpoint):

+
+ +
+
@abc.abstractmethod
+def listen(self, endpoint):
+  """Listens at the given endpoint for incoming connections
+  Args:
+    endpoint (twisted.internet.interfaces.IStreamServerEndpoint):
+  """
+  raise NotImplementedError()
+
+ +
+
+ +
+ + +
+
+

def nodes(

self)

+
+ + + + +

list[str]: The currently connected nodes

+
+ +
+
@abc.abstractmethod
+def nodes(self) -> DNodes:
+  """list[str]: The currently connected nodes"""
+  raise NotImplementedError()
+
+ +
+
+ +
+ + +
+
+

def off(

self, node, path, intf, event, cb)

+
+ + + + +

Unregisters a callback for an event on the bus

+

Args: + node (str): a node on the bus + path (str): a path on the given node + intf (str): an interface at the given path + event (str): a method of the given interface + cb (callable): a callable that gets called with a dict of event arguments

+
+ +
+
@abc.abstractmethod
+def off(self, node: str, path: str, intf: str, event: str, cb):
+  """Unregisters a callback for an event on the bus
+  Args:
+    node (str): a node on the bus
+    path (str): a path on the given node
+    intf (str): an interface at the given path
+    event (str): a method of the given interface
+    cb (callable): a callable that gets called with a dict of event arguments
+  """
+  raise NotImplementedError()
+
+ +
+
+ +
+ + +
+
+

def on(

self, node, path, intf, event, cb)

+
+ + + + +

Registers a callback for an event on the bus

+

Args: + node (str): a node on the bus + path (str): a path on the given node + intf (str): an interface at the given path + event (str): a method of the given interface + cb (callable): a callable that gets called with a dict of event arguments

+
+ +
+
@abc.abstractmethod
+def on(self, node: str, path: str, intf: str, event: str, cb):
+  """Registers a callback for an event on the bus
+  Args:
+    node (str): a node on the bus
+    path (str): a path on the given node
+    intf (str): an interface at the given path
+    event (str): a method of the given interface
+    cb (callable): a callable that gets called with a dict of event arguments
+  """
+  raise NotImplementedError()
+
+ +
+
+ +
+ + +
+
+

def register(

self, path, impl)

+
+ + + + +

Registers an implementation with the node

+

Args: + impl (Implementation):

+
+ +
+
@abc.abstractmethod
+def register(self, path: str, impl: Implementation):
+  """Registers an implementation with the node
+  Args:
+    impl (Implementation):
+  """
+  raise NotImplementedError()
+
+ +
+
+ +
+ + +
+
+

def unregister(

self, path, impl)

+
+ + + + +

Removes an implementation from the node

+

Args: + impl (supcon.intf.Implementation):

+
+ +
+
@abc.abstractmethod
+def unregister(self, path: str, impl: Implementation):
+  """Removes an implementation from the node
+  Args:
+    impl (supcon.intf.Implementation):
+  """
+  raise NotImplementedError()
+
+ +
+
+ +
+ +
+
+ +
+

class Object

+ + +
+ +
+
class Object(object):
+
+  def __init__(self, interfaces=None):
+    """
+    Args:
+      interfaces (DInterfaces): the interfaces that are implemented by this Object
+    """
+    if interfaces is None:
+      interfaces = []
+
+    self.__intfs = {}
+    self.__impls = {}
+
+    def getCallCb(intf):
+      return lambda method, inArgs: self.__call(intf, method, inArgs)
+
+    for interface in interfaces:
+      if not isinstance(interface, DInterface):
+        raise ValueError('interface must be an instance of {}'.format(DInterface))
+      self.__intfs[interface.name] = interface
+
+      implementation = Implementation(interface)
+      callCb = getCallCb(implementation.intf)
+      for method in interface.methods:
+        implementation.setCallCb(method, callCb)
+      self.__impls[implementation.intf] = implementation
+
+  @property
+  def interfaces(self):
+    return self.__intfs.values()
+
+  @property
+  def implementations(self):
+    return self.__impls.values()
+
+  def interface(self, intf):
+    return self.__intfs[intf]
+
+  def implementation(self, intf):
+    return self.__impls[intf]
+
+  def __call(self, intf: str, method: str, inArgs) -> defer.Deferred:
+    pass
+
+ +
+
+ + +
+

Ancestors (in MRO)

+
    +
  • Object
  • +
  • builtins.object
  • +
+

Instance variables

+
+

var implementations

+ + + + +
+
+ +
+
+

var interfaces

+ + + + +
+
+ +
+

Methods

+ +
+
+

def __init__(

self, interfaces=None)

+
+ + + + +

Args: +interfaces (DInterfaces): the interfaces that are implemented by this Object

+
+ +
+
def __init__(self, interfaces=None):
+  """
+  Args:
+    interfaces (DInterfaces): the interfaces that are implemented by this Object
+  """
+  if interfaces is None:
+    interfaces = []
+  self.__intfs = {}
+  self.__impls = {}
+  def getCallCb(intf):
+    return lambda method, inArgs: self.__call(intf, method, inArgs)
+  for interface in interfaces:
+    if not isinstance(interface, DInterface):
+      raise ValueError('interface must be an instance of {}'.format(DInterface))
+    self.__intfs[interface.name] = interface
+    implementation = Implementation(interface)
+    callCb = getCallCb(implementation.intf)
+    for method in interface.methods:
+      implementation.setCallCb(method, callCb)
+    self.__impls[implementation.intf] = implementation
+
+ +
+
+ +
+ + +
+
+

def implementation(

self, intf)

+
+ + + + +
+ +
+
def implementation(self, intf):
+  return self.__impls[intf]
+
+ +
+
+ +
+ + +
+
+

def interface(

self, intf)

+
+ + + + +
+ +
+
def interface(self, intf):
+  return self.__intfs[intf]
+
+ +
+
+ +
+ +
+
+ +
+ +
+
diff --git a/doc/keyed.m.md b/doc/keyed.m.md new file mode 100644 index 0000000..a902ffc --- /dev/null +++ b/doc/keyed.m.md @@ -0,0 +1,753 @@ + + + + + + + + + +
+ + + + +
+ + + + + + +
+

supcon.keyed module

+ + + +
+
lass KeyedMethod():
+ def __init__(self, key, fget):
+   self.__key = key
+   self.__fget = fget
+   self.__doc__ = fget.__doc__
+ @property
+ def key(self):
+   return self.__key
+ @property
+ def fget(self):
+   return self.__fget
+ def __get__(self, obj, objType=None):
+   if obj is None:
+     return self
+   if self.__fget is None:
+     raise AttributeError("unreadable attribute")
+   return lambda *args, **kwargs: self.__fget(obj, *args, **kwargs)
+ef lKeyedMethods(aClass):
+ """
+ Returns a dict of all KeyedMethods of the given class indexed by attribute
+ name. The dict does not contain the KeyedMethods of any super class.
+ """
+ if aClass is object:
+   return {}
+ lAttrs = vars(aClass)
+ if not '_lKeyedMethods' in lAttrs:
+   methods = {}
+   for name, attr in lAttrs.items():
+     if isinstance(attr, KeyedMethod):
+       methods[name] = attr
+   aClass._lKeyedMethods = methods
+ return aClass._lKeyedMethods
+ef mKeyedMethods(aClass):
+ """
+ Returns a dict of all KeyedMethods of the given class indexed by attribute
+ name. The dict contains the KeyedMethods of all super classes that are not
+ hidden by the method resolution order.
+ """
+ if not '_mKeyedMethods' in vars(aClass):
+   methods = {}
+   for sClass in reversed(aClass.mro()):
+     methods.update(lKeyedMethods(sClass))
+   aClass._mKeyedMethods = methods
+ return aClass._mKeyedMethods
+ef iKeyedMethods(aClass):
+ """
+ Returns a dict of all KeyedMethods of the given class indexed by key. The dict
+ contains the KeyedMethods of all super classes that are not hidden by the
+ method resolution order.
+ """
+ if not '_iKeyedMethods' in vars(aClass):
+   methods = {}
+   for _name, attr in mKeyedMethods(aClass).items():
+     if attr.key in methods:
+       raise ValueError()
+     methods[attr.key] = attr
+   aClass._iKeyedMethods = methods
+ return aClass._iKeyedMethods
+ef keyedMethod(key):
+ """
+ Can be used to decorate a method with a key
+ """
+ return lambda fget: KeyedMethod(key, fget)
+ef callKeyedMethod(obj, key, *args, **kwargs):
+ """
+ Resolves a keyed method by the given key and and calls it with the given
+ arguments.
+ """
+ return iKeyedMethods(type(obj))[key].__get__(obj)(*args, **kwargs)
+
+ +
+ +
+ +
+ +

Functions

+ +
+
+

def callKeyedMethod(

obj, key, *args, **kwargs)

+
+ + + + +

Resolves a keyed method by the given key and and calls it with the given +arguments.

+
+ +
+
def callKeyedMethod(obj, key, *args, **kwargs):
+  """
+  Resolves a keyed method by the given key and and calls it with the given
+  arguments.
+  """
+  return iKeyedMethods(type(obj))[key].__get__(obj)(*args, **kwargs)
+
+ +
+
+ +
+ + +
+
+

def iKeyedMethods(

aClass)

+
+ + + + +

Returns a dict of all KeyedMethods of the given class indexed by key. The dict +contains the KeyedMethods of all super classes that are not hidden by the +method resolution order.

+
+ +
+
def iKeyedMethods(aClass):
+  """
+  Returns a dict of all KeyedMethods of the given class indexed by key. The dict
+  contains the KeyedMethods of all super classes that are not hidden by the
+  method resolution order.
+  """
+  if not '_iKeyedMethods' in vars(aClass):
+    methods = {}
+    for _name, attr in mKeyedMethods(aClass).items():
+      if attr.key in methods:
+        raise ValueError()
+      methods[attr.key] = attr
+    aClass._iKeyedMethods = methods
+  return aClass._iKeyedMethods
+
+ +
+
+ +
+ + +
+
+

def keyedMethod(

key)

+
+ + + + +

Can be used to decorate a method with a key

+
+ +
+
def keyedMethod(key):
+  """
+  Can be used to decorate a method with a key
+  """
+  return lambda fget: KeyedMethod(key, fget)
+
+ +
+
+ +
+ + +
+
+

def lKeyedMethods(

aClass)

+
+ + + + +

Returns a dict of all KeyedMethods of the given class indexed by attribute +name. The dict does not contain the KeyedMethods of any super class.

+
+ +
+
def lKeyedMethods(aClass):
+  """
+  Returns a dict of all KeyedMethods of the given class indexed by attribute
+  name. The dict does not contain the KeyedMethods of any super class.
+  """
+  if aClass is object:
+    return {}
+
+  lAttrs = vars(aClass)
+  if not '_lKeyedMethods' in lAttrs:
+    methods = {}
+    for name, attr in lAttrs.items():
+      if isinstance(attr, KeyedMethod):
+        methods[name] = attr
+    aClass._lKeyedMethods = methods
+  return aClass._lKeyedMethods
+
+ +
+
+ +
+ + +
+
+

def mKeyedMethods(

aClass)

+
+ + + + +

Returns a dict of all KeyedMethods of the given class indexed by attribute +name. The dict contains the KeyedMethods of all super classes that are not +hidden by the method resolution order.

+
+ +
+
def mKeyedMethods(aClass):
+  """
+  Returns a dict of all KeyedMethods of the given class indexed by attribute
+  name. The dict contains the KeyedMethods of all super classes that are not
+  hidden by the method resolution order.
+  """
+  if not '_mKeyedMethods' in vars(aClass):
+    methods = {}
+    for sClass in reversed(aClass.mro()):
+      methods.update(lKeyedMethods(sClass))
+    aClass._mKeyedMethods = methods
+  return aClass._mKeyedMethods
+
+ +
+
+ +
+ + +

Classes

+ +
+

class KeyedMethod

+ + +
+ +
+
class KeyedMethod():
+
+  def __init__(self, key, fget):
+    self.__key = key
+    self.__fget = fget
+    self.__doc__ = fget.__doc__
+
+  @property
+  def key(self):
+    return self.__key
+
+  @property
+  def fget(self):
+    return self.__fget
+
+  def __get__(self, obj, objType=None):
+    if obj is None:
+      return self
+    if self.__fget is None:
+      raise AttributeError("unreadable attribute")
+    return lambda *args, **kwargs: self.__fget(obj, *args, **kwargs)
+
+ +
+
+ + +
+

Ancestors (in MRO)

+ +

Instance variables

+
+

var fget

+ + + + +
+
+ +
+
+

var key

+ + + + +
+
+ +
+

Methods

+ +
+
+

def __init__(

self, key, fget)

+
+ + + + +

Initialize self. See help(type(self)) for accurate signature.

+
+ +
+
def __init__(self, key, fget):
+  self.__key = key
+  self.__fget = fget
+  self.__doc__ = fget.__doc__
+
+ +
+
+ +
+ +
+
+ +
+ +
+
diff --git a/doc/store.m.md b/doc/store.m.md new file mode 100644 index 0000000..c746418 --- /dev/null +++ b/doc/store.m.md @@ -0,0 +1,851 @@ + + + + + + + + + +
+ + + + +
+ + + + + + +
+

supcon.store module

+ + + +
+
mport msgpack
+mport twisted.internet.defer as defer
+mport twisted.internet.threads as threads
+lass Store(object):
+ def load(self, key: str) -> defer.Deferred:
+   pass
+ def store(self, key: str, data: bytes) -> defer.Deferred:
+   pass
+ def delete(self, key: str) -> defer.Deferred:
+   pass
+ef readFile(name: str) -> defer.Deferred:
+ print('readFile')
+ def inThread():
+   print('readFile > inThread')
+   with open(name, 'r+b') as fd:
+     return fd.read()
+ return threads.deferToThread(inThread)
+ef writeFile(name: str, data: bytes) -> defer.Deferred:
+ print('writeFile')
+ def inThread():
+   print('writeFile > inThread')
+   with open(name, 'w+b') as fd:
+     return fd.write(data)
+ return threads.deferToThread(inThread)
+lass FileStore(Store):
+ def __init__(self, name):
+   self.__name = name
+   self.__data = {}
+   self.__last = self.__readData()
+ @defer.inlineCallbacks
+ def __readData(self):
+   data = yield readFile(self.__name)
+   data = msgpack.unpackb(data, encoding='utf-8')
+   if isinstance(data, dict):
+     self.__data = data
+ @property
+ def name(self):
+   return self.__name
+ def __queue(self, job) -> defer.Deferred:
+   d = defer.Deferred()
+   def trampolineSucceed(result):
+     if isinstance(result, defer.Deferred):
+       result.addCallbacks(trampolineSucceed, trampolineFail)
+     else:
+       d.callback(result)
+   def trampolineFail(result):
+     if isinstance(result, defer.Deferred):
+       result.addCallbacks(trampolineSucceed, trampolineFail)
+     else:
+       d.errback(result)
+   self.__last.addBoth(lambda _: job())
+   self.__last.addCallbacks(trampolineSucceed, trampolineFail)
+   return d
+ def load(self, key: str) -> defer.Deferred:
+   def job():
+     return self.__data[key] if key in self.__data else None
+   return self.__queue(job)
+ def store(self, key: str, data) -> defer.Deferred:
+   def job():
+     self.__data[key] = data
+     return writeFile(self.__name, msgpack.packb(self.__data, use_bin_type=True))
+   return self.__queue(job)
+ def delete(self, key: str) -> defer.Deferred:
+   def job():
+     if key in self.__data:
+       del self.__data[key]
+       return writeFile(self.__name, msgpack.packb(self.__data, use_bin_type=True))
+   return self.__queue(job)
+
+ +
+ +
+ +
+ +

Functions

+ +
+
+

def readFile(

name)

+
+ + + + +
+ +
+
def readFile(name: str) -> defer.Deferred:
+  print('readFile')
+  def inThread():
+    print('readFile > inThread')
+    with open(name, 'r+b') as fd:
+      return fd.read()
+
+  return threads.deferToThread(inThread)
+
+ +
+
+ +
+ + +
+
+

def writeFile(

name, data)

+
+ + + + +
+ +
+
def writeFile(name: str, data: bytes) -> defer.Deferred:
+  print('writeFile')
+  def inThread():
+    print('writeFile > inThread')
+    with open(name, 'w+b') as fd:
+      return fd.write(data)
+
+  return threads.deferToThread(inThread)
+
+ +
+
+ +
+ + +

Classes

+ +
+

class FileStore

+ + +
+ +
+
class FileStore(Store):
+
+  def __init__(self, name):
+    self.__name = name
+    self.__data = {}
+    self.__last = self.__readData()
+
+  @defer.inlineCallbacks
+  def __readData(self):
+    data = yield readFile(self.__name)
+    data = msgpack.unpackb(data, encoding='utf-8')
+    if isinstance(data, dict):
+      self.__data = data
+
+  @property
+  def name(self):
+    return self.__name
+
+  def __queue(self, job) -> defer.Deferred:
+    d = defer.Deferred()
+
+    def trampolineSucceed(result):
+      if isinstance(result, defer.Deferred):
+        result.addCallbacks(trampolineSucceed, trampolineFail)
+      else:
+        d.callback(result)
+
+    def trampolineFail(result):
+      if isinstance(result, defer.Deferred):
+        result.addCallbacks(trampolineSucceed, trampolineFail)
+      else:
+        d.errback(result)
+
+    self.__last.addBoth(lambda _: job())
+    self.__last.addCallbacks(trampolineSucceed, trampolineFail)
+
+    return d
+
+  def load(self, key: str) -> defer.Deferred:
+    def job():
+      return self.__data[key] if key in self.__data else None
+    return self.__queue(job)
+
+  def store(self, key: str, data) -> defer.Deferred:
+    def job():
+      self.__data[key] = data
+      return writeFile(self.__name, msgpack.packb(self.__data, use_bin_type=True))
+    return self.__queue(job)
+
+  def delete(self, key: str) -> defer.Deferred:
+    def job():
+      if key in self.__data:
+        del self.__data[key]
+        return writeFile(self.__name, msgpack.packb(self.__data, use_bin_type=True))
+    return self.__queue(job)
+
+ +
+
+ + +
+

Ancestors (in MRO)

+ +

Instance variables

+
+

var name

+ + + + +
+
+ +
+

Methods

+ +
+
+

def __init__(

self, name)

+
+ + + + +

Initialize self. See help(type(self)) for accurate signature.

+
+ +
+
def __init__(self, name):
+  self.__name = name
+  self.__data = {}
+  self.__last = self.__readData()
+
+ +
+
+ +
+ + +
+
+

def delete(

self, key)

+
+ + + + +
+ +
+
def delete(self, key: str) -> defer.Deferred:
+  def job():
+    if key in self.__data:
+      del self.__data[key]
+      return writeFile(self.__name, msgpack.packb(self.__data, use_bin_type=True))
+  return self.__queue(job)
+
+ +
+
+ +
+ + +
+
+

def load(

self, key)

+
+ + + + +
+ +
+
def load(self, key: str) -> defer.Deferred:
+  def job():
+    return self.__data[key] if key in self.__data else None
+  return self.__queue(job)
+
+ +
+
+ +
+ + +
+
+

def store(

self, key, data)

+
+ + + + +
+ +
+
def store(self, key: str, data) -> defer.Deferred:
+  def job():
+    self.__data[key] = data
+    return writeFile(self.__name, msgpack.packb(self.__data, use_bin_type=True))
+  return self.__queue(job)
+
+ +
+
+ +
+ +
+
+ +
+

class Store

+ + +
+ +
+
class Store(object):
+
+  def load(self, key: str) -> defer.Deferred:
+    pass
+
+  def store(self, key: str, data: bytes) -> defer.Deferred:
+    pass
+
+  def delete(self, key: str) -> defer.Deferred:
+    pass
+
+ +
+
+ + +
+

Ancestors (in MRO)

+
    +
  • Store
  • +
  • builtins.object
  • +
+

Methods

+ +
+
+

def delete(

self, key)

+
+ + + + +
+ +
+
def delete(self, key: str) -> defer.Deferred:
+  pass
+
+ +
+
+ +
+ + +
+
+

def load(

self, key)

+
+ + + + +
+ +
+
def load(self, key: str) -> defer.Deferred:
+  pass
+
+ +
+
+ +
+ + +
+
+

def store(

self, key, data)

+
+ + + + +
+ +
+
def store(self, key: str, data: bytes) -> defer.Deferred:
+  pass
+
+ +
+
+ +
+ +
+
+ +
+ +
+
diff --git a/doc/switch.m.md b/doc/switch.m.md new file mode 100644 index 0000000..4edf50f --- /dev/null +++ b/doc/switch.m.md @@ -0,0 +1,1491 @@ + + + + + + + + + +
+ + + + +
+ + + + + + +
+

supcon.switch module

+ + + +
+
# -*- coding: utf-8 -*-
+
+import pigpio
+import supcon.util
+import supcon.intf
+
+""" the interface com.screwerk.Sensor """
+SensorIntf = supcon.intf.DInterface.load({
+  'name':
+  'com.screwerk.Sensor',
+  'events': [{
+    'name': 'changed',
+    'args': [
+      {
+        'name': 'value', 'description': 'The new sensor value'
+      }
+    ],
+    'description': 'Fires when the sensor value changes'
+  }],
+  'methods': [{
+    'name': 'value',
+    'outArgs': [
+      {
+        'name': 'value', 'description': 'The sensor value'
+      }
+    ],
+    'description': 'Returns the sensor value'
+  }]
+})
+
+""" the interface com.screwerk.Switch """
+SwitchIntf = supcon.intf.DInterface.load({
+  'name': 'com.screwerk.Switch',
+  'methods': [
+    {
+      'name': 'on', 'description': 'Toggles the switch on'
+    }, {
+      'name': 'off', 'description': 'Toggles the switch off'
+    }, {
+      'name': 'toggle', 'description': 'Toggles the switch'
+    }
+  ]
+})
+
+
+def returnTrue(*args): return True
+def returnNone(*args): return None
+def returnFirst(*args): return args[0]
+
+
+class SoftSensor(object):
+  def __init__(self, value=None, guard=None, effect=None, convert=None):
+
+    self.__guard = guard or returnTrue
+    self.__effect = effect or returnNone
+    self.__convert = convert or returnFirst
+
+    self.__value = self.__convert(value)
+
+    self.__sensor = supcon.intf.Implementation(SensorIntf)
+    self.__sensor.setCallCb('value', lambda: {'value': self.__value})
+
+  @property
+  def value(self):
+    return self.__value
+
+  @property
+  def sensor(self):
+    return self.__sensor
+
+  def update(self, value):
+    value = self.__convert(value)
+    if self.__value == value:
+      return False
+    if not self.__guard(value, self):
+      return False
+
+    self.__value = value
+    self.__effect(value, self)
+    self.__sensor.fire('changed', {'value': value})
+    return True
+
+
+class SoftSwitch(SoftSensor):
+  def __init__(self, value=False, guard=None, effect=None, convert=None):
+    boolConvert = (lambda value: bool(convert(value))) if convert else bool
+    SoftSensor.__init__(self, value, guard, effect, boolConvert)
+
+    update = lambda value: {} if self.update(value) else {}
+    self.__switch = supcon.intf.Implementation(SwitchIntf)
+    self.__switch.setCallCb('on', lambda: update(True))
+    self.__switch.setCallCb('off', lambda: update(False))
+    self.__switch.setCallCb('toggle', lambda: update(not self.value))
+
+  @property
+  def switch(self):
+    return self.__switch
+
+
+class PiGPIOSensor(object):
+  def __init__(self, reactor, gpio):
+    self.__reactor = reactor
+    self.__gpio = gpio
+
+    self.__pi = pigpio.pi()
+    self.__pi.set_mode(self.__gpio, pigpio.INPUT)
+    self.__pi.set_glitch_filter(self.__gpio, 50000)
+    self.__pi.set_pull_up_down(self.__gpio, pigpio.PUD_UP)
+    self.__pi.callback(self.__gpio, pigpio.EITHER_EDGE, self.__callback)
+
+    self.__value = bool(self.__pi.read(self.__gpio))
+
+    self.__sensor = supcon.intf.Implementation(SensorIntf)
+    self.__sensor.setCallCb('value', lambda: {'value': self.__value})
+
+  @property
+  def sensor(self):
+    return self.__sensor
+
+  def __callback(self, gpio, level, tick):
+    if level >= 2:
+      return
+
+    self.__reactor.callFromThread(self.__update, bool(level))
+
+  def __update(self, value):
+    if self.__value == bool(value):
+      return
+
+    self.__value = bool(value)
+    self.__sensor.fire('changed', {'value': self.__value})
+
+
+class PiGPIOSwitch(SoftSwitch):
+  def __init__(self, gpio, guard=None, effect=None, convert=None):
+    self.__gpio = gpio
+
+    self.__pi = pigpio.pi()
+    self.__pi.set_mode(self.__gpio, pigpio.OUTPUT)
+
+    value = self.__pi.read(self.__gpio)
+
+    def writeEffect(value, switch):
+      self.__pi.write(self.__gpio, int(value))
+      if callable(effect):
+        effect(value, switch)
+
+    super().__init__(value, guard, writeEffect, convert)
+
+
+class NodeSensor(supcon.util.EventEmitter):
+
+  def __init__(self, local: supcon.intf.Node, node: str, path: str):
+    supcon.util.EventEmitter.__init__(self)
+
+    self.__local = local
+    self.__node = node
+    self.__path = path
+
+    self.__value = None
+
+    self.__local.on(self.__local.name, '/', 'supcon.Local', 'intf', self.__onIntf)
+    self.__local.on(self.__local.name, '/', 'supcon.Local', 'intfLost', self.__onIntfLost)
+    self.__local.on(self.node, self.path, 'com.screwerk.Sensor', 'changed', self.__onChanged)
+
+    if self.__local.nodes().hasIntf(self.node, self.path, 'com.screwerk.com'):
+      self.__fetchValue()
+
+  def __del__(self):
+    self.__local.off(self.__local.name, '/', 'supcon.Local', 'intf', self.__onIntf)
+    self.__local.off(self.__local.name, '/', 'supcon.Local', 'intfLost', self.__onIntfLost)
+    self.__local.off(self.node, self.path, 'com.screwerk.Sensor', 'changed', self.__onChanged)
+
+  @property
+  def node(self):
+    return self.__node
+
+  @property
+  def path(self):
+    return self.__path
+
+  @property
+  def value(self):
+    return self.__value
+
+  def __fetchValue(self):
+    self.__local.call(self.node, self.path, 'com.screwerk.Sensor', 'value', {}).addCallbacks(
+      lambda args: self.__update(args['value']),
+      lambda reason: self.__update(None)
+    )
+
+  def __update(self, value):
+    if self.__value == value:
+      return
+
+    self.__value = value
+    self.emit('changed', self.__value)
+
+  def __onIntf(self, args, event):
+    if (args.node == self.node and args.path == self.path and args.intf.name == 'com.screwerk.Sensor'):
+      self.__fetchValue()
+
+  def __onIntfLost(self, args, event):
+    if (args.node == self.node and args.path == self.path and args.intf.name == 'com.screwerk.Sensor'):
+      self.__update(None)
+
+  def __onChanged(self, args, event):
+       self.__update(args['value'])
+
+ +
+ +
+ +
+

Module variables

+
+

var SensorIntf

+ + +

the interface com.screwerk.Switch

+
+
+ +
+
+

var SwitchIntf

+ + +
+
+ +
+ +

Functions

+ +
+
+

def returnFirst(

*args)

+
+ + + + +
+ +
+
def returnFirst(*args): return args[0]
+
+ +
+
+ +
+ + +
+
+

def returnNone(

*args)

+
+ + + + +
+ +
+
def returnNone(*args): return None
+
+ +
+
+ +
+ + +
+
+

def returnTrue(

*args)

+
+ + + + +
+ +
+
def returnTrue(*args): return True
+
+ +
+
+ +
+ + +

Classes

+ +
+

class NodeSensor

+ + +
+ +
+
class NodeSensor(supcon.util.EventEmitter):
+
+  def __init__(self, local: supcon.intf.Node, node: str, path: str):
+    supcon.util.EventEmitter.__init__(self)
+
+    self.__local = local
+    self.__node = node
+    self.__path = path
+
+    self.__value = None
+
+    self.__local.on(self.__local.name, '/', 'supcon.Local', 'intf', self.__onIntf)
+    self.__local.on(self.__local.name, '/', 'supcon.Local', 'intfLost', self.__onIntfLost)
+    self.__local.on(self.node, self.path, 'com.screwerk.Sensor', 'changed', self.__onChanged)
+
+    if self.__local.nodes().hasIntf(self.node, self.path, 'com.screwerk.com'):
+      self.__fetchValue()
+
+  def __del__(self):
+    self.__local.off(self.__local.name, '/', 'supcon.Local', 'intf', self.__onIntf)
+    self.__local.off(self.__local.name, '/', 'supcon.Local', 'intfLost', self.__onIntfLost)
+    self.__local.off(self.node, self.path, 'com.screwerk.Sensor', 'changed', self.__onChanged)
+
+  @property
+  def node(self):
+    return self.__node
+
+  @property
+  def path(self):
+    return self.__path
+
+  @property
+  def value(self):
+    return self.__value
+
+  def __fetchValue(self):
+    self.__local.call(self.node, self.path, 'com.screwerk.Sensor', 'value', {}).addCallbacks(
+      lambda args: self.__update(args['value']),
+      lambda reason: self.__update(None)
+    )
+
+  def __update(self, value):
+    if self.__value == value:
+      return
+
+    self.__value = value
+    self.emit('changed', self.__value)
+
+  def __onIntf(self, args, event):
+    if (args.node == self.node and args.path == self.path and args.intf.name == 'com.screwerk.Sensor'):
+      self.__fetchValue()
+
+  def __onIntfLost(self, args, event):
+    if (args.node == self.node and args.path == self.path and args.intf.name == 'com.screwerk.Sensor'):
+      self.__update(None)
+
+  def __onChanged(self, args, event):
+       self.__update(args['value'])
+
+ +
+
+ + +
+

Ancestors (in MRO)

+
    +
  • NodeSensor
  • +
  • supcon.util.EventEmitter
  • +
  • builtins.object
  • +
+

Instance variables

+
+

var node

+ + + + +
+
+ +
+
+

var path

+ + + + +
+
+ +
+
+

var value

+ + + + +
+
+ +
+

Methods

+ +
+
+

def __init__(

self, local, node, path)

+
+ + + + +

Initialize self. See help(type(self)) for accurate signature.

+
+ +
+
def __init__(self, local: supcon.intf.Node, node: str, path: str):
+  supcon.util.EventEmitter.__init__(self)
+  self.__local = local
+  self.__node = node
+  self.__path = path
+  self.__value = None
+  self.__local.on(self.__local.name, '/', 'supcon.Local', 'intf', self.__onIntf)
+  self.__local.on(self.__local.name, '/', 'supcon.Local', 'intfLost', self.__onIntfLost)
+  self.__local.on(self.node, self.path, 'com.screwerk.Sensor', 'changed', self.__onChanged)
+  if self.__local.nodes().hasIntf(self.node, self.path, 'com.screwerk.com'):
+    self.__fetchValue()
+
+ +
+
+ +
+ + +
+
+

def emit(

self, event, *args)

+
+ + + + +

Calls all callbacks for the given event with the given arguments

+
+ +
+
def emit(self, event, *args):
+  """
+  Calls all callbacks for the given event with the given arguments
+  """
+  if event not in self.__callbacks:
+    return
+  for callback in self.__callbacks[event].values():
+    schedule(callback(*args))
+
+ +
+
+ +
+ + +
+
+

def off(

self, event, callback)

+
+ + + + +

Unregisters the given callback for the given event

+
+ +
+
def off(self, event, callback):
+  """
+  Unregisters the given callback for the given event
+  """
+  if event not in self.__callbacks:
+    return
+  if id(callback) not in self.__callbacks[event]:
+    return
+  del self.__callbacks[event][id(callback)]
+
+ +
+
+ +
+ + +
+
+

def on(

self, event, callback)

+
+ + + + +

Registers the given callback for the given event

+
+ +
+
def on(self, event, callback):
+  """
+  Registers the given callback for the given event
+  """
+  if type not in self.__callbacks:
+    self.__callbacks[event] = {}
+  self.__callbacks[event][id(callback)] = callback
+  return lambda: self.off(event, callback)
+
+ +
+
+ +
+ +
+
+ +
+

class PiGPIOSensor

+ + +
+ +
+
class PiGPIOSensor(object):
+  def __init__(self, reactor, gpio):
+    self.__reactor = reactor
+    self.__gpio = gpio
+
+    self.__pi = pigpio.pi()
+    self.__pi.set_mode(self.__gpio, pigpio.INPUT)
+    self.__pi.set_glitch_filter(self.__gpio, 50000)
+    self.__pi.set_pull_up_down(self.__gpio, pigpio.PUD_UP)
+    self.__pi.callback(self.__gpio, pigpio.EITHER_EDGE, self.__callback)
+
+    self.__value = bool(self.__pi.read(self.__gpio))
+
+    self.__sensor = supcon.intf.Implementation(SensorIntf)
+    self.__sensor.setCallCb('value', lambda: {'value': self.__value})
+
+  @property
+  def sensor(self):
+    return self.__sensor
+
+  def __callback(self, gpio, level, tick):
+    if level >= 2:
+      return
+
+    self.__reactor.callFromThread(self.__update, bool(level))
+
+  def __update(self, value):
+    if self.__value == bool(value):
+      return
+
+    self.__value = bool(value)
+    self.__sensor.fire('changed', {'value': self.__value})
+
+ +
+
+ + +
+

Ancestors (in MRO)

+ +

Instance variables

+
+

var sensor

+ + + + +
+
+ +
+

Methods

+ +
+
+

def __init__(

self, reactor, gpio)

+
+ + + + +

Initialize self. See help(type(self)) for accurate signature.

+
+ +
+
def __init__(self, reactor, gpio):
+  self.__reactor = reactor
+  self.__gpio = gpio
+  self.__pi = pigpio.pi()
+  self.__pi.set_mode(self.__gpio, pigpio.INPUT)
+  self.__pi.set_glitch_filter(self.__gpio, 50000)
+  self.__pi.set_pull_up_down(self.__gpio, pigpio.PUD_UP)
+  self.__pi.callback(self.__gpio, pigpio.EITHER_EDGE, self.__callback)
+  self.__value = bool(self.__pi.read(self.__gpio))
+  self.__sensor = supcon.intf.Implementation(SensorIntf)
+  self.__sensor.setCallCb('value', lambda: {'value': self.__value})
+
+ +
+
+ +
+ +
+
+ +
+

class PiGPIOSwitch

+ + +
+ +
+
class PiGPIOSwitch(SoftSwitch):
+  def __init__(self, gpio, guard=None, effect=None, convert=None):
+    self.__gpio = gpio
+
+    self.__pi = pigpio.pi()
+    self.__pi.set_mode(self.__gpio, pigpio.OUTPUT)
+
+    value = self.__pi.read(self.__gpio)
+
+    def writeEffect(value, switch):
+      self.__pi.write(self.__gpio, int(value))
+      if callable(effect):
+        effect(value, switch)
+
+    super().__init__(value, guard, writeEffect, convert)
+
+ +
+
+ + +
+

Ancestors (in MRO)

+ +

Instance variables

+
+

var sensor

+ +

+ Inheritance: + SoftSwitch.sensor +

+ + + +
+
+ +
+
+

var switch

+ + + + +
+
+ +
+
+

var value

+ + + + +
+
+ +
+

Methods

+ +
+
+

def __init__(

self, gpio, guard=None, effect=None, convert=None)

+
+ + + + +

Initialize self. See help(type(self)) for accurate signature.

+
+ +
+
def __init__(self, gpio, guard=None, effect=None, convert=None):
+  self.__gpio = gpio
+  self.__pi = pigpio.pi()
+  self.__pi.set_mode(self.__gpio, pigpio.OUTPUT)
+  value = self.__pi.read(self.__gpio)
+  def writeEffect(value, switch):
+    self.__pi.write(self.__gpio, int(value))
+    if callable(effect):
+      effect(value, switch)
+  super().__init__(value, guard, writeEffect, convert)
+
+ +
+
+ +
+ + +
+
+

def update(

self, value)

+
+ + + + +
+ +
+
def update(self, value):
+  value = self.__convert(value)
+  if self.__value == value:
+    return False
+  if not self.__guard(value, self):
+    return False
+  self.__value = value
+  self.__effect(value, self)
+  self.__sensor.fire('changed', {'value': value})
+  return True
+
+ +
+
+ +
+ +
+
+ +
+

class SoftSensor

+ + +
+ +
+
class SoftSensor(object):
+  def __init__(self, value=None, guard=None, effect=None, convert=None):
+
+    self.__guard = guard or returnTrue
+    self.__effect = effect or returnNone
+    self.__convert = convert or returnFirst
+
+    self.__value = self.__convert(value)
+
+    self.__sensor = supcon.intf.Implementation(SensorIntf)
+    self.__sensor.setCallCb('value', lambda: {'value': self.__value})
+
+  @property
+  def value(self):
+    return self.__value
+
+  @property
+  def sensor(self):
+    return self.__sensor
+
+  def update(self, value):
+    value = self.__convert(value)
+    if self.__value == value:
+      return False
+    if not self.__guard(value, self):
+      return False
+
+    self.__value = value
+    self.__effect(value, self)
+    self.__sensor.fire('changed', {'value': value})
+    return True
+
+ +
+
+ + +
+

Ancestors (in MRO)

+ +

Instance variables

+
+

var sensor

+ + + + +
+
+ +
+
+

var value

+ + + + +
+
+ +
+

Methods

+ +
+
+

def __init__(

self, value=None, guard=None, effect=None, convert=None)

+
+ + + + +

Initialize self. See help(type(self)) for accurate signature.

+
+ +
+
def __init__(self, value=None, guard=None, effect=None, convert=None):
+  self.__guard = guard or returnTrue
+  self.__effect = effect or returnNone
+  self.__convert = convert or returnFirst
+  self.__value = self.__convert(value)
+  self.__sensor = supcon.intf.Implementation(SensorIntf)
+  self.__sensor.setCallCb('value', lambda: {'value': self.__value})
+
+ +
+
+ +
+ + +
+
+

def update(

self, value)

+
+ + + + +
+ +
+
def update(self, value):
+  value = self.__convert(value)
+  if self.__value == value:
+    return False
+  if not self.__guard(value, self):
+    return False
+  self.__value = value
+  self.__effect(value, self)
+  self.__sensor.fire('changed', {'value': value})
+  return True
+
+ +
+
+ +
+ +
+
+ +
+

class SoftSwitch

+ + +
+ +
+
class SoftSwitch(SoftSensor):
+  def __init__(self, value=False, guard=None, effect=None, convert=None):
+    boolConvert = (lambda value: bool(convert(value))) if convert else bool
+    SoftSensor.__init__(self, value, guard, effect, boolConvert)
+
+    update = lambda value: {} if self.update(value) else {}
+    self.__switch = supcon.intf.Implementation(SwitchIntf)
+    self.__switch.setCallCb('on', lambda: update(True))
+    self.__switch.setCallCb('off', lambda: update(False))
+    self.__switch.setCallCb('toggle', lambda: update(not self.value))
+
+  @property
+  def switch(self):
+    return self.__switch
+
+ +
+
+ + +
+

Ancestors (in MRO)

+ +

Instance variables

+
+

var sensor

+ +

+ Inheritance: + SoftSensor.sensor +

+ + + +
+
+ +
+
+

var switch

+ + + + +
+
+ +
+
+

var value

+ + + + +
+
+ +
+

Methods

+ +
+
+

def __init__(

self, value=False, guard=None, effect=None, convert=None)

+
+ + + + +

Initialize self. See help(type(self)) for accurate signature.

+
+ +
+
def __init__(self, value=False, guard=None, effect=None, convert=None):
+  boolConvert = (lambda value: bool(convert(value))) if convert else bool
+  SoftSensor.__init__(self, value, guard, effect, boolConvert)
+  update = lambda value: {} if self.update(value) else {}
+  self.__switch = supcon.intf.Implementation(SwitchIntf)
+  self.__switch.setCallCb('on', lambda: update(True))
+  self.__switch.setCallCb('off', lambda: update(False))
+  self.__switch.setCallCb('toggle', lambda: update(not self.value))
+
+ +
+
+ +
+ + +
+
+

def update(

self, value)

+
+ + + + +
+ +
+
def update(self, value):
+  value = self.__convert(value)
+  if self.__value == value:
+    return False
+  if not self.__guard(value, self):
+    return False
+  self.__value = value
+  self.__effect(value, self)
+  self.__sensor.fire('changed', {'value': value})
+  return True
+
+ +
+
+ +
+ +
+
+ +
+ +
+
diff --git a/doc/util.m.md b/doc/util.m.md new file mode 100644 index 0000000..769000c --- /dev/null +++ b/doc/util.m.md @@ -0,0 +1,1346 @@ + + + + + + + + + +
+ + + + +
+ + + + + + +
+

supcon.util module

+ + + +
+
# -*- coding: utf-8 -*-
+
+import abc
+
+import asyncio
+import collections
+import collections.abc
+
+import twisted.internet.defer
+
+def schedule(obj):
+  if asyncio.iscoroutine(obj):
+    obj = twisted.internet.defer.ensureDeferred(obj)
+  return obj
+
+class Dumpable(abc.ABC):
+  """
+  The abstract base class `supcon.util.Dumpable`.
+
+  Implementations of this abstract base class can be converted to and from
+  'simple' datastructures containing only ints, floats, strings, lists and
+  dicts. These simple datastructures can be send and received by the supcon
+  bus. If you only ever use simple datastructures you can ignore this class.
+  """
+
+  @abc.abstractmethod
+  def dump(self):
+    """
+    Converts the `supcon.util.Dumpable` into a 'simple' datastructure.
+    """
+    pass
+
+  @abc.abstractclassmethod
+  def load(cls, data):
+    """
+    Converts the given 'simple' datastructure **data** into an instance of
+    **cls**, a subclass of `supcon.util.Dumpable`
+    """
+    pass
+
+
+class Named(abc.ABC):
+  """
+  The abstract base class `supcon.util.Named`.
+
+  Implementations of this abstract base class must have a property
+  `supcon.util.Named.name`, that can be used as a key in a dictionary.
+  """
+
+  @property
+  @abc.abstractmethod
+  def name(self) -> str:
+    """
+    The name of the instance. Is useable as a dictionary key.
+    """
+    pass
+
+
+class NamedList(collections.abc.Mapping, Dumpable):
+  """
+  A `supcon.util.NamedList` is an ordered, readonly dictionary of
+  `supcon.util.Named` and `supcon.util.Dumpable` elements of value type
+  `supcon.util.NamedList.vtype`.
+
+  The class `supcon.util.NamedList` itself can not be instantiated. It must be
+  subclassed and the class property `supcon.util.NamedList.vtype` set. The class
+  property `supcon.util.NamedList.vtype` denotes the type of the elements that
+  the list can contain. The type `supcon.util.NamedList.vtype` must be a
+  subclass of `supcon.util.Named` and of `supcon.util.Dumpable`. Elements in the
+  `supcon.util.NamedList.vtype` can be accessed by the values of their
+  `supcon.util.Named.name` property.
+  """
+
+  vtype = None
+  """
+  Must be assigned in a subclass of `supcon.util.NamedList` to a subclass
+  of `supcon.util.Named` and `supcon.util.Dumpable`
+  """
+
+  def __init__(self, values=None):
+    """
+    Initializes the `supcon.util.NamedList` with the given list of **values**.
+
+    An element of **values** must be of type `supcon.util.NamedList.vtype` or a
+    'simple' datastructure that can be converted to an instance of
+    `supcon.util.NamedList.vtype` by `supcon.util.NamedList.vtype`.load().
+    """
+    if values is None:
+      values = []
+    if not issubclass(self.vtype, Named):
+      raise ValueError('the value type must be a subclass of Named')
+    if not issubclass(self.vtype, Dumpable):
+      raise ValueError('the value type must be a subclass of Dumpable')
+
+    self.__values = collections.OrderedDict()
+
+    for value in values:
+      if not isinstance(value, self.vtype):
+        value = self.vtype.load(value)
+      if value.name in self.__values:
+        raise ValueError('value.name must be unique in the given list of values')
+      self.__values[value.name] = value
+
+  def __getitem__(self, key):
+    return self.__values[key]
+
+  def __iter__(self):
+    return iter(self.__values)
+
+  def __len__(self):
+    return len(self.__values)
+
+  def __repr__(self):
+    return "{}.{}({})".format(
+      self.__module__, self.__class__.__name__,
+      ', '.join([repr(v) for k, v in self.items()])
+    )
+
+  def dump(self):
+    """
+    Converts the `supcon.util.NamedList` into a list of 'simple' datastructures.
+    """
+    return [value.dump() for value in self.values()]
+
+  @classmethod
+  def load(cls, data):
+    """
+    Converts the given list of 'simple' datastructures **data** into an instance
+    of **cls**, a subclass of `supcon.util.NamedList`.
+    """
+    return cls(data)
+
+  @classmethod
+  def to(cls, value):
+    """
+    If the the **value** is an instance of **cls**, a subclass of
+    `supcon.util.NamedList`, the value is returned. If value is not an instance
+    of **cls** the method tries to convert the **value** into an instance of
+    **cls**.
+    """
+    return value if isinstance(value, cls) else cls.load(value)
+
+
+class EventEmitter(object):
+
+  def __init__(self):
+    self.__callbacks = {}
+
+  def on(self, event, callback):
+    """
+    Registers the given callback for the given event
+    """
+    if type not in self.__callbacks:
+      self.__callbacks[event] = {}
+    self.__callbacks[event][id(callback)] = callback
+    return lambda: self.off(event, callback)
+
+  def off(self, event, callback):
+    """
+    Unregisters the given callback for the given event
+    """
+    if event not in self.__callbacks:
+      return
+    if id(callback) not in self.__callbacks[event]:
+      return
+    del self.__callbacks[event][id(callback)]
+
+  def emit(self, event, *args):
+    """
+    Calls all callbacks for the given event with the given arguments
+    """
+    if event not in self.__callbacks:
+      return
+    for callback in self.__callbacks[event].values():
+      schedule(callback(*args))
+
+ +
+ +
+ +
+ +

Functions

+ +
+
+

def schedule(

obj)

+
+ + + + +
+ +
+
def schedule(obj):
+  if asyncio.iscoroutine(obj):
+    obj = twisted.internet.defer.ensureDeferred(obj)
+  return obj
+
+ +
+
+ +
+ + +

Classes

+ +
+

class Dumpable

+ + +

The abstract base class Dumpable.

+

Implementations of this abstract base class can be converted to and from +'simple' datastructures containing only ints, floats, strings, lists and +dicts. These simple datastructures can be send and received by the supcon +bus. If you only ever use simple datastructures you can ignore this class.

+
+ +
+
class Dumpable(abc.ABC):
+  """
+  The abstract base class `supcon.util.Dumpable`.
+
+  Implementations of this abstract base class can be converted to and from
+  'simple' datastructures containing only ints, floats, strings, lists and
+  dicts. These simple datastructures can be send and received by the supcon
+  bus. If you only ever use simple datastructures you can ignore this class.
+  """
+
+  @abc.abstractmethod
+  def dump(self):
+    """
+    Converts the `supcon.util.Dumpable` into a 'simple' datastructure.
+    """
+    pass
+
+  @abc.abstractclassmethod
+  def load(cls, data):
+    """
+    Converts the given 'simple' datastructure **data** into an instance of
+    **cls**, a subclass of `supcon.util.Dumpable`
+    """
+    pass
+
+ +
+
+ + +
+

Ancestors (in MRO)

+
    +
  • Dumpable
  • +
  • abc.ABC
  • +
  • builtins.object
  • +
+

Class Methods

+ +
+
+

def load(

cls, data)

+
+ + + + +

Converts the given 'simple' datastructure data into an instance of +cls, a subclass of Dumpable

+
+ +
+
@abc.abstractclassmethod
+def load(cls, data):
+  """
+  Converts the given 'simple' datastructure **data** into an instance of
+  **cls**, a subclass of `supcon.util.Dumpable`
+  """
+  pass
+
+ +
+
+ +
+ +

Methods

+ +
+
+

def dump(

self)

+
+ + + + +

Converts the Dumpable into a 'simple' datastructure.

+
+ +
+
@abc.abstractmethod
+def dump(self):
+  """
+  Converts the `supcon.util.Dumpable` into a 'simple' datastructure.
+  """
+  pass
+
+ +
+
+ +
+ +
+
+ +
+

class EventEmitter

+ + +
+ +
+
class EventEmitter(object):
+
+  def __init__(self):
+    self.__callbacks = {}
+
+  def on(self, event, callback):
+    """
+    Registers the given callback for the given event
+    """
+    if type not in self.__callbacks:
+      self.__callbacks[event] = {}
+    self.__callbacks[event][id(callback)] = callback
+    return lambda: self.off(event, callback)
+
+  def off(self, event, callback):
+    """
+    Unregisters the given callback for the given event
+    """
+    if event not in self.__callbacks:
+      return
+    if id(callback) not in self.__callbacks[event]:
+      return
+    del self.__callbacks[event][id(callback)]
+
+  def emit(self, event, *args):
+    """
+    Calls all callbacks for the given event with the given arguments
+    """
+    if event not in self.__callbacks:
+      return
+    for callback in self.__callbacks[event].values():
+      schedule(callback(*args))
+
+ +
+
+ + +
+

Ancestors (in MRO)

+ +

Methods

+ +
+
+

def __init__(

self)

+
+ + + + +

Initialize self. See help(type(self)) for accurate signature.

+
+ +
+
def __init__(self):
+  self.__callbacks = {}
+
+ +
+
+ +
+ + +
+
+

def emit(

self, event, *args)

+
+ + + + +

Calls all callbacks for the given event with the given arguments

+
+ +
+
def emit(self, event, *args):
+  """
+  Calls all callbacks for the given event with the given arguments
+  """
+  if event not in self.__callbacks:
+    return
+  for callback in self.__callbacks[event].values():
+    schedule(callback(*args))
+
+ +
+
+ +
+ + +
+
+

def off(

self, event, callback)

+
+ + + + +

Unregisters the given callback for the given event

+
+ +
+
def off(self, event, callback):
+  """
+  Unregisters the given callback for the given event
+  """
+  if event not in self.__callbacks:
+    return
+  if id(callback) not in self.__callbacks[event]:
+    return
+  del self.__callbacks[event][id(callback)]
+
+ +
+
+ +
+ + +
+
+

def on(

self, event, callback)

+
+ + + + +

Registers the given callback for the given event

+
+ +
+
def on(self, event, callback):
+  """
+  Registers the given callback for the given event
+  """
+  if type not in self.__callbacks:
+    self.__callbacks[event] = {}
+  self.__callbacks[event][id(callback)] = callback
+  return lambda: self.off(event, callback)
+
+ +
+
+ +
+ +
+
+ +
+

class Named

+ + +

The abstract base class Named.

+

Implementations of this abstract base class must have a property +name, that can be used as a key in a dictionary.

+
+ +
+
class Named(abc.ABC):
+  """
+  The abstract base class `supcon.util.Named`.
+
+  Implementations of this abstract base class must have a property
+  `supcon.util.Named.name`, that can be used as a key in a dictionary.
+  """
+
+  @property
+  @abc.abstractmethod
+  def name(self) -> str:
+    """
+    The name of the instance. Is useable as a dictionary key.
+    """
+    pass
+
+ +
+
+ + +
+

Ancestors (in MRO)

+
    +
  • Named
  • +
  • abc.ABC
  • +
  • builtins.object
  • +
+

Instance variables

+
+

var name

+ + + + +

The name of the instance. Is useable as a dictionary key.

+
+
+ +
+
+
+ +
+

class NamedList

+ + +

A NamedList is an ordered, readonly dictionary of +Named and Dumpable elements of value type +vtype.

+

The class NamedList itself can not be instantiated. It must be +subclassed and the class property vtype set. The class +property vtype denotes the type of the elements that +the list can contain. The type vtype must be a +subclass of Named and of Dumpable. Elements in the +vtype can be accessed by the values of their +name property.

+
+ +
+
class NamedList(collections.abc.Mapping, Dumpable):
+  """
+  A `supcon.util.NamedList` is an ordered, readonly dictionary of
+  `supcon.util.Named` and `supcon.util.Dumpable` elements of value type
+  `supcon.util.NamedList.vtype`.
+
+  The class `supcon.util.NamedList` itself can not be instantiated. It must be
+  subclassed and the class property `supcon.util.NamedList.vtype` set. The class
+  property `supcon.util.NamedList.vtype` denotes the type of the elements that
+  the list can contain. The type `supcon.util.NamedList.vtype` must be a
+  subclass of `supcon.util.Named` and of `supcon.util.Dumpable`. Elements in the
+  `supcon.util.NamedList.vtype` can be accessed by the values of their
+  `supcon.util.Named.name` property.
+  """
+
+  vtype = None
+  """
+  Must be assigned in a subclass of `supcon.util.NamedList` to a subclass
+  of `supcon.util.Named` and `supcon.util.Dumpable`
+  """
+
+  def __init__(self, values=None):
+    """
+    Initializes the `supcon.util.NamedList` with the given list of **values**.
+
+    An element of **values** must be of type `supcon.util.NamedList.vtype` or a
+    'simple' datastructure that can be converted to an instance of
+    `supcon.util.NamedList.vtype` by `supcon.util.NamedList.vtype`.load().
+    """
+    if values is None:
+      values = []
+    if not issubclass(self.vtype, Named):
+      raise ValueError('the value type must be a subclass of Named')
+    if not issubclass(self.vtype, Dumpable):
+      raise ValueError('the value type must be a subclass of Dumpable')
+
+    self.__values = collections.OrderedDict()
+
+    for value in values:
+      if not isinstance(value, self.vtype):
+        value = self.vtype.load(value)
+      if value.name in self.__values:
+        raise ValueError('value.name must be unique in the given list of values')
+      self.__values[value.name] = value
+
+  def __getitem__(self, key):
+    return self.__values[key]
+
+  def __iter__(self):
+    return iter(self.__values)
+
+  def __len__(self):
+    return len(self.__values)
+
+  def __repr__(self):
+    return "{}.{}({})".format(
+      self.__module__, self.__class__.__name__,
+      ', '.join([repr(v) for k, v in self.items()])
+    )
+
+  def dump(self):
+    """
+    Converts the `supcon.util.NamedList` into a list of 'simple' datastructures.
+    """
+    return [value.dump() for value in self.values()]
+
+  @classmethod
+  def load(cls, data):
+    """
+    Converts the given list of 'simple' datastructures **data** into an instance
+    of **cls**, a subclass of `supcon.util.NamedList`.
+    """
+    return cls(data)
+
+  @classmethod
+  def to(cls, value):
+    """
+    If the the **value** is an instance of **cls**, a subclass of
+    `supcon.util.NamedList`, the value is returned. If value is not an instance
+    of **cls** the method tries to convert the **value** into an instance of
+    **cls**.
+    """
+    return value if isinstance(value, cls) else cls.load(value)
+
+ +
+
+ + +
+

Ancestors (in MRO)

+
    +
  • NamedList
  • +
  • collections.abc.Mapping
  • +
  • collections.abc.Collection
  • +
  • collections.abc.Sized
  • +
  • collections.abc.Iterable
  • +
  • collections.abc.Container
  • +
  • Dumpable
  • +
  • abc.ABC
  • +
  • builtins.object
  • +
+

Class variables

+
+

var vtype

+ + + + +

Must be assigned in a subclass of NamedList to a subclass +of Named and Dumpable

+
+
+ +
+

Class Methods

+ +
+
+

def load(

cls, data)

+
+ + + + +

Converts the given list of 'simple' datastructures data into an instance +of cls, a subclass of NamedList.

+
+ +
+
@classmethod
+def load(cls, data):
+  """
+  Converts the given list of 'simple' datastructures **data** into an instance
+  of **cls**, a subclass of `supcon.util.NamedList`.
+  """
+  return cls(data)
+
+ +
+
+ +
+ + +
+
+

def to(

cls, value)

+
+ + + + +

If the the value is an instance of cls, a subclass of +NamedList, the value is returned. If value is not an instance +of cls the method tries to convert the value into an instance of +cls.

+
+ +
+
@classmethod
+def to(cls, value):
+  """
+  If the the **value** is an instance of **cls**, a subclass of
+  `supcon.util.NamedList`, the value is returned. If value is not an instance
+  of **cls** the method tries to convert the **value** into an instance of
+  **cls**.
+  """
+  return value if isinstance(value, cls) else cls.load(value)
+
+ +
+
+ +
+ +

Methods

+ +
+
+

def __init__(

self, values=None)

+
+ + + + +

Initializes the NamedList with the given list of values.

+

An element of values must be of type vtype or a +'simple' datastructure that can be converted to an instance of +vtype by vtype.load().

+
+ +
+
def __init__(self, values=None):
+  """
+  Initializes the `supcon.util.NamedList` with the given list of **values**.
+  An element of **values** must be of type `supcon.util.NamedList.vtype` or a
+  'simple' datastructure that can be converted to an instance of
+  `supcon.util.NamedList.vtype` by `supcon.util.NamedList.vtype`.load().
+  """
+  if values is None:
+    values = []
+  if not issubclass(self.vtype, Named):
+    raise ValueError('the value type must be a subclass of Named')
+  if not issubclass(self.vtype, Dumpable):
+    raise ValueError('the value type must be a subclass of Dumpable')
+  self.__values = collections.OrderedDict()
+  for value in values:
+    if not isinstance(value, self.vtype):
+      value = self.vtype.load(value)
+    if value.name in self.__values:
+      raise ValueError('value.name must be unique in the given list of values')
+    self.__values[value.name] = value
+
+ +
+
+ +
+ + +
+
+

def dump(

self)

+
+ + + + +

Converts the NamedList into a list of 'simple' datastructures.

+
+ +
+
def dump(self):
+  """
+  Converts the `supcon.util.NamedList` into a list of 'simple' datastructures.
+  """
+  return [value.dump() for value in self.values()]
+
+ +
+
+ +
+ + +
+
+

def get(

self, key, default=None)

+
+ + + + +

D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None.

+
+ +
+
def get(self, key, default=None):
+    'D.get(k[,d]) -> D[k] if k in D, else d.  d defaults to None.'
+    try:
+        return self[key]
+    except KeyError:
+        return default
+
+ +
+
+ +
+ + +
+
+

def items(

self)

+
+ + + + +

D.items() -> a set-like object providing a view on D's items

+
+ +
+
def items(self):
+    "D.items() -> a set-like object providing a view on D's items"
+    return ItemsView(self)
+
+ +
+
+ +
+ + +
+
+

def keys(

self)

+
+ + + + +

D.keys() -> a set-like object providing a view on D's keys

+
+ +
+
def keys(self):
+    "D.keys() -> a set-like object providing a view on D's keys"
+    return KeysView(self)
+
+ +
+
+ +
+ + +
+
+

def values(

self)

+
+ + + + +

D.values() -> an object providing a view on D's values

+
+ +
+
def values(self):
+    "D.values() -> an object providing a view on D's values"
+    return ValuesView(self)
+
+ +
+
+ +
+ +
+
+ +
+ +
+
diff --git a/listen.py b/listen.py new file mode 100755 index 0000000..ec50458 --- /dev/null +++ b/listen.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 + +import supcon.core + +from twisted.internet import reactor +from twisted.internet.endpoints import TCP4ClientEndpoint + +local = 'listen' +local = supcon.core.Node(local) + +printArgs = lambda *args: print(', '.join([ '{}'.format(i) for i in args ])) +pluckArgs = lambda mapping, keys: (mapping[key] for key in keys) + +def onIntf(args, event): + (_Node, _Path, _Intf, eEvent) = event + (aNode, aPath, aInterface) = pluckArgs(args, ['node', 'path', 'intf']) + aInterface = supcon.intf.DInterface.load(aInterface) + for aEvent in aInterface.events: + reg = local.on if eEvent == 'intf' else local.off + reg(aNode, aPath, aInterface.name, aEvent, printArgs) + +local.on(local.name, '/', 'supcon.Local', 'intf', onIntf) +local.on(local.name, '/', 'supcon.Local', 'intfLost', onIntf) + +local.on(local.name, '/', 'supcon.Local', 'intf', printArgs) +local.on(local.name, '/', 'supcon.Local', 'intfLost', printArgs) + +local.connect(TCP4ClientEndpoint(reactor, 'localhost', 8123)) + +reactor.run() diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..061bbaf --- /dev/null +++ b/setup.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +from setuptools import setup, find_packages + +#with open('README.rst') as f: +# readme = f.read() + +#with open('LICENSE') as f: +# license = f.read() + +install_requirements = [ + 'msgpack-python', + 'twisted', + 'pigpio', + 'pyopenssl', + 'service-identity', +] +development_requirements = [ + 'pdoc', + 'pylint', + 'pygments', +] + +setup( + name='supcon', + version='0.0.1', + description='framework to access the supcon bus from python', +# long_description=readme, + long_description='', + author='Sebastian Brix', + author_email='sebastian.brix@graph-it.com', + url='git://git.graph-it.com/screwerk/supcon', +# license=license, + license='', + install_requires=install_requirements, + extras_require={ + 'dev': development_requirements + }, + packages=find_packages(exclude=('doc')), +) diff --git a/supcon/__init__.py b/supcon/__init__.py new file mode 100644 index 0000000..4ba52de --- /dev/null +++ b/supcon/__init__.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +""" +The `supcon` Framework provides inter-process communication (IPC) and remote +procedure call (RPC) mechanisms, that allow communication between multiple +computer programs running on different hosts in a TCP/IP network. + +Each participating program exposes one or more `supcon.intf.Node`s to the +network. Each `supcon.intf.Node` has a name that must be unique among the +nodes of the communicating programs. + + #!python + import supcon.core + + lServer = supcon.core.Node('my-unique-server') + lClient = supcon.core.Node('my-unique-client') + +You can give a `supcon.intf.Node` one or more endpoints on the network to either +connect to or to listen for incomming connections. + + #!python + from twisted.internet import reactor + from twisted.internet.endpoints import TCP4ClientEndpoint + from twisted.internet.endpoints import TCP4ServerEndpoint + + lServer.listen(TCP4ServerEndpoint(reactor, 7000)) + lClient.connect(TCP4ClientEndpoint(reactor, 'localhost', 7000)) + +When a `supcon.intf.Node` is unable to create a connection to the given endpoint +or an established connection terminates, the node tries to reestablish the +connection. + +Programs can expose events and provide methods via the node. These events and +methods are defined in `supcon.intf.Interface`s. Lets create one: + + #!python + import supcon.intf + + timerIntf = supcon.intf.Interface({ + 'name': 'com.example.Timer', + 'events': [ + { + 'name': 'timeout', + 'args': [ + { 'name': 'timer', 'description': '(str) - The name of the timer' } + ] + 'description': 'Gets triggerd, whenever a timer times out.' + } + ], + 'methods': [ + { + 'name': 'add', + 'inArgs': [ + { 'name': 'timer', 'description': '(str) - A name for the timer' } + { 'name': 'seconds', 'description': '(float) - A duration' } + ], + 'description': 'Adds a new timer.' + } + ] + }) + +Lets implement the interface and register the implementation at the local node: + + #!python + + timerImpl = supcon.intf.Implementation(timerIntf) + + def fireTimeout(timer): + timerImpl.fire('timeout', { 'timer': timer }) + + def onAdd(timer, seconds): + reactor.callLater(seconds, lambda: fireTimeout(timer)) + + timerImpl.setCallCb('add', onAdd) + + lServer.register('/myTimer', timerImpl) + +Now you can watch for timeout events and add a timer via the +lClient reference: + + #!python + lClient.on('my-unique-server', '/myTimer', 'com.example.Timer', 'timeout', + lambda *args: print(', '.join([ '{}'.format(i) for i in args ])) + ) + + lClient.call('my-unique-server', '/myTimer', 'com.example.Timer', 'add', { + 'timer': '5Seconds', 'seconds': 5 + }) + +And don't forget to start the reactor (aka event loop) + + #!python + reactor.run() + +""" diff --git a/supcon/clone.py b/supcon/clone.py new file mode 100644 index 0000000..f0ce4a3 --- /dev/null +++ b/supcon/clone.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- + +import supcon.intf + +class _Clone(supcon.intf.Implementation): + + def __init__(self, local, rnode, rpath, interface): + super().__init__(interface) + + self.__local = local + self.__rnode = rnode + self.__rpath = rpath + + for event in self.interface.events: + self.__local.on(self.__rnode, self.__rpath, self.intf, event, self.__on) + + def __del__(self): + for event in self.interface.events: + self.__local.off(self.__rnode, self.__rpath, self.intf, event, self.__on) + + def __on(self, args: dict, event): + (_Node, _Path, _Intf, eEvent) = event + self.fire(eEvent, args) + + def call(self, method, inArgs): + #print('_Clone.call', method, inArgs) + return self.__local.call(self.__rnode, self.__rpath, self.intf, method, inArgs) + +class Cloner(object): + + def __init__(self, local: supcon.intf.Node, lPath: str, rNode: str, rPath: str, rIntf: str): + #print('Cloner.__init__') + self.__local = local + self.__lPath = lPath + + self.__rNode = rNode + self.__rPath = rPath + self.__rIntf = rIntf + + self.__impl = None + + self.__local.on(self.__local.name, '/', 'supcon.Local', 'intf', self.__onIntf) + self.__local.on(self.__local.name, '/', 'supcon.Local', 'intfLost', self.__onIntf) + + nodes = local.nodes() + if nodes.hasIntf(rNode, rPath, rIntf): + self.__setupImpl(nodes[rNode][rPath][rIntf]) + + @property + def local(self) -> supcon.intf.Node: + return self.__local + + @property + def lPath(self) -> str: + return self.__lPath + + @property + def rNode(self) -> str: + return self.__rNode + + @property + def rPath(self) -> str: + return self.__rPath + + @property + def rIntf(self) -> str: + return self.__rIntf + + def __onIntf(self, args, event): + if args['node'] != self.__rNode or args['path'] != self.__rPath: + return + aInterface = supcon.intf.DInterface.load(args['intf']) + if aInterface.name != self.__rIntf: + return + + if event[3] == 'intf': + self.__setupImpl(aInterface) + else: + self.__teardownImpl() + + def __setupImpl(self, interface): + #print('Cloner.__setupImpl') + self.__impl = _Clone(self.__local, self.__rNode, self.__rPath, interface) + self.__local.register(self.__lPath, self.__impl) + + def __teardownImpl(self): + self.__local.unregister(self.__lPath, self.__impl) + self.__impl = None + diff --git a/supcon/core.py b/supcon/core.py new file mode 100644 index 0000000..3b68f24 --- /dev/null +++ b/supcon/core.py @@ -0,0 +1,813 @@ +# -*- coding: utf-8 -*- +""" +This module provides the implementation of abstract class `supcon.intf.Node`. +""" + +import sys +import traceback + +import uuid +import struct +import msgpack + +import twisted.internet.defer as defer +import twisted.internet.protocol +import twisted.internet.endpoints + +import twisted.application.service +import twisted.application.internet + +import supcon.intf + +_printArgs = lambda *args: print(', '.join(['{}'.format(i) for i in args])) + +LocalIntf = supcon.intf.DInterface.load({ + 'name': 'supcon.Local', + 'events': [ + { + 'name': 'node', + 'args': [ + {'name': 'node', 'description': 'The node'}, + ], + }, + { + 'name': 'nodeLost', + 'args': [ + {'name': 'node', 'description': 'The lost node'}, + ], + }, + { + 'name': 'path', + 'args': [ + {'name': 'node', 'description': 'The node'}, + {'name': 'path', 'description': 'The path'}, + ], + }, + { + 'name': 'pathLost', + 'args': [ + {'name': 'node', 'description': 'The node'}, + {'name': 'path', 'description': 'The path'}, + ], + }, + { + 'name': 'intf', + 'args': [ + {'name': 'node', 'description': 'The node'}, + {'name': 'path', 'description': 'The path'}, + {'name': 'intf', 'description': 'The interface'}, + ], + }, + { + 'name': 'intfLost', + 'args': [ + {'name': 'node', 'description': 'The node'}, + {'name': 'path', 'description': 'The path'}, + {'name': 'intf', 'description': 'The interface'}, + ], + }, + ], + 'methods': [ + { + 'name': 'nodes', + 'outArgs': [ + {'name': 'nodes', 'description': 'A datastructure about all known nodes, paths and interfaces'}, + ], + }, + ], + 'description': 'All methods and events are only locally available' +}) + +RemoteIntf = supcon.intf.DInterface.load({ + 'name': 'supcon.Remote', + 'events': [ + { + 'name': 'intf', + 'args': [ + {'name': 'path', 'description': 'The path'}, + {'name': 'intf', 'description': 'The interface'}, + ], + }, + { + 'name': 'intfLost', + 'args': [ + {'name': 'path', 'description': 'The path'}, + {'name': 'intf', 'description': 'The interface'}, + ], + }, + ], + 'methods': [ + { + 'name': 'node', + 'outArgs': [ + {'name': 'node', 'description': 'A datastructure about all paths and interfaces of the node'}, + ], + }, + { + 'name': 'on', + 'inArgs': [ + {'name': 'path', 'description': 'The path'}, + {'name': 'intf', 'description': 'The interface'}, + {'name': 'event', 'description': 'The event'}, + ], + }, + { + 'name': 'off', + 'inArgs': [ + {'name': 'path', 'description': 'The path'}, + {'name': 'intf', 'description': 'The interface'}, + {'name': 'event', 'description': 'The event'}, + ], + }, + ], + 'description': 'All methods and events are only used internally' +}) + + +class _Protocol(twisted.internet.protocol.Protocol): + """This class handels handshake and message encoding between nodes on the bus. + + This class is just an implementation detail of the class Node. + """ + def __init__(self): + super().__init__() + self.factory = None + + self.__data = bytearray() + self.__name = None + + def connectionMade(self): + self.sendMesg({ + 'type': 'handshake', + 'name': self.factory.name + }) + + def connectionLost(self, reason): + if not self.__name: + return + self.factory.delNode(self.__name) + + def dataReceived(self, data): + self.__data.extend(data) + + while len(self.__data) > 4: + + size = struct.unpack_from(' bool: + if node in self.__nodes: + return True + + self.__nodes = self.__nodes.addNode(node) + + self.__fire('node', {'node': node}) + return True + + def delNode(self, node: str) -> bool: + if node not in self.__nodes: + return True + + for path in self.__nodes[node]: + for intf in self.__nodes[node][path]: + self.delIntf(node, path, self.__nodes[node][path][intf]) + + self.__nodes = self.__nodes.delNode(node) + self.__fire('nodeLost', {'node': node}) + return True + + def addIntf(self, node: str, path: str, interface: supcon.intf.DInterface) -> bool: + #_printArgs('_KnownMgr.addIntf', node, path, interface) + if node not in self.__nodes: + return False + + if path not in self.__nodes[node]: + self.__nodes = self.__nodes.addPath(node, path) + self.__fire('path', {'node': node, 'path': path}) + + if interface.name in self.__nodes[node][path]: + return True + + self.__nodes = self.__nodes.addInterface(node, path, interface) + self.__fire('intf', {'node': node, 'path': path, 'intf': interface.dump()}) + return True + + def delIntf(self, node: str, path: str, interface: supcon.intf.DInterface) -> bool: + #_printArgs('_KnownMgr.delIntf', node, path, interface) + if not self.__nodes.hasIntf(node, path, interface.name): + return True + + self.__nodes = self.__nodes.delInterface(node, path, interface) + self.__fire('intfLost', {'node': node, 'path': path, 'intf': interface.dump()}) + + if not self.__nodes[node][path]: + self.__nodes = self.__nodes.delPath(node, path) + self.__fire('pathLost', {'node': node, 'path': path}) + + return True + + def getNode(self, node: str) -> supcon.intf.DNode: + if node in self.__nodes: + return self.__nodes[node] + return None + + def setNode(self, newNode: supcon.intf.DNode): + #_printArgs('_KnownMgr.setNode', newNode.dump()) + oldNode = self.getNode(newNode.name) + + for oldPath in oldNode.values(): + for oldIntf in oldPath.values(): + if not newNode.hasIntf(oldPath.name, oldIntf.name): + self.delIntf(oldNode.name, oldPath.name, oldIntf) + + for newPath in newNode.values(): + for newIntf in newPath.values(): + if not oldNode.hasIntf(newPath.name, newIntf.name): + self.addIntf(newNode.name, newPath.name, newIntf) + + def getNodes(self) -> supcon.intf.DNodes: + return self.__nodes + + +class _InformMgr(object): + + def __init__(self): + + self.__keys = {} + self.__nodes = {} + + def on(self, path, intf, event, node): + key = (path, intf, event) + + if key not in self.__nodes: + self.__nodes[key] = {} + self.__nodes[key][node] = True + + if node not in self.__keys: + self.__keys[node] = {} + self.__keys[node][key] = True + + def off(self, path, intf, event, node): + key = (path, intf, event) + if key not in self.__nodes or node not in self.__nodes[key]: + return + + del self.__nodes[key][node] + if not self.__nodes[key]: + del self.__nodes[key] + + del self.__keys[node][key] + if not self.__keys[node]: + del self.__keys[node] + + def nodes(self, path, intf, event): + key = (path, intf, event) + if key not in self.__nodes: + return [] + return self.__nodes[key].keys() + + def delNode(self, node): + if node not in self.__keys: + return + for key in self.__keys[node]: + del self.__nodes[key][node] + if not self.__nodes[key]: + del self.__nodes[key] + del self.__keys[node] + + +class _DeferredMgr(object): + """ This class manages Deferred """ + + def __init__(self): + self.__infos = {} + + def create(self, data=None) -> (str, defer.Deferred): + """Creates a Deferred with an info. + + Args: + data: Some additional data + + Returns: + (str, defer.Deferred): A Tuple of an unique id and a Deferred + """ + pid = str(uuid.uuid4()) + + def canceller(_d): + del self.__infos[pid] + + d = defer.Deferred(canceller) + self.__infos[pid] = (d, data) + + return (pid, d) + + def succeed(self, value, pid): + """Succeeds the Deferred identified by the given unique id with the given + response. + + Args: + pid (str): A unique id of a Deferred created with _DeferredMgr.create() + value (mixed): The value to succeed the Deferred with + """ + if pid not in self.__infos: + return + + d = self.__infos[pid][0] + del self.__infos[pid] + d.callback(value) + + def fail(self, reason, pid): + """Fail the Deferred identified by the given unique id with the given + reason. + + Args: + reason (Exception): The reason to fail the Deferred with + pid (str): A unique id of a Deferred created with _DeferredMgr.create() + """ + if pid not in self.__infos: + return + + d = self.__infos[pid][0] + del self.__infos[pid] + d.errback(reason) + + def failAll(self, reason, predicate): + """Fail all Deferred for which predicate(data) returns true. + + Args: + reason (Exception): The reason to fail the Deferred with + predicate (function): A predicate + """ + for pid, info in self.__infos.copy().items(): + if predicate(pid, info[1]): + del self.__infos[pid] + info[0].errback(reason) + + +class _CallbackMgr(object): + + def __init__(self): + self.__cbs = {} + + def on(self, key, cb): + first = False + if key not in self.__cbs: + self.__cbs[key] = {} + first = True + self.__cbs[key][id(cb)] = cb + return first + + def off(self, key, cb): + if key not in self.__cbs or id(cb) not in self.__cbs[key]: + return False + del self.__cbs[key][id(cb)] + if not self.__cbs[key]: + del self.__cbs[key] + return True + return False + + def fire(self, key, args): + #_printArgs('_CallbackMgr.fire', key, args) + if key not in self.__cbs: + return + for cb in self.__cbs[key].values(): + try: + cb(args, key) + except BaseException as _: + traceback.print_exc() + + def keys(self, predicate): + return [key for key in self.__cbs if predicate(key)] + + +class Node(supcon.intf.Node): + + def __init__(self, name): + super().__init__(name) + + self.__impls = {} + self.__protocols = {} + + self.__factory = _Factory(self.name, self.__addNode, self.__delNode, self.__recvMesg) + + self.__service = twisted.application.service.MultiService() + self.__service.startService() + + fire = lambda event, args: \ + self.__fireEventLocal(self.name, '/', LocalIntf.name, event, args) + + self.__knownMgr = _KnownMgr(fire) + self.__informMgr = _InformMgr() + self.__callbackMgr = _CallbackMgr() + self.__deferredMgr = _DeferredMgr() + + self.__knownMgr.addNode(self.name) + + def connect(self, endpoint): + service = twisted.application.internet.ClientService(endpoint, self.__factory) + self.__service.addService(service) + + def listen(self, endpoint): + endpoint.listen(self.__factory) + + def __addNode(self, node: str, protocol: _Protocol): + if node in self.__protocols: + return False + self.__protocols[node] = protocol + + self.__knownMgr.addNode(node) + + # reestablish existing callbacks by calls to supcon.Remote.on + for key in self.__callbackMgr.keys(lambda key: key[0] == node): + self.__callRemote(node, '/', RemoteIntf.name, 'on', { + 'path': key[1], + 'intf': key[2], + 'event': key[3] + }) + + self.__callRemote(node, '/', RemoteIntf.name, 'node', {}).addCallback( + lambda args: self.__knownMgr.setNode(supcon.intf.DNode.load(args['node'])) + ).addErrback(_printArgs) + + return True + + def __delNode(self, node): + if node not in self.__protocols: + return False + del self.__protocols[node] + + self.__knownMgr.delNode(node) + self.__informMgr.delNode(node) + + reason = RuntimeError('node {} lost'.format(node)) + predicate = lambda pid, data: data[0] == node + self.__deferredMgr.failAll(reason, predicate) + + return True + + def nodes(self): + return self.__knownMgr.getNodes() + + def register(self, path: str, impl: supcon.intf.Implementation): + path = supcon.intf.DPath.toName(path) + + if path not in self.__impls: + self.__impls[path] = {} + + if impl.intf in self.__impls[path]: + raise ValueError('interface {} already registered at path {}'.format(impl.intf, path)) + + self.__impls[path][impl.intf] = { + 'impl': impl, + 'fire': lambda event, args: self.__fireImplEvent(path, impl.intf, event, args) + } + impl.addFireCb(self.__impls[path][impl.intf]['fire']) + + self.__knownMgr.addIntf(self.name, path, impl.interface) + self.__broadcastEvent('intf', {'path': path, 'intf': impl.interface.dump()}) + + def unregister(self, path: str, impl: supcon.intf.Implementation): + path = supcon.intf.DPath.toName(path) + + if path not in self.__impls: + raise ValueError('unknown path {}'.format(path)) + + if impl.intf not in self.__impls[path]: + raise ValueError('unknown interface {} at path {}'.format(impl.intf, path)) + + if impl != self.__impls[path][impl.intf]['impl']: + raise ValueError('unknown implementation for interface {} at path {}'.format(impl.intf, path)) + + impl.delFireCb(self.__impls[path][impl.intf]['fire']) + del self.__impls[path][impl.intf] + + self.__knownMgr.delIntf(self.name, path, impl.interface) + self.__broadcastEvent('intfLost', {'path': path, 'intf': impl.interface.dump()}) + + def on(self, node, path, intf, event, cb): + if self.__callbackMgr.on((node, path, intf, event), cb): + if node != self.name and node in self.__protocols: + self.__callRemote(node, '/', RemoteIntf.name, 'on', { + 'path': path, 'intf': intf, 'event': event + }) + + def off(self, node, path, intf, event, cb): + if self.__callbackMgr.off((node, path, intf, event), cb): + if node != self.name and node in self.__protocols: + self.__callRemote(node, '/', RemoteIntf.name, 'off', { + 'path': path, 'intf': intf, 'event': event + }) + + def call(self, node, path, intf, method, args) -> defer.Deferred: + # TODO: don't raise Exceptions + assert path != '/' or intf != RemoteIntf.name, \ + 'unable to call: method {} of interface {} at path /'.format(method, RemoteIntf.name) + + if node == self.name: + if path == '/' and intf == LocalIntf.name: + return self.__callLocal(method, args) + return self.__callImpl(path, intf, method, args) + return self.__callRemote(node, path, intf, method, args) + + def __callImpl(self, path, intf, method, args) -> defer.Deferred: + # TODO: don't raise Exception + assert path != '/' or intf != LocalIntf.name, \ + 'unable to call impl: method {} of interface {} at path /'.format(method, LocalIntf.name) + assert path != '/' or intf != RemoteIntf.name, \ + 'unable to call impl: method {} of interface {} at path /'.format(method, RemoteIntf.name) + + try: + if path not in self.__impls: + raise ValueError('unknown path') # TODO: nicer error message + if intf not in self.__impls[path]: + raise ValueError('unknown intf') # TODO: nicer error message + if method not in self.__impls[path][intf]['impl'].interface.methods: + raise ValueError('unknown method') # TODO: nicer error message + + d = self.__impls[path][intf]['impl'].call(method, args) + if not isinstance(d, defer.Deferred): + d = defer.succeed(d) + except BaseException as e: + d = defer.fail(e) + + return d + + def __callLocal(self, method, args) -> defer.Deferred: + # TODO: don't raise Exception + assert method in LocalIntf.methods, \ + '{} is not an method of interface {} at path /'.format(method, RemoteIntf.name) + LocalIntf.methods[method].validateInArgs(args) + + if method == 'nodes': + return defer.succeed({'nodes': self.__knownMgr.getNodes().dump()}) + + return defer.fail(RuntimeError('method {} not yet implemented'.format(method))) + + def __callRemote(self, node, path, intf, method, args) -> defer.Deferred: + # TODO: don't raise Exception + assert path != '/' or intf != LocalIntf.name, \ + 'unable to call remote: method {} of interface {} at path /'.format(method, LocalIntf.name) + + # TODO: validate args + + (cid, p) = self.__deferredMgr.create((node, path, intf, method)) + self.__sendCall(node, path, intf, method, args, cid) + return p + + def __fireImplEvent(self, path, intf, event, args): + assert path != '/' or intf != LocalIntf.name, \ + 'unable to fire impl: event {} of interface {} at path /'.format(event, RemoteIntf.name) + assert path != '/' or intf != RemoteIntf.name, \ + 'unable to fire impl: event {} of interface {} at path /'.format(event, RemoteIntf.name) + + self.__fireEventLocal(self.name, path, intf, event, args) + self.__fireEventRemote(path, intf, event, args) + + def __fireEventLocal(self, node: str, path: str, intf: str, event: str, args: dict): + assert path != '/' or intf != RemoteIntf.name, \ + 'unable to fire local: event {} of interface {} at path /'.format(event, RemoteIntf.name) + #_printArgs('Node.__fireEventLocal', node, path, intf, event, args) + + # TODO: validate args + + self.__callbackMgr.fire((node, path, intf, event), args) + + def __fireEventRemote(self, path: str, intf: str, event: str, args: dict): + assert path != '/' or intf != LocalIntf.name, \ + 'unable to fire remote: event {} of interface {} at path /'.format(event, LocalIntf.name) + + for node in self.__informMgr.nodes(path, intf, event): + self.__sendEvent(node, path, intf, event, args) + + def __broadcastEvent(self, event: str, args: dict): + try: + assert event in RemoteIntf.events, \ + '{} is not an event of interface {} at path /'.format(event, RemoteIntf.name) + RemoteIntf.events[event].validateArgs(args) + except BaseException as _: + traceback.print_exc() + return + + for node in self.__protocols: + self.__sendEvent(node, '/', RemoteIntf.name, event, args) + + def __recvMesg(self, node: str, mesg: dict): + """Gets called by _Protocol on new Message""" + if node not in self.__protocols: + return False + + if mesg['type'] == 'call': + self.__recvCall(node, mesg['path'], mesg['intf'], mesg['name'], mesg['args'], mesg['id']) + elif mesg['type'] == 'error': + self.__recvError(node, mesg['args'], mesg['id']) + elif mesg['type'] == 'response': + self.__recvResult(node, mesg['args'], mesg['id']) + elif mesg['type'] == 'event': + self.__recvEvent(node, mesg['path'], mesg['intf'], mesg['name'], mesg['args']) + else: + return False + + return True + + def __recvCall(self, node: str, path: str, intf: str, method: str, args: dict, cid: str): + try: + assert path != '/' or intf != LocalIntf.name, \ + 'unable to recv call: method {} of interface {} at path / from node {}'.format(method, intf, node) + + if path == '/' and intf == RemoteIntf.name: + assert method in RemoteIntf.methods, \ + '{} is not an method of interface {} at path /'.format(method, RemoteIntf.name) + RemoteIntf.methods[method].validateInArgs(args) + + if method == 'node': + d = defer.succeed({'node': self.__knownMgr.getNode(self.name).dump()}) + elif method == 'on': + self.__informMgr.on(args['path'], args['intf'], args['event'], node) + d = defer.succeed({}) + elif method == 'off': + self.__informMgr.on(args['path'], args['intf'], args['event'], node) + d = defer.succeed({}) + else: + raise ValueError('method {} is not yet implemented'.format(method)) + else: + d = self.__callImpl(path, intf, method, args) + except BaseException as e: + traceback.print_exc() + d = defer.fail(e) + + d.addCallbacks( + lambda result: self.__sendResult(node, result, cid), + lambda reason: self.__sendError(node, repr(reason), cid) + ).addErrback(_printArgs) + + def __recvError(self, _node: str, reason: str, cid: str): + try: + self.__deferredMgr.fail(RuntimeError(reason), cid) + except BaseException as _: + traceback.print_exc() + + def __recvResult(self, _node: str, result: dict, cid: str): + try: + self.__deferredMgr.succeed(result, cid) + except BaseException as _: + traceback.print_exc() + + def __recvEvent(self, node: str, path: str, intf: str, event: str, args: dict): + try: + assert path != '/' or intf != LocalIntf.name, \ + 'unable to recv event: event {} of interface {} at path /'.format(event, LocalIntf.name) + + if path == '/' and intf == RemoteIntf.name: + assert event in RemoteIntf.events, \ + '{} is not an event of interface {} at path /'.format(event, RemoteIntf.name) + RemoteIntf.events[event].validateArgs(args) + + self.__recvEventRemote(node, event, args) + else: + self.__fireEventLocal(node, path, intf, event, args) + except BaseException as _: + traceback.print_exc() + + def __recvEventRemote(self, node, event, args): + if event == 'intf': + path = args['path'] + interface = supcon.intf.DInterface.load(args['intf']) + + self.__knownMgr.addIntf(node, path, interface) + + elif event == 'intfLost': + path = args['path'] + interface = supcon.intf.DInterface.load(args['intf']) + + self.__knownMgr.delIntf(node, path, interface) + + reason = RuntimeError('interface {} at path {} on node {} lost'.format(node, path, interface.name)) + predicate = lambda pid, data: data[0] == node and data[1] == path and data[2] == interface.name + self.__deferredMgr.failAll(reason, predicate) + + else: + raise ValueError('event {} not yet implemented'.format(event)) + + def __sendCall(self, node: str, path: str, intf: str, method: str, args: dict, cid: str) -> bool: + try: + assert path != '/' or intf != LocalIntf.name, \ + 'unable to send call: method {} of interface {} at path /'.format(method, LocalIntf.name) + if node not in self.__protocols: + raise RuntimeError('unknown node {}'.format(node)) + + self.__protocols[node].sendMesg({ + 'type': 'call', + 'path': path, + 'intf': intf, + 'name': method, + 'args': args, + 'id': cid + }) + return True + except BaseException as _: + traceback.print_exc() + return False + + def __sendError(self, node: str, reason: str, cid: str) -> bool: + try: + if node not in self.__protocols: + raise RuntimeError('unknown node {}'.format(node)) + + self.__protocols[node].sendMesg({ + 'type': 'error', + 'args': reason, + 'id': cid + }) + return True + except BaseException as _: + traceback.print_exc() + return False + + def __sendResult(self, node: str, result: dict, cid: str) -> bool: + try: + if node not in self.__protocols: + raise RuntimeError('unknown node {}'.format(node)) + + self.__protocols[node].sendMesg({ + 'type': 'response', + 'args': result, + 'id': cid + }) + return True + except BaseException as _: + traceback.print_exc() + return False + + def __sendEvent(self, node: str, path: str, intf: str, event: str, args: dict) -> bool: + try: + assert path != '/' or intf != LocalIntf.name, \ + 'unable to send event: event {} of interface {} at path /'.format(event, LocalIntf.name) + if node not in self.__protocols: + traceback.print_stack() + raise RuntimeError('unknown node {}'.format(node)) + + self.__protocols[node].sendMesg({ + 'type': 'event', + 'path': path, + 'intf': intf, + 'name': event, + 'args': args + }) + return True + except BaseException as _: + exc_info = sys.exc_info() + traceback.print_exception(*exc_info) + return False diff --git a/supcon/graph.py b/supcon/graph.py new file mode 100644 index 0000000..e6b7b0f --- /dev/null +++ b/supcon/graph.py @@ -0,0 +1,144 @@ + + +import struct +import msgpack + +import twisted.internet.defer as defer +import twisted.internet.interfaces +import twisted.internet.protocol + +class _Protocol(twisted.internet.protocol.Protocol): + + def __init__(self): + super().__init__() + self.factory = None + + self.__data = bytearray() + self.__name = None + + def dataReceived(self, data): + self.__data.extend(data) + + while len(self.__data) > 4: + + size = struct.unpack_from(' str: + """str: the name""" + return self.__name + + @classmethod + def toName(cls, value) -> str: + """Converts the value into a name. If this is impossible a ValueError is + raised. + + Args: + value (any): the value + Raises: + ValueError + """ + value = str(value) + if not cls.regex.fullmatch(value): + raise ValueError("value {} must match {} fully".format(value, cls.regex.pattern)) + return value + + +class NamedAndDescribed(Named): + """A base class for the interface describing classes `supcon.intf.Argument`, + `supcon.intf.Event`, `supcon.intf.Method` and `supcon.intf.Interface` that + all have the properties name and description.""" + + def __init__(self, name, description=''): + """Initializes the NamedAndDescribed instance + + Args: + name (str): the name of the argument + description (str): a description of the argument + """ + Named.__init__(self, name) + self.__description = self.toDescription(description) + + @property + def description(self) -> str: + """str: a description""" + return self.__description + + @classmethod + def toDescription(cls, value) -> str: + """Converts the value into a description. If this is impossible a ValueError + is raised. + + Args: + value (any): the value + Raises: + ValueError + """ + return str(value) + + +class DArgument(supcon.util.Dumpable, NamedAndDescribed): + """Describes an input or output argument of a `supcon.intf.DMethod` or an + argument of a `supcon.intf.DEvent`""" + + regex = re.compile('[a-zA-Z0-9]+') + + def validate(self, argument): + """Validates the given argument value. Raises an ValueError if validation + fails. + """ + pass + + def dump(self): + data = {'name': self.name} + if self.description != '': + data['description'] = self.description + return data + + @classmethod + def load(cls, data): + return cls(**data) + + +class DArguments(supcon.util.NamedList): + """A readonly map of `supcon.intf.DArgument` instances""" + vtype = DArgument + + +class DEvent(supcon.util.Dumpable, NamedAndDescribed): + """Describes an event that can be emitted by an implementation on the bus""" + + regex = re.compile('[a-zA-Z0-9]+') + + def __init__(self, name, args=DArguments(), description=''): + """Initializes the Event + + Args: + name (str): the name of the event + args (DArguments): the list of arguments of the event + description (str): a description of the event + """ + NamedAndDescribed.__init__(self, name, description) + self.__args = DArguments.to(args) + + @property + def args(self) -> DArguments: + """DArguments: the list of arguments of the event""" + return self.__args + + def validateArgs(self, args): + """Validates the given argument map. Raises an ValueError if validation + fails. + """ + for arg in args: + if arg not in self.__args: + raise ValueError('event {} has no argument {}'.format(self.name, arg)) + self.__args[arg].validate(args[arg]) + for arg in self.__args: + if arg not in args: + raise ValueError('event {} needs argument {}'.format(self.name, arg)) + + def dump(self): + data = {'name': self.name} + if self.args: + data['args'] = self.args.dump() + if self.description != '': + data['description'] = self.description + return data + + @classmethod + def load(cls, data): + return cls(**data) + + +class DEvents(supcon.util.NamedList): + """A readonly map of `supcon.intf.DEvent` instances""" + vtype = DEvent + + +class DMethod(supcon.util.Dumpable, NamedAndDescribed): + """A DMethod that can be called on an Object on the Bus""" + + regex = re.compile('[a-zA-Z0-9]+') + + def __init__(self, name, inArgs=DArguments(), outArgs=DArguments(), description=''): + """Initializes the Event + + Args: + name (str): the name of the method + inArgs (DArguments): the list of input arguments of the method + outArgs (DArguments): the list of output arguments of the method + description (str): a description of the method + """ + NamedAndDescribed.__init__(self, name, description) + self.__inArgs = DArguments.to(inArgs)# + self.__outArgs = DArguments.to(outArgs) + + @property + def inArgs(self) -> DArguments: + """DArguments: The input arguments of the method""" + return self.__inArgs + + @property + def outArgs(self) -> DArguments: + """DArguments: The output arguments of the method""" + return self.__outArgs + + def validateInArgs(self, inArgs): + """Validates the given argument Mapping. Raises an ValueError if validation fails + """ + for arg in inArgs: + if arg not in self.__inArgs: + raise ValueError('method {} has no input argument {}'.format(self.name, arg)) + self.__inArgs[arg].validate(inArgs[arg]) + for arg in self.__inArgs: + if arg not in inArgs: + raise ValueError('method {} needs input argument {}'.format(self.name, arg)) + + def validateOutArgs(self, outArgs): + """Validates the given argument Mapping. Raises an ValueError if validation fails + """ + for arg in outArgs: + if arg not in self.__outArgs: + raise ValueError('method {} has no output argument {}'.format(self.name, arg)) + self.__outArgs[arg].validate(outArgs[arg]) + for arg in self.__outArgs: + if arg not in outArgs: + raise ValueError('method {} needs output argument {}'.format(self.name, arg)) + + def dump(self): + data = {'name': self.name} + if self.inArgs: + data['inArgs'] = self.inArgs.dump() + if self.outArgs: + data['outArgs'] = self.outArgs.dump() + if self.description != '': + data['description'] = self.description + return data + + @classmethod + def load(cls, data): + return cls(**data) + + +class DMethods(supcon.util.NamedList): + """A readonly map of `supcon.intf.DMethod` instances""" + vtype = DMethod + + +class DInterface(supcon.util.Dumpable, NamedAndDescribed): + """An Interface that is implemented by an Object on the Bus""" + + regex = re.compile('([a-zA-Z0-9_]+\\.)*[a-zA-Z0-9_]+') + + def __init__(self, name, events=DEvents(), methods=DMethods(), description=''): + """Initializes the Event + + Args: + name (str): the name of the interface + events (DEvents): the list of events of the interface + methods (DMethods): the list of methods of the interface + description (str): a description of the interface + """ + NamedAndDescribed.__init__(self, name, description) + self.__events = DEvents.to(events) + self.__methods = DMethods.to(methods) + + @property + def events(self) -> DEvents: + """DEvent: the list of events this interface can emit""" + return self.__events + + @property + def methods(self) -> DMethods: + """DMethods: the list of methods this interface provides""" + return self.__methods + + def validateEvent(self, event, args): + """Validates that the given event is an event of this interface and that + the given arguments are arguments of the event + + Args: + event (str): the event + args (dict): the arguments + Raises: + ValueError + """ + event = str(event) + if event not in self.__events: + raise ValueError('event {} is unknown'.format(event)) + self.__events[event].validateArgs(args) + + def validateCall(self, method, inArgs): + """Asserts that the given method is a method of this interface and that + the given arguments are input arguments of the method + + Args: + event (str): the event + inArgs (dict): the input arguments + Raises: + AssertationError + """ + method = str(method) + if method not in self.__methods: + raise ValueError('method {} is unknown'.format(method)) + self.__methods[method].validateInArgs(inArgs) + + def validateReturn(self, method, outArgs): + """Asserts that the given method is a method of this interface and that + the given arguments are output arguments of the method + + Args: + event (str): the event + outArgs (dict): the output arguments + Raises: + AssertationError + """ + method = str(method) + if method not in self.__methods: + raise ValueError('method {} is unknown'.format(method)) + self.__methods[method].validateOutArgs(outArgs) + + def dump(self): + data = {'name': self.name} + if self.events: + data['events'] = self.events.dump() + if self.methods: + data['methods'] = self.methods.dump() + if self.description != '': + data['description'] = self.description + return data + + @classmethod + def load(cls, data): + return cls(**data) + + +class DInterfaces(supcon.util.NamedList): + """A readonly map of `supcon.intf.DInterface` instances""" + vtype = DInterface + + +class DPath(Named, supcon.util.NamedList): + """A named readonly map of `supcon.intf.DInterface` instances""" + + vtype = DInterface + regex = re.compile('/(([a-zA-Z0-9_]+/)*[a-zA-Z0-9_]+)?') + + def __init__(self, name, interfaces=None): + """Initializes the Path instance + + Args: + name (str): the name of the argument + interfaces (): ... + """ + if interfaces is None: + interfaces = [] + Named.__init__(self, name) + supcon.util.NamedList.__init__(self, interfaces) + + def dump(self): + return { + 'name': self.name, + 'interfaces': [value.dump() for value in self.values()] + } + + @classmethod + def load(cls, data) -> 'DPath': + return cls(**data) + + def addInterface(self, interface: DInterface) -> 'DPath': + if interface.name in self: + raise ValueError('interface {} at path {} already exists'.format(interface.name, self.name)) + interfaces = itertools.chain(self.values(), [interface]) + return DPath(self.name, interfaces) + + def delInterface(self, interface: DInterface) -> 'DPath': + if interface.name not in self: + raise ValueError('interface {} at path {} does not exist'.format(interface.name, self.name)) + interfaces = (v for v in self.values() if v.name != interface.name) + return DPath(self.name, interfaces) + + +class DNode(Named, supcon.util.NamedList): + """A named readonly map of `supcon.intf.DPath` instances""" + + vtype = DPath + regex = re.compile('.+') + + def __init__(self, name, paths=None): + """Initializes the Path instance + + Args: + name (str): the name of the argument + paths (): ... + """ + if paths is None: + paths = [] + + Named.__init__(self, name) + supcon.util.NamedList.__init__(self, paths) + + def dump(self): + return { + 'name': self.name, + 'paths': [value.dump() for value in self.values()] + } + + @classmethod + def load(cls, data) -> 'DPath': + return cls(**data) + + def hasPath(self, path: str) -> bool: + return path in self + + def hasIntf(self, path: str, intf: str) -> bool: + return path in self and intf in self[path] + + def addPath(self, path: str) -> 'DNode': + if path in self: + raise ValueError('path {} on node {} already exists'.format(path, self.name)) + paths = itertools.chain(self.values(), [DPath(path)]) + return DNode(self.name, paths) + + def delPath(self, path: str) -> 'DNode': + if path not in self: + raise ValueError('path {} on node {} does not exist'.format(path, self.name)) + if self[path]: + raise ValueError('path {} on node {} is not empty'.format(path, self.name)) + paths = (v for v in self.values() if v.name != path) + return DNode(self.name, paths) + + def addInterface(self, path: str, interface: DInterface) -> 'DNode': + if path not in self: + raise ValueError('path {} on node {} does not exist'.format(path, self.name)) + if interface.name in self[path]: + raise ValueError('interface {} at path {} on node {} already exists'.format(interface.name, path, self.name)) + paths = (v for v in self.values() if v.name != path) + paths = itertools.chain(paths, [self[path].addInterface(interface)]) + return DNode(self.name, paths) + + def delInterface(self, path: str, interface: DInterface) -> 'DNode': + if path not in self: + raise ValueError('path {} on node {} does not exist'.format(path, self.name)) + if interface.name not in self[path]: + raise ValueError('interface {} at path {} on node {} does not exist'.format(interface.name, path, self.name)) + paths = (v for v in self.values() if v.name != path) + paths = itertools.chain(paths, [self[path].delInterface(interface)]) + return DNode(self.name, paths) + + +class DNodes(supcon.util.NamedList): + """A readonly map of `supcon.intf.DNode` instances""" + vtype = DNode + + def hasNode(self, node: str) -> bool: + return node in self + + def hasPath(self, node: str, path: str) -> bool: + return node in self and path in self[node] + + def hasIntf(self, node: str, path: str, intf: str) -> bool: + return node in self and path in self[node] and intf in self[node][path] + + def addNode(self, node: str) -> 'DNodes': + if node in self: + raise ValueError('node {} already exists'.format(node)) + nodes = itertools.chain(self.values(), [DNode(node)]) + return DNodes(nodes) + + def delNode(self, node: str) -> 'DNodes': + if node not in self: + raise ValueError('node {} does not exist'.format(node)) + if self[node]: + raise ValueError('node {} is not empty'.format(node)) + nodes = (v for v in self.values() if v.name != node) + return DNodes(nodes) + + def addPath(self, node: str, path: str) -> 'DNodes': + if node not in self: + raise ValueError('node {} does not exist'.format(node)) + nodes = (v for v in self.values() if v.name != node) + nodes = itertools.chain(nodes, [self[node].addPath(path)]) + return DNodes(nodes) + + def delPath(self, node: str, path: str) -> 'DNodes': + if node not in self: + raise ValueError('node {} does not exist'.format(node)) + nodes = (v for v in self.values() if v.name != node) + nodes = itertools.chain(nodes, [self[node].delPath(path)]) + return DNodes(nodes) + + def addInterface(self, node: str, path: str, interface: DInterface) -> 'DNodes': + if node not in self: + raise ValueError('node {} does not exist'.format(node)) + nodes = (v for v in self.values() if v.name != node) + nodes = itertools.chain(nodes, [self[node].addInterface(path, interface)]) + return DNodes(nodes) + + def delInterface(self, node: str, path: str, interface: DInterface) -> 'DNodes': + if node not in self: + raise ValueError('node {} does not exist'.format(node)) + nodes = (v for v in self.values() if v.name != node) + nodes = itertools.chain(nodes, [self[node].delInterface(path, interface)]) + return DNodes(nodes) + + +class Implementation(object): + """The base class for interface implementations""" + + def __init__(self, interface: DInterface): + """ + Args: + interface (DInterface): the interface that is implemented by this Implementation + """ + if not isinstance(interface, DInterface): + raise ValueError('interface must be an instance of {}'.format(DInterface)) + self.__interface = interface + + self.__callCbs = {} + self.__fireCbs = [] + + @property + def intf(self) -> str: + """str: The name of the implemented interface""" + return self.__interface.name + + @property + def interface(self) -> DInterface: + """Interface: The implemented interface""" + return self.__interface + + def setCallCb(self, method: str, cb): + """Sets a callback for the given method. The method must be a method of the + interface this Implementation implements. The callback must implement the + given method. The callback gets called by calls to Implementation.call() + + Args: + method (str): the interface method that the callback implements + cb (callable): the callback + """ + method = DMethod.toName(method) + if method in self.__callCbs: + raise ValueError('Callback for method {} is already set!'.format(method)) + if method not in self.__interface.methods: + raise ValueError('Interface has no method {}!'.format(method)) + self.__callCbs[method] = cb + + def call(self, method: str, inArgs) -> defer.Deferred: + """Calls the given interface method with the given arguments. This method + calls the callback set by Implementation.setCallCb() + + Args: + method (str): the called interface method + inArgs (Mapping): a map of input arguments + Returns: + defer.Deferred: Resolves with the result of the called method + """ + def validateReturn(outArgs): + try: + self.__interface.validateReturn(method, outArgs) + except: + traceback.print_exc() + raise + return outArgs + + try: + self.__interface.validateCall(method, inArgs) + if method not in self.__callCbs: + raise ValueError('Callback for method {} is not set!'.format(method)) + + d = self.__callCbs[method](**inArgs) + if not isinstance(d, defer.Deferred): + d = defer.succeed(d) + d.addCallback(validateReturn) + except BaseException as e: + traceback.print_exc() + d = defer.fail(e) + + return d + + def addFireCb(self, cb): + """Adds a callback that gets called, when this Implementation fires an + event. + + Args: + cb (callable): the callback + """ + self.__fireCbs.append(cb) + + def delFireCb(self, cb): + """Removes a callback that gets called, when this Implementation fires an + event. + + Args: + cb (callable): the callback + """ + self.__fireCbs.remove(cb) + + def fire(self, event: str, args): + """Fires the given event with the given arguments. + + Args: + event (str): the event name + args (collecion.Mapping): the event arguments + """ + self.__interface.validateEvent(event, args) + + for cb in self.__fireCbs: + cb(event, args) + + +class Object(object): + + def __init__(self, interfaces=None): + """ + Args: + interfaces (DInterfaces): the interfaces that are implemented by this Object + """ + if interfaces is None: + interfaces = [] + + self.__intfs = {} + self.__impls = {} + + for interface in interfaces: + if not isinstance(interface, DInterface): + raise ValueError('interface must be an instance of {}'.format(DInterface)) + self.__intfs[interface.name] = interface + + implementation = Implementation(interface) + self.__impls[implementation.intf] = implementation + + @property + def interfaces(self): + return self.__intfs.values() + + @property + def implementations(self): + return self.__impls.values() + + def interface(self, intf): + return self.__intfs[intf] + + def implementation(self, intf): + return self.__impls[intf] + + def setCallCb(self, intf: str, method: str, cb): + self.__impls[intf].setCallCb(method, cb) + + def fire(self, intf: str, event: str, args): + self.__impls[intf].fire(event, args) + + +class Node(abc.ABC): + """The Node Interface. This class defines the methods that participants can + use to access the supcon bus.""" + + def __init__(self, name): + super().__init__() + self.__name = self.toName(name) + + @property + def name(self) -> str: + """str: The name of the node on the bus""" + return self.__name + + @classmethod + def toName(cls, value) -> str: + return DNode.toName(value) + + @abc.abstractmethod + def nodes(self) -> DNodes: + """list[str]: The currently connected nodes""" + raise NotImplementedError() + + @abc.abstractmethod + def connect(self, endpoint): + """Connects the node to the given endpoint. + + If the connection failes or closes the connection gets reestablished with an + exponential timeout up to two minutes. + + Args: + endpoint (twisted.internet.interfaces.IStreamClientEndpoint): + """ + raise NotImplementedError() + + @abc.abstractmethod + def listen(self, endpoint): + """Listens at the given endpoint for incoming connections + + Args: + endpoint (twisted.internet.interfaces.IStreamServerEndpoint): + """ + raise NotImplementedError() + + @abc.abstractmethod + def register(self, path: str, impl: Implementation): + """Registers an implementation with the node + + Args: + impl (Implementation): + """ + raise NotImplementedError() + + @abc.abstractmethod + def unregister(self, path: str, impl: Implementation): + """Removes an implementation from the node + + Args: + impl (supcon.intf.Implementation): + """ + raise NotImplementedError() + + @abc.abstractmethod + def call(self, node: str, path: str, intf: str, method: str, args: dict) -> defer.Deferred: + """Calls a method on the bus + + Args: + node (str): a node on the bus + path (str): a path on the given node + intf (str): an interface at the given path + method (str): a method of the given interface + args (dict): a dict of method arguments + + Returns: + defer.Deferred: + """ + raise NotImplementedError() + + @abc.abstractmethod + def on(self, node: str, path: str, intf: str, event: str, cb): + """Registers a callback for an event on the bus + + Args: + node (str): a node on the bus + path (str): a path on the given node + intf (str): an interface at the given path + event (str): a method of the given interface + cb (callable): a callable that gets called with a dict of event arguments + """ + raise NotImplementedError() + + @abc.abstractmethod + def off(self, node: str, path: str, intf: str, event: str, cb): + """Unregisters a callback for an event on the bus + + Args: + node (str): a node on the bus + path (str): a path on the given node + intf (str): an interface at the given path + event (str): a method of the given interface + cb (callable): a callable that gets called with a dict of event arguments + """ + raise NotImplementedError() diff --git a/supcon/keyed.py b/supcon/keyed.py new file mode 100644 index 0000000..a30e9a4 --- /dev/null +++ b/supcon/keyed.py @@ -0,0 +1,80 @@ + +class KeyedMethod(): + + def __init__(self, key, fget): + self.__key = key + self.__fget = fget + self.__doc__ = fget.__doc__ + + @property + def key(self): + return self.__key + + @property + def fget(self): + return self.__fget + + def __get__(self, obj, objType=None): + if obj is None: + return self + if self.__fget is None: + raise AttributeError("unreadable attribute") + return lambda *args, **kwargs: self.__fget(obj, *args, **kwargs) + +def lKeyedMethods(aClass): + """ + Returns a dict of all KeyedMethods of the given class indexed by attribute + name. The dict does not contain the KeyedMethods of any super class. + """ + if aClass is object: + return {} + + lAttrs = vars(aClass) + if not '_lKeyedMethods' in lAttrs: + methods = {} + for name, attr in lAttrs.items(): + if isinstance(attr, KeyedMethod): + methods[name] = attr + aClass._lKeyedMethods = methods + return aClass._lKeyedMethods + +def mKeyedMethods(aClass): + """ + Returns a dict of all KeyedMethods of the given class indexed by attribute + name. The dict contains the KeyedMethods of all super classes that are not + hidden by the method resolution order. + """ + if not '_mKeyedMethods' in vars(aClass): + methods = {} + for sClass in reversed(aClass.mro()): + methods.update(lKeyedMethods(sClass)) + aClass._mKeyedMethods = methods + return aClass._mKeyedMethods + +def iKeyedMethods(aClass): + """ + Returns a dict of all KeyedMethods of the given class indexed by key. The dict + contains the KeyedMethods of all super classes that are not hidden by the + method resolution order. + """ + if not '_iKeyedMethods' in vars(aClass): + methods = {} + for _name, attr in mKeyedMethods(aClass).items(): + if attr.key in methods: + raise ValueError() + methods[attr.key] = attr + aClass._iKeyedMethods = methods + return aClass._iKeyedMethods + +def keyedMethod(key): + """ + Can be used to decorate a method with a key + """ + return lambda fget: KeyedMethod(key, fget) + +def callKeyedMethod(obj, key, *args, **kwargs): + """ + Resolves a keyed method by the given key and and calls it with the given + arguments. + """ + return iKeyedMethods(type(obj))[key].__get__(obj)(*args, **kwargs) diff --git a/supcon/store.py b/supcon/store.py new file mode 100644 index 0000000..d734dbd --- /dev/null +++ b/supcon/store.py @@ -0,0 +1,90 @@ + +import msgpack + +import twisted.internet.defer as defer +import twisted.internet.threads as threads + +class Store(object): + + def load(self, key: str) -> defer.Deferred: + pass + + def store(self, key: str, data: bytes) -> defer.Deferred: + pass + + def delete(self, key: str) -> defer.Deferred: + pass + +def readFile(name: str) -> defer.Deferred: + print('readFile') + def inThread(): + print('readFile > inThread') + with open(name, 'r+b') as fd: + return fd.read() + + return threads.deferToThread(inThread) + +def writeFile(name: str, data: bytes) -> defer.Deferred: + print('writeFile') + def inThread(): + print('writeFile > inThread') + with open(name, 'w+b') as fd: + return fd.write(data) + + return threads.deferToThread(inThread) + +class FileStore(Store): + + def __init__(self, name): + self.__name = name + self.__data = {} + self.__last = self.__readData() + + @defer.inlineCallbacks + def __readData(self): + data = yield readFile(self.__name) + data = msgpack.unpackb(data, encoding='utf-8') + if isinstance(data, dict): + self.__data = data + + @property + def name(self): + return self.__name + + def __queue(self, job) -> defer.Deferred: + d = defer.Deferred() + + def trampolineSucceed(result): + if isinstance(result, defer.Deferred): + result.addCallbacks(trampolineSucceed, trampolineFail) + else: + d.callback(result) + + def trampolineFail(result): + if isinstance(result, defer.Deferred): + result.addCallbacks(trampolineSucceed, trampolineFail) + else: + d.errback(result) + + self.__last.addBoth(lambda _: job()) + self.__last.addCallbacks(trampolineSucceed, trampolineFail) + + return d + + def load(self, key: str) -> defer.Deferred: + def job(): + return self.__data[key] if key in self.__data else None + return self.__queue(job) + + def store(self, key: str, data) -> defer.Deferred: + def job(): + self.__data[key] = data + return writeFile(self.__name, msgpack.packb(self.__data, use_bin_type=True)) + return self.__queue(job) + + def delete(self, key: str) -> defer.Deferred: + def job(): + if key in self.__data: + del self.__data[key] + return writeFile(self.__name, msgpack.packb(self.__data, use_bin_type=True)) + return self.__queue(job) diff --git a/supcon/switch.py b/supcon/switch.py new file mode 100644 index 0000000..6726e2b --- /dev/null +++ b/supcon/switch.py @@ -0,0 +1,208 @@ +# -*- coding: utf-8 -*- + +import pigpio +import supcon.util +import supcon.intf + +""" the interface com.screwerk.Sensor """ +SensorIntf = supcon.intf.DInterface.load({ + 'name': + 'com.screwerk.Sensor', + 'events': [{ + 'name': 'changed', + 'args': [ + { + 'name': 'value', 'description': 'The new sensor value' + } + ], + 'description': 'Fires when the sensor value changes' + }], + 'methods': [{ + 'name': 'value', + 'outArgs': [ + { + 'name': 'value', 'description': 'The sensor value' + } + ], + 'description': 'Returns the sensor value' + }] +}) + +""" the interface com.screwerk.Switch """ +SwitchIntf = supcon.intf.DInterface.load({ + 'name': 'com.screwerk.Switch', + 'methods': [ + { + 'name': 'on', 'description': 'Toggles the switch on' + }, { + 'name': 'off', 'description': 'Toggles the switch off' + }, { + 'name': 'toggle', 'description': 'Toggles the switch' + } + ] +}) + + +def returnTrue(*args): return True +def returnNone(*args): return None +def returnFirst(*args): return args[0] + + +class SoftSensor(object): + def __init__(self, value=None, guard=None, effect=None, convert=None): + + self.__guard = guard or returnTrue + self.__effect = effect or returnNone + self.__convert = convert or returnFirst + + self.__value = self.__convert(value) + + self.__sensor = supcon.intf.Implementation(SensorIntf) + self.__sensor.setCallCb('value', lambda: {'value': self.__value}) + + @property + def value(self): + return self.__value + + @property + def sensor(self): + return self.__sensor + + def update(self, value): + value = self.__convert(value) + if self.__value == value: + return False + if not self.__guard(value, self): + return False + + self.__value = value + self.__effect(value, self) + self.__sensor.fire('changed', {'value': value}) + return True + + +class SoftSwitch(SoftSensor): + def __init__(self, value=False, guard=None, effect=None, convert=None): + boolConvert = (lambda value: bool(convert(value))) if convert else bool + SoftSensor.__init__(self, value, guard, effect, boolConvert) + + update = lambda value: {} if self.update(value) else {} + self.__switch = supcon.intf.Implementation(SwitchIntf) + self.__switch.setCallCb('on', lambda: update(True)) + self.__switch.setCallCb('off', lambda: update(False)) + self.__switch.setCallCb('toggle', lambda: update(not self.value)) + + @property + def switch(self): + return self.__switch + + +class PiGPIOSensor(object): + def __init__(self, reactor, gpio): + self.__reactor = reactor + self.__gpio = gpio + + self.__pi = pigpio.pi() + self.__pi.set_mode(self.__gpio, pigpio.INPUT) + self.__pi.set_glitch_filter(self.__gpio, 50000) + self.__pi.set_pull_up_down(self.__gpio, pigpio.PUD_UP) + self.__pi.callback(self.__gpio, pigpio.EITHER_EDGE, self.__callback) + + self.__value = bool(self.__pi.read(self.__gpio)) + + self.__sensor = supcon.intf.Implementation(SensorIntf) + self.__sensor.setCallCb('value', lambda: {'value': self.__value}) + + @property + def sensor(self): + return self.__sensor + + def __callback(self, gpio, level, tick): + if level >= 2: + return + + self.__reactor.callFromThread(self.__update, bool(level)) + + def __update(self, value): + if self.__value == bool(value): + return + + self.__value = bool(value) + self.__sensor.fire('changed', {'value': self.__value}) + + +class PiGPIOSwitch(SoftSwitch): + def __init__(self, gpio, guard=None, effect=None, convert=None): + self.__gpio = gpio + + self.__pi = pigpio.pi() + self.__pi.set_mode(self.__gpio, pigpio.OUTPUT) + + value = self.__pi.read(self.__gpio) + + def writeEffect(value, switch): + self.__pi.write(self.__gpio, int(value)) + if callable(effect): + effect(value, switch) + + super().__init__(value, guard, writeEffect, convert) + + +class NodeSensor(supcon.util.EventEmitter): + + def __init__(self, local: supcon.intf.Node, node: str, path: str): + supcon.util.EventEmitter.__init__(self) + + self.__local = local + self.__node = node + self.__path = path + + self.__value = None + + self.__local.on(self.__local.name, '/', 'supcon.Local', 'intf', self.__onIntf) + self.__local.on(self.__local.name, '/', 'supcon.Local', 'intfLost', self.__onIntfLost) + self.__local.on(self.node, self.path, 'com.screwerk.Sensor', 'changed', self.__onChanged) + + if self.__local.nodes().hasIntf(self.node, self.path, 'com.screwerk.com'): + self.__fetchValue() + + def __del__(self): + self.__local.off(self.__local.name, '/', 'supcon.Local', 'intf', self.__onIntf) + self.__local.off(self.__local.name, '/', 'supcon.Local', 'intfLost', self.__onIntfLost) + self.__local.off(self.node, self.path, 'com.screwerk.Sensor', 'changed', self.__onChanged) + + @property + def node(self): + return self.__node + + @property + def path(self): + return self.__path + + @property + def value(self): + return self.__value + + def __fetchValue(self): + self.__local.call(self.node, self.path, 'com.screwerk.Sensor', 'value', {}).addCallbacks( + lambda args: self.__update(args['value']), + lambda reason: self.__update(None) + ) + + def __update(self, value): + if self.__value == value: + return + + self.__value = value + self.emit('changed', self.__value) + + def __onIntf(self, args, event): + if (args.node == self.node and args.path == self.path and args.intf.name == 'com.screwerk.Sensor'): + self.__fetchValue() + + def __onIntfLost(self, args, event): + if (args.node == self.node and args.path == self.path and args.intf.name == 'com.screwerk.Sensor'): + self.__update(None) + + def __onChanged(self, args, event): + self.__update(args['value']) diff --git a/supcon/util.py b/supcon/util.py new file mode 100644 index 0000000..379f450 --- /dev/null +++ b/supcon/util.py @@ -0,0 +1,175 @@ +# -*- coding: utf-8 -*- + +import abc + +import asyncio +import collections +import collections.abc + +import twisted.internet.defer + +def schedule(obj): + if asyncio.iscoroutine(obj): + obj = twisted.internet.defer.ensureDeferred(obj) + return obj + +class Dumpable(abc.ABC): + """ + The abstract base class `supcon.util.Dumpable`. + + Implementations of this abstract base class can be converted to and from + 'simple' datastructures containing only ints, floats, strings, lists and + dicts. These simple datastructures can be send and received by the supcon + bus. If you only ever use simple datastructures you can ignore this class. + """ + + @abc.abstractmethod + def dump(self): + """ + Converts the `supcon.util.Dumpable` into a 'simple' datastructure. + """ + pass + + @abc.abstractclassmethod + def load(cls, data): + """ + Converts the given 'simple' datastructure **data** into an instance of + **cls**, a subclass of `supcon.util.Dumpable` + """ + pass + + +class Named(abc.ABC): + """ + The abstract base class `supcon.util.Named`. + + Implementations of this abstract base class must have a property + `supcon.util.Named.name`, that can be used as a key in a dictionary. + """ + + @property + @abc.abstractmethod + def name(self) -> str: + """ + The name of the instance. Is useable as a dictionary key. + """ + pass + + +class NamedList(collections.abc.Mapping, Dumpable): + """ + A `supcon.util.NamedList` is an ordered, readonly dictionary of + `supcon.util.Named` and `supcon.util.Dumpable` elements of value type + `supcon.util.NamedList.vtype`. + + The class `supcon.util.NamedList` itself can not be instantiated. It must be + subclassed and the class property `supcon.util.NamedList.vtype` set. The class + property `supcon.util.NamedList.vtype` denotes the type of the elements that + the list can contain. The type `supcon.util.NamedList.vtype` must be a + subclass of `supcon.util.Named` and of `supcon.util.Dumpable`. Elements in the + `supcon.util.NamedList.vtype` can be accessed by the values of their + `supcon.util.Named.name` property. + """ + + vtype = None + """ + Must be assigned in a subclass of `supcon.util.NamedList` to a subclass + of `supcon.util.Named` and `supcon.util.Dumpable` + """ + + def __init__(self, values=None): + """ + Initializes the `supcon.util.NamedList` with the given list of **values**. + + An element of **values** must be of type `supcon.util.NamedList.vtype` or a + 'simple' datastructure that can be converted to an instance of + `supcon.util.NamedList.vtype` by `supcon.util.NamedList.vtype`.load(). + """ + if values is None: + values = [] + if not issubclass(self.vtype, Named): + raise ValueError('the value type must be a subclass of Named') + if not issubclass(self.vtype, Dumpable): + raise ValueError('the value type must be a subclass of Dumpable') + + self.__values = collections.OrderedDict() + + for value in values: + if not isinstance(value, self.vtype): + value = self.vtype.load(value) + if value.name in self.__values: + raise ValueError('value.name must be unique in the given list of values') + self.__values[value.name] = value + + def __getitem__(self, key): + return self.__values[key] + + def __iter__(self): + return iter(self.__values) + + def __len__(self): + return len(self.__values) + + def __repr__(self): + return "{}.{}({})".format( + self.__module__, self.__class__.__name__, + ', '.join([repr(v) for k, v in self.items()]) + ) + + def dump(self): + """ + Converts the `supcon.util.NamedList` into a list of 'simple' datastructures. + """ + return [value.dump() for value in self.values()] + + @classmethod + def load(cls, data): + """ + Converts the given list of 'simple' datastructures **data** into an instance + of **cls**, a subclass of `supcon.util.NamedList`. + """ + return cls(data) + + @classmethod + def to(cls, value): + """ + If the the **value** is an instance of **cls**, a subclass of + `supcon.util.NamedList`, the value is returned. If value is not an instance + of **cls** the method tries to convert the **value** into an instance of + **cls**. + """ + return value if isinstance(value, cls) else cls.load(value) + + +class EventEmitter(object): + + def __init__(self): + self.__callbacks = {} + + def on(self, event, callback): + """ + Registers the given callback for the given event + """ + if type not in self.__callbacks: + self.__callbacks[event] = {} + self.__callbacks[event][id(callback)] = callback + return lambda: self.off(event, callback) + + def off(self, event, callback): + """ + Unregisters the given callback for the given event + """ + if event not in self.__callbacks: + return + if id(callback) not in self.__callbacks[event]: + return + del self.__callbacks[event][id(callback)] + + def emit(self, event, *args): + """ + Calls all callbacks for the given event with the given arguments + """ + if event not in self.__callbacks: + return + for callback in self.__callbacks[event].values(): + schedule(callback(*args)) -- 2.34.1