openpectus 0.1.10__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openpectus-0.1.10/PKG-INFO +35 -0
- openpectus-0.1.10/README.md +6 -0
- openpectus-0.1.10/openpectus/__init__.py +28 -0
- openpectus-0.1.10/openpectus/aggregator/__init__.py +0 -0
- openpectus-0.1.10/openpectus/aggregator/aggregator.py +334 -0
- openpectus-0.1.10/openpectus/aggregator/aggregator_message_handlers.py +128 -0
- openpectus-0.1.10/openpectus/aggregator/aggregator_server.py +69 -0
- openpectus-0.1.10/openpectus/aggregator/alembic.ini +116 -0
- openpectus-0.1.10/openpectus/aggregator/command_examples.py +200 -0
- openpectus-0.1.10/openpectus/aggregator/command_util.py +128 -0
- openpectus-0.1.10/openpectus/aggregator/csv_generator.py +64 -0
- openpectus-0.1.10/openpectus/aggregator/data/__init__.py +0 -0
- openpectus-0.1.10/openpectus/aggregator/data/alembic/README +1 -0
- openpectus-0.1.10/openpectus/aggregator/data/alembic/env.py +92 -0
- openpectus-0.1.10/openpectus/aggregator/data/alembic/script.py.mako +26 -0
- openpectus-0.1.10/openpectus/aggregator/data/alembic/versions/05c4bf88d0bb_add_recentengine.py +42 -0
- openpectus-0.1.10/openpectus/aggregator/data/alembic/versions/5dbc834cf70e_added_aggregator_computer_name_and_.py +39 -0
- openpectus-0.1.10/openpectus/aggregator/data/alembic/versions/646d56d49b76_bootstrap.py +100 -0
- openpectus-0.1.10/openpectus/aggregator/data/alembic/versions/72449425de7c_change_computer_name_to_engine_computer_.py +30 -0
- openpectus-0.1.10/openpectus/aggregator/data/alembic/versions/774e4df9ce40_add_uod_fields.py +38 -0
- openpectus-0.1.10/openpectus/aggregator/data/alembic/versions/95bfa6af85ea_add_engine_hardware_str_to_recentrun_.py +35 -0
- openpectus-0.1.10/openpectus/aggregator/data/alembic/versions/b792689d4cd4_change_errorlog_to_aggregatederrorlog.py +30 -0
- openpectus-0.1.10/openpectus/aggregator/data/alembic/versions/bcda1e0dd461_removed_processunits_table.py +38 -0
- openpectus-0.1.10/openpectus/aggregator/data/alembic/versions/d517729a9be0_added_errorlog_to_recentrun.py +35 -0
- openpectus-0.1.10/openpectus/aggregator/data/alembic/versions/d53dfbe96f00_introduce_required_roles.py +39 -0
- openpectus-0.1.10/openpectus/aggregator/data/alembic/versions/ec559277c8da_add_engine_version_to_recent_run.py +35 -0
- openpectus-0.1.10/openpectus/aggregator/data/database.py +95 -0
- openpectus-0.1.10/openpectus/aggregator/data/models.py +134 -0
- openpectus-0.1.10/openpectus/aggregator/data/repository.py +228 -0
- openpectus-0.1.10/openpectus/aggregator/deps.py +21 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/.gitkeep +0 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/01cd0f50f18fa8b0.json +1 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/063fe22fc098362e.json +1 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/11.ff0cfa45c1a3cb96.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/1231.5ca29ab7188040de.js +7 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/1391.a9e0ec7083e3c96f.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/1463.9f3526e6c4351022.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/1487.e230c3d3a0ec6b7d.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/1527.3f4c8f28025f0331.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/1559.d9998019e80ec328.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/1565.0791799366c24c84.js +12 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/163.9f09f9be5a049a57.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/1767.0c8c5fca4bc73254.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/1779.6f887eea291f87ab.js +7 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/18d828edb3a5432b.json +1 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/1927.f8704e501e824236.js +7 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/2121.e134891bd29f76b9.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/2183.370bc0b6c35657fc.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/2218.6d01c0073475c9c5.js +600 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/2293.c7e5905056387edc.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/2407.44e6f7acb83a2eac.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/2545.18e26589920529b5.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/2579.888b55420f408742.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/25dd2e6f4f6bb57b.wasm +0 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/263.23947fbff77164ee.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/26f02aa1eb5d7e87.svg +3 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/2759.20f77866e8c2d5d9.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/2775.e192cb83d986f240.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/2947.dcc2586fa910d7d3.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/2f68dc993ac33358.json +1 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/3047.c03d64a899d6d1c0.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/3095.ec3f2351eb9cc4c3.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/3163.a8dda7a07ebe01eb.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/3175e9ac30563f77.json +1 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/3359.34afef50c40263ec.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/342848b6abc793c0.json +1 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/345.f2fa272ceca6b486.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/3700.0d5ef582c4a0bbb5.js +7 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/3787.ebed2f9cd0a53262.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/3db9e4085e6f7566.svg +3 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/3e5cab7c367952da.json +1 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/4035.2250ff985d6fb19e.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/4113.1a2e5bf0e3c8f4fe.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/4211.21bb71d03e50ebe9.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/4231.c6d37184dfcfc7ed.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/4303.a70c30160f8e0b87.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/4334.11181c2144dc26d3.js +7 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/4359.64320bb966a5bba1.js +7 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/4405.6e123938e5743dcb.js +7 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/4519.b7c155caf62e2141.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/4719.f7c6d5372bdab6ba.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/4727.ad166e4014a5f90f.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/4779.9398374a40adab75.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/4823.eb0d238db4ce5703.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/487.fa773d0172005f9e.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/4951.df9cf6f7c9f572c9.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/4f04ecbf20c9c20c.json +1 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/50e2d8f6067cd824.json +1 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/515.ba6ff850cba2c044.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/5166.ee5112abc0dc7185.js +7 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/5271.09a305ebd8a3305e.js +7 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/5372.4e26060b1137e889.js +7 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/5383.e6d32f4215eee44d.js +7 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/5447.40ee42ef92db61cb.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/5457.d0f2f07f4120b6e8.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/5591.5569fffbbc79b367.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/57552f2ebe0f0696.json +1 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/5861.7cd66943d357412f.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/5967.84883a8372a67aa0.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/59a2f126e3e45d27.svg +3 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/5c73f8ba5f60f3a2.json +1 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/5f84911a0a3564a6.svg +3 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/6039.aab73d4b4a4e8959.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/6081.7865f0e834a5075a.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/6085.bf0780646692e0dd.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/6217.a0bed12bb62404d6.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/6311.c6675bc2be7fcd64.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/6403.adf0a6806326e7fd.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/6423.95b1c82cc075d7c6.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/67f329d51ecf4c74.svg +5 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/6855.25addbbab63a8c7e.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/6887.0b935367b73e6c79.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/6969.ff37f0e9e6ad6902.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/7111.cbe1ae374f5b8e1f.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/7191.f1e744cb8e86f2a7.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/7333.ffd2adc5049b8378.js +7 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/7623.92b9c7f80d2c0441.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/7655.861b067d51f9c2c7.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/7864.13a2f5429526d31f.js +1 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/7927.35b5d86c44f72f47.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/8271.f7824233fd3ed6b2.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/8471.2aaea08c54289ca7.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/849.671d268c3ee3a2de.js +7 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/8503.f4337b6576285775.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/8631.0d334387d9db7154.js +11 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/8759.a80d2a8ee7ba62e2.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/881c595b719cb1a5.json +1 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/8881.f738bb87048a077b.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/8899.f5e53397a335cfbb.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/8917.5679ad3f0df455c3.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/923.060ab05056e54cff.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/9559.c8082321e028bdfa.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/9636795e44a0db5a.json +1 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/9645.5b56a2344bfbd10e.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/9686.d9e5a398477474d4.js +1 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/9831.5ebfd462178e7753.js +8 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/9867.5885e41addd90142.js +7 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/9fb5f5764acec207.json +1 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/assets/fonts/OpenSans-Italic-VariableFont.ttf +0 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/assets/fonts/OpenSans-VariableFont.ttf +0 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/assets/images/Diagram 1-A.svg +1 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/assets/images/Diagram 1-B.svg +1 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/assets/images/Diagram 1.svg +1 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/assets/images/Diagram 2.svg +1 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/assets/monaco-editor-workers/index.d.ts +2 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/assets/monaco-editor-workers/index.d.ts.map +1 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/assets/monaco-editor-workers/index.js +50 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/assets/monaco-editor-workers/index.js.map +1 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/assets/monaco-editor-workers/workers/cssWorker-es.js +38929 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/assets/monaco-editor-workers/workers/cssWorker-iife.js +84 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/assets/monaco-editor-workers/workers/editorWorker-es.js +7352 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/assets/monaco-editor-workers/workers/editorWorker-iife.js +11 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/assets/monaco-editor-workers/workers/htmlWorker-es.js +20557 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/assets/monaco-editor-workers/workers/htmlWorker-iife.js +458 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/assets/monaco-editor-workers/workers/jsonWorker-es.js +12369 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/assets/monaco-editor-workers/workers/jsonWorker-iife.js +42 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/assets/monaco-editor-workers/workers/tsWorker-es.js +145755 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/assets/monaco-editor-workers/workers/tsWorker-iife.js +37021 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/b53687f2b68f09f1.json +1 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/c41032800378b064.html +123 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/c4986cd9e95c7fe9.json +1 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/ca6ab87326ed6420.svg +5 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/codicon.81ea7998906c6b7c.ttf +0 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/codicon.ttf +0 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/common.789d1d67a7c72cf1.js +7 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/e2b596c3b5c310df.svg +4 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/eade5345c958f59a.svg +4 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/eaf474c3aab5a73c.json +1 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/ebce7b0b44cad8d7.svg +5 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/eca1c8feaf9235e8.svg +5 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/ef8dfbb9d11844c8.json +1 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/f079808011976925.js +1 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/f1c9615d89775192.ttf +0 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/favicon.ico +0 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/index.html +13 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/main.2d961af7e87c9467.js +77 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/mockServiceWorker.js +284 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/runtime.603128b4d98381ce.js +1 -0
- openpectus-0.1.10/openpectus/aggregator/frontend-dist/styles.40cfc284e358e52a.css +1 -0
- openpectus-0.1.10/openpectus/aggregator/frontend_publisher.py +62 -0
- openpectus-0.1.10/openpectus/aggregator/generate_openapi_spec_and_typescript_interfaces.py +35 -0
- openpectus-0.1.10/openpectus/aggregator/main.py +47 -0
- openpectus-0.1.10/openpectus/aggregator/models.py +173 -0
- openpectus-0.1.10/openpectus/aggregator/routers/__init__.py +0 -0
- openpectus-0.1.10/openpectus/aggregator/routers/auth.py +92 -0
- openpectus-0.1.10/openpectus/aggregator/routers/dto.py +400 -0
- openpectus-0.1.10/openpectus/aggregator/routers/process_unit.py +302 -0
- openpectus-0.1.10/openpectus/aggregator/routers/recent_runs.py +133 -0
- openpectus-0.1.10/openpectus/aggregator/routers/serialization.py +58 -0
- openpectus-0.1.10/openpectus/aggregator/routers/version.py +18 -0
- openpectus-0.1.10/openpectus/aggregator/spa.py +34 -0
- openpectus-0.1.10/openpectus/build.json +1 -0
- openpectus-0.1.10/openpectus/c4-documentation.dsl +79 -0
- openpectus-0.1.10/openpectus/dist/openpectus-0.1.10.tar.gz +0 -0
- openpectus-0.1.10/openpectus/engine/__init__.py +0 -0
- openpectus-0.1.10/openpectus/engine/archiver.py +151 -0
- openpectus-0.1.10/openpectus/engine/commands.py +73 -0
- openpectus-0.1.10/openpectus/engine/composite_hardware.py +99 -0
- openpectus-0.1.10/openpectus/engine/configuration/demo_uod.py +190 -0
- openpectus-0.1.10/openpectus/engine/configuration/opcua_test.py +91 -0
- openpectus-0.1.10/openpectus/engine/engine.py +826 -0
- openpectus-0.1.10/openpectus/engine/engine_message_builder.py +150 -0
- openpectus-0.1.10/openpectus/engine/engine_message_handlers.py +80 -0
- openpectus-0.1.10/openpectus/engine/engine_runner.py +381 -0
- openpectus-0.1.10/openpectus/engine/hardware.py +203 -0
- openpectus-0.1.10/openpectus/engine/hardware_recovery.py +382 -0
- openpectus-0.1.10/openpectus/engine/internal_commands.py +125 -0
- openpectus-0.1.10/openpectus/engine/internal_commands_impl.py +320 -0
- openpectus-0.1.10/openpectus/engine/labjack/LICENSE +81 -0
- openpectus-0.1.10/openpectus/engine/labjack/LabJackM.dll +0 -0
- openpectus-0.1.10/openpectus/engine/labjack/LabJackWUSB.dll +0 -0
- openpectus-0.1.10/openpectus/engine/labjack/libLabJackM.so +0 -0
- openpectus-0.1.10/openpectus/engine/labjack/ljm_constants.json +7509 -0
- openpectus-0.1.10/openpectus/engine/labjack_hardware.py +233 -0
- openpectus-0.1.10/openpectus/engine/main.py +308 -0
- openpectus-0.1.10/openpectus/engine/method_model.py +88 -0
- openpectus-0.1.10/openpectus/engine/models.py +47 -0
- openpectus-0.1.10/openpectus/engine/opcua_hardware.py +341 -0
- openpectus-0.1.10/openpectus/engine/uod_builder_api.py +20 -0
- openpectus-0.1.10/openpectus/lang/__init__.py +4 -0
- openpectus-0.1.10/openpectus/lang/exec/__init__.py +0 -0
- openpectus-0.1.10/openpectus/lang/exec/analyzer.py +421 -0
- openpectus-0.1.10/openpectus/lang/exec/base_unit.py +20 -0
- openpectus-0.1.10/openpectus/lang/exec/clock.py +12 -0
- openpectus-0.1.10/openpectus/lang/exec/commands.py +140 -0
- openpectus-0.1.10/openpectus/lang/exec/errors.py +75 -0
- openpectus-0.1.10/openpectus/lang/exec/pinterpreter.py +709 -0
- openpectus-0.1.10/openpectus/lang/exec/readings.py +233 -0
- openpectus-0.1.10/openpectus/lang/exec/runlog.py +539 -0
- openpectus-0.1.10/openpectus/lang/exec/tag_lifetime.py +130 -0
- openpectus-0.1.10/openpectus/lang/exec/tags.py +354 -0
- openpectus-0.1.10/openpectus/lang/exec/tags_impl.py +152 -0
- openpectus-0.1.10/openpectus/lang/exec/timer.py +87 -0
- openpectus-0.1.10/openpectus/lang/exec/units.py +269 -0
- openpectus-0.1.10/openpectus/lang/exec/uod.py +938 -0
- openpectus-0.1.10/openpectus/lang/grammar/__init__.py +0 -0
- openpectus-0.1.10/openpectus/lang/grammar/codegen/__init__.py +0 -0
- openpectus-0.1.10/openpectus/lang/grammar/codegen/pcode.tokens +46 -0
- openpectus-0.1.10/openpectus/lang/grammar/codegen/pcodeLexer.py +168 -0
- openpectus-0.1.10/openpectus/lang/grammar/codegen/pcodeLexer.tokens +46 -0
- openpectus-0.1.10/openpectus/lang/grammar/codegen/pcodeListener.py +318 -0
- openpectus-0.1.10/openpectus/lang/grammar/codegen/pcodeParser.py +2689 -0
- openpectus-0.1.10/openpectus/lang/grammar/pcode.g4 +113 -0
- openpectus-0.1.10/openpectus/lang/grammar/pgrammar.py +56 -0
- openpectus-0.1.10/openpectus/lang/grammar/pprogrambuilder.py +292 -0
- openpectus-0.1.10/openpectus/lang/grammar/pprogramformatter.py +59 -0
- openpectus-0.1.10/openpectus/lang/model/__init__.py +0 -0
- openpectus-0.1.10/openpectus/lang/model/pprogram.py +552 -0
- openpectus-0.1.10/openpectus/protocol/__init__.py +0 -0
- openpectus-0.1.10/openpectus/protocol/aggregator_dispatcher.py +246 -0
- openpectus-0.1.10/openpectus/protocol/aggregator_messages.py +48 -0
- openpectus-0.1.10/openpectus/protocol/dispatch_interface.py +16 -0
- openpectus-0.1.10/openpectus/protocol/engine_dispatcher.py +264 -0
- openpectus-0.1.10/openpectus/protocol/engine_messages.py +124 -0
- openpectus-0.1.10/openpectus/protocol/exceptions.py +16 -0
- openpectus-0.1.10/openpectus/protocol/messages.py +43 -0
- openpectus-0.1.10/openpectus/protocol/models.py +170 -0
- openpectus-0.1.10/openpectus/protocol/serialization.py +39 -0
- openpectus-0.1.10/openpectus/sentry.py +126 -0
- openpectus-0.1.10/pyproject.toml +88 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: openpectus
|
|
3
|
+
Version: 0.1.10
|
|
4
|
+
Summary: Pectus is a process control system for Unit Operations such as filtration, chromatography, precipitation,
|
|
5
|
+
Author-email: Eskild Schroll-Fleischer <eyfl@novonordisk.com>, Morten Passow Odgaard <mpo@mjolner.dk>, "Jan H. Knudsen" <jhk@mjolner.dk>
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: antlr4-python3-runtime==4.13.1
|
|
8
|
+
Requires-Dist: pint==0.22
|
|
9
|
+
Requires-Dist: typing_extensions==4.8.0
|
|
10
|
+
Requires-Dist: pydantic==1.10.13
|
|
11
|
+
Requires-Dist: fastapi==0.103.2
|
|
12
|
+
Requires-Dist: uvicorn[standard]==0.23.2
|
|
13
|
+
Requires-Dist: httpx==0.25.0
|
|
14
|
+
Requires-Dist: fastapi_websocket_rpc==0.1.25
|
|
15
|
+
Requires-Dist: fastapi_websocket_pubsub==0.3.8
|
|
16
|
+
Requires-Dist: colorlog==6.7.0
|
|
17
|
+
Requires-Dist: sqlalchemy==2.0.23
|
|
18
|
+
Requires-Dist: asyncua==1.0.6
|
|
19
|
+
Requires-Dist: labjack-ljm==1.23.0
|
|
20
|
+
Requires-Dist: pyright==1.1.381
|
|
21
|
+
Requires-Dist: alembic==1.13.1
|
|
22
|
+
Requires-Dist: sentry-sdk==2.1.1
|
|
23
|
+
Requires-Dist: PyJWT==2.10.0
|
|
24
|
+
Requires-Dist: flake8==6.1.0 ; extra == "development"
|
|
25
|
+
Requires-Dist: black==23.3.0 ; extra == "development"
|
|
26
|
+
Requires-Dist: build==0.10.0 ; extra == "development"
|
|
27
|
+
Provides-Extra: development
|
|
28
|
+
|
|
29
|
+
# Open Pectus
|
|
30
|
+
Pectus is a process control system for Unit Operations such as filtration, chromatography, precipitation, solubilization and refolding. Pectus implements a language called P-code which is used to write methods and control components on the Unit Operation.
|
|
31
|
+
|
|
32
|
+
General documentation about case, architecture and P-Code is available at [Read the Docs](https://open-pectus.readthedocs.io/en/latest/).
|
|
33
|
+
|
|
34
|
+
Documentation for developers is available in the [DEVELOPMENT.md](DEVELOPMENT.md) file.
|
|
35
|
+
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Open Pectus
|
|
2
|
+
Pectus is a process control system for Unit Operations such as filtration, chromatography, precipitation, solubilization and refolding. Pectus implements a language called P-code which is used to write methods and control components on the Unit Operation.
|
|
3
|
+
|
|
4
|
+
General documentation about case, architecture and P-Code is available at [Read the Docs](https://open-pectus.readthedocs.io/en/latest/).
|
|
5
|
+
|
|
6
|
+
Documentation for developers is available in the [DEVELOPMENT.md](DEVELOPMENT.md) file.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
""" Pectus is a process control system for Unit Operations such as filtration, chromatography, precipitation,
|
|
2
|
+
solubilization and refolding. Pectus implements a language called P-code which is used to write methods and
|
|
3
|
+
control components on the Unit Operation. """
|
|
4
|
+
__version__ = "0.1.10"
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
import colorlog
|
|
11
|
+
|
|
12
|
+
build_number = "build_number not set"
|
|
13
|
+
build_info = {}
|
|
14
|
+
|
|
15
|
+
path = os.path.dirname(os.path.realpath(__file__))
|
|
16
|
+
with open(os.path.join(path, "build.json")) as file:
|
|
17
|
+
dct = json.load(file)
|
|
18
|
+
build_info = dct
|
|
19
|
+
build_number = dct["build_number"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def log_setup_colorlog(root_loglevel: int = logging.INFO):
|
|
23
|
+
handler = colorlog.StreamHandler()
|
|
24
|
+
handler.setFormatter(colorlog.ColoredFormatter('%(log_color)s%(levelname)s:%(name)s:%(message)s'))
|
|
25
|
+
|
|
26
|
+
logging.root.setLevel(root_loglevel)
|
|
27
|
+
logging.root.handlers.clear()
|
|
28
|
+
logging.root.addHandler(handler)
|
|
File without changes
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
|
|
5
|
+
import openpectus.aggregator.models as Mdl
|
|
6
|
+
from openpectus.protocol.aggregator_dispatcher import AggregatorDispatcher
|
|
7
|
+
import openpectus.protocol.aggregator_messages as AM
|
|
8
|
+
import openpectus.protocol.engine_messages as EM
|
|
9
|
+
import openpectus.protocol.messages as M
|
|
10
|
+
from openpectus.aggregator.data import database
|
|
11
|
+
from openpectus.aggregator.data.repository import RecentRunRepository, PlotLogRepository, RecentEngineRepository
|
|
12
|
+
from openpectus.aggregator.frontend_publisher import FrontendPublisher
|
|
13
|
+
from openpectus.aggregator.models import EngineData
|
|
14
|
+
from openpectus.protocol.models import SystemTagName, MethodStatusEnum
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
EngineDataMap = dict[str, EngineData]
|
|
19
|
+
persistance_threshold_seconds = 5
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FromEngine:
|
|
23
|
+
def __init__(self, engine_data_map: EngineDataMap, publisher: FrontendPublisher):
|
|
24
|
+
self._engine_data_map = engine_data_map
|
|
25
|
+
self.publisher = publisher
|
|
26
|
+
|
|
27
|
+
def register_engine_data(self, engine_data: EngineData):
|
|
28
|
+
logger.debug(f"Data for engine {engine_data.engine_id} registered")
|
|
29
|
+
self._engine_data_map[engine_data.engine_id] = engine_data
|
|
30
|
+
asyncio.create_task(self.publisher.publish_process_units_changed())
|
|
31
|
+
asyncio.create_task(self.publisher.publish_control_state_changed(engine_data.engine_id))
|
|
32
|
+
|
|
33
|
+
def engine_connected(self, engine_id: str):
|
|
34
|
+
logger.debug("engine_connected")
|
|
35
|
+
|
|
36
|
+
def engine_disconnected(self, engine_id: str):
|
|
37
|
+
logger.debug("engine_disconnected")
|
|
38
|
+
engine_data = self._engine_data_map.get(engine_id)
|
|
39
|
+
if engine_data is not None:
|
|
40
|
+
with database.create_scope():
|
|
41
|
+
repo = RecentEngineRepository(database.scoped_session())
|
|
42
|
+
repo.store_recent_engine(engine_data)
|
|
43
|
+
logger.info("Recent engine saved")
|
|
44
|
+
del self._engine_data_map[engine_id]
|
|
45
|
+
asyncio.create_task(self.publisher.publish_process_units_changed())
|
|
46
|
+
else:
|
|
47
|
+
logger.warning("No data to save for engine " + engine_id)
|
|
48
|
+
|
|
49
|
+
def engine_reconnected(self, msg: EM.ReconnectedMsg):
|
|
50
|
+
logger.info(f"Engine_reconnected. Processing ReconnectedMsg {msg.ident}")
|
|
51
|
+
engine_id = msg.engine_id
|
|
52
|
+
engine_data = self._engine_data_map.get(engine_id)
|
|
53
|
+
if engine_data is None:
|
|
54
|
+
logger.error("No engine data available on reconnect for engine " + engine_id)
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
# Use this to debug reconnect msg timestamps/older tags
|
|
58
|
+
# created_time = datetime.fromtimestamp(msg.created_tick).strftime("%H:%M:%S")
|
|
59
|
+
# logger.debug(f"ReconnectedMsg created_tick_time: {created_time})")
|
|
60
|
+
|
|
61
|
+
# ft02 = next((tag for tag in msg.tags if tag.name == "FT02"))
|
|
62
|
+
# ft02_time = datetime.fromtimestamp(ft02.tick_time).strftime("%H:%M:%S")
|
|
63
|
+
# logger.debug(f"ReconnectedMsg ft02_time: {ft02_time})")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# apply the state from msg to the current state
|
|
67
|
+
engine_data.method = msg.method
|
|
68
|
+
run_id_tag = next((tag for tag in msg.tags if tag.name == SystemTagName.RUN_ID), None)
|
|
69
|
+
# verify consistent message
|
|
70
|
+
if msg.run_id is None:
|
|
71
|
+
if run_id_tag is not None and run_id_tag.value is not None:
|
|
72
|
+
logger.error(f"Mismatch in ReconnectedMsg, tag run_id {run_id_tag.value}, msg run_id {msg.run_id}")
|
|
73
|
+
return
|
|
74
|
+
else:
|
|
75
|
+
if run_id_tag is None or run_id_tag.value is None:
|
|
76
|
+
logger.error(f"Mismatch in ReconnectedMsg, tag run_id {None}, msg run_id {msg.run_id}")
|
|
77
|
+
return
|
|
78
|
+
engine_data.run_data.run_started = datetime.fromtimestamp(msg.run_started_tick) \
|
|
79
|
+
if msg.run_started_tick is not None else None
|
|
80
|
+
engine_data.tags_info.upsert(run_id_tag) # sets engine_data.run_id
|
|
81
|
+
|
|
82
|
+
# handle possible change to recent engine
|
|
83
|
+
with database.create_scope():
|
|
84
|
+
repo = RecentEngineRepository(database.scoped_session())
|
|
85
|
+
recent_engine = repo.get_recent_engine_by_engine_id(engine_id)
|
|
86
|
+
if recent_engine is None:
|
|
87
|
+
logger.info(f"Reconnected engine {engine_id} has no recent engine data")
|
|
88
|
+
else:
|
|
89
|
+
if recent_engine.run_id != msg.run_id and recent_engine.run_id is not None:
|
|
90
|
+
logger.info("Marking recent engine stopped ")
|
|
91
|
+
repo.mark_recent_engine_stopped(engine_data)
|
|
92
|
+
|
|
93
|
+
# process tag values normally
|
|
94
|
+
self.tag_values_changed(engine_id, msg.tags)
|
|
95
|
+
logger.info(f"Done processing ReconnectedMsg {msg.ident}")
|
|
96
|
+
|
|
97
|
+
def run_started(self): # to replace run_id_changed guessing
|
|
98
|
+
raise NotImplementedError()
|
|
99
|
+
|
|
100
|
+
def run_stopped(self):
|
|
101
|
+
raise NotImplementedError()
|
|
102
|
+
|
|
103
|
+
def uod_info_changed(
|
|
104
|
+
self,
|
|
105
|
+
engine_id: str,
|
|
106
|
+
readings: list[Mdl.ReadingInfo],
|
|
107
|
+
commands: list[Mdl.CommandInfo],
|
|
108
|
+
plot_configuration: Mdl.PlotConfiguration,
|
|
109
|
+
hardware_str: str,
|
|
110
|
+
required_roles: set[str]):
|
|
111
|
+
try:
|
|
112
|
+
self._engine_data_map[engine_id].readings = readings
|
|
113
|
+
self._engine_data_map[engine_id].commands = commands
|
|
114
|
+
self._engine_data_map[engine_id].plot_configuration = plot_configuration
|
|
115
|
+
self._engine_data_map[engine_id].hardware_str = hardware_str
|
|
116
|
+
self._engine_data_map[engine_id].required_roles = required_roles
|
|
117
|
+
except KeyError:
|
|
118
|
+
logger.error(f'No engine registered under id {engine_id} when trying to set uod info.')
|
|
119
|
+
|
|
120
|
+
def tag_values_changed(self, engine_id: str, changed_tag_values: list[Mdl.TagValue]):
|
|
121
|
+
with database.create_scope():
|
|
122
|
+
plot_log_repo = PlotLogRepository(database.scoped_session())
|
|
123
|
+
recent_run_repo = RecentRunRepository(database.scoped_session())
|
|
124
|
+
|
|
125
|
+
engine_data = self._engine_data_map.get(engine_id)
|
|
126
|
+
if engine_data is None:
|
|
127
|
+
logger.error(f'No engine registered under id {engine_id} when trying to upsert tag values.')
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
for changed_tag_value in changed_tag_values:
|
|
131
|
+
if changed_tag_value.name == SystemTagName.METHOD_STATUS.value:
|
|
132
|
+
if changed_tag_value.value == MethodStatusEnum.ERROR:
|
|
133
|
+
engine_data.run_data.interrupted_by_error = True
|
|
134
|
+
else:
|
|
135
|
+
engine_data.run_data.interrupted_by_error = False
|
|
136
|
+
|
|
137
|
+
if changed_tag_value.name == SystemTagName.RUN_ID.value:
|
|
138
|
+
self._run_id_changed(plot_log_repo, recent_run_repo, engine_data, changed_tag_value)
|
|
139
|
+
|
|
140
|
+
was_inserted = engine_data.tags_info.upsert(changed_tag_value)
|
|
141
|
+
|
|
142
|
+
if changed_tag_value.name in [
|
|
143
|
+
SystemTagName.METHOD_STATUS.value,
|
|
144
|
+
SystemTagName.SYSTEM_STATE.value,
|
|
145
|
+
SystemTagName.RUN_ID.value,
|
|
146
|
+
]:
|
|
147
|
+
asyncio.create_task(self.publisher.publish_process_units_changed())
|
|
148
|
+
|
|
149
|
+
# if a tag doesn't appear with value until after start and run_id, we need to store the info here
|
|
150
|
+
if was_inserted and engine_data.run_id is not None:
|
|
151
|
+
plot_log_repo.store_new_tag_info(engine_data.engine_id, engine_data.run_id, changed_tag_value)
|
|
152
|
+
|
|
153
|
+
self._persist_tag_values(engine_data, plot_log_repo)
|
|
154
|
+
|
|
155
|
+
def _run_id_changed(
|
|
156
|
+
self,
|
|
157
|
+
plot_log_repo: PlotLogRepository,
|
|
158
|
+
recent_run_repo: RecentRunRepository,
|
|
159
|
+
engine_data: EngineData,
|
|
160
|
+
run_id_tag: Mdl.TagValue):
|
|
161
|
+
""" Handles persistance related to start and end of a run """
|
|
162
|
+
|
|
163
|
+
logger.info(f"RunId changed from {engine_data.run_id} to {run_id_tag.value}, Engine: {engine_data.engine_id}")
|
|
164
|
+
if run_id_tag.value is None and engine_data.run_id is None:
|
|
165
|
+
logger.info("Engine has no active run")
|
|
166
|
+
elif run_id_tag.value is None and engine_data.run_id is not None:
|
|
167
|
+
# Run stopped
|
|
168
|
+
logger.info(f"Run was stopped. Saving Recent Run. Engine: {engine_data.engine_id}")
|
|
169
|
+
recent_run_repo.store_recent_run(engine_data)
|
|
170
|
+
engine_data.run_data.error_log = Mdl.AggregatedErrorLog.empty()
|
|
171
|
+
asyncio.create_task(self.publisher.publish_error_log_changed(engine_data.engine_id))
|
|
172
|
+
elif run_id_tag.value is not None and engine_data.run_id is not None:
|
|
173
|
+
# Ongoing run. run_id was only detected as changed because of complete tags set,
|
|
174
|
+
# caused by e.g. an aggregator restart
|
|
175
|
+
if run_id_tag.value != engine_data.run_id:
|
|
176
|
+
logger.error(f"Run_Id inconsistent, run_id tag {run_id_tag.value}, engine_data {engine_data.run_id}")
|
|
177
|
+
else:
|
|
178
|
+
logger.info(f"Run {run_id_tag.value} resumed")
|
|
179
|
+
else:
|
|
180
|
+
# Run started
|
|
181
|
+
logger.info(f"A new Run was started, Engine: {engine_data.engine_id}, Run_Id: {str(run_id_tag.value)}")
|
|
182
|
+
engine_data.run_data.run_started = datetime.fromtimestamp(run_id_tag.tick_time, timezone.utc)
|
|
183
|
+
plot_log_repo.create_plot_log(engine_data, str(run_id_tag.value))
|
|
184
|
+
|
|
185
|
+
def _persist_tag_values(self, engine_data: EngineData, plot_log_repo: PlotLogRepository):
|
|
186
|
+
latest_persisted_tick_time = engine_data.run_data.latest_persisted_tick_time
|
|
187
|
+
tag_values = engine_data.tags_info.map.values()
|
|
188
|
+
latest_tag_tick_time = max([tag.tick_time for tag in tag_values]) if len(tag_values) > 0 else 0
|
|
189
|
+
time_threshold_exceeded = latest_persisted_tick_time is None \
|
|
190
|
+
or latest_tag_tick_time - latest_persisted_tick_time > persistance_threshold_seconds
|
|
191
|
+
# time_threshold_exceeded = latest_persisted_tick_time is None \
|
|
192
|
+
# or time.time() - latest_persisted_tick_time > persistance_threshold_seconds
|
|
193
|
+
if engine_data.run_id is not None and time_threshold_exceeded:
|
|
194
|
+
tag_values_to_persist = [tag_value.copy() for tag_value in engine_data.tags_info.map.values()
|
|
195
|
+
if latest_persisted_tick_time is None
|
|
196
|
+
or tag_value.tick_time > latest_persisted_tick_time]
|
|
197
|
+
"""
|
|
198
|
+
We manipulate the tick_time of the tagValues we persist. But it's not changing it to something that didn't
|
|
199
|
+
exist in the engine, because we change the tick_time to a tick_time that comes from an actual later reading
|
|
200
|
+
where those tagValues we manipulate was read and simply had not changed value since the value we have been
|
|
201
|
+
reported.
|
|
202
|
+
It's difficult to explain, but after this manipulation, the tagValues will still match an actual tagValue
|
|
203
|
+
read in the engine, just not one reported.
|
|
204
|
+
We do this because it solves a lot of issues when we later try to match values based on the tick_time.
|
|
205
|
+
"""
|
|
206
|
+
highest_tick_time_to_persist = max([tag_Value.tick_time for tag_Value in tag_values_to_persist])
|
|
207
|
+
for tag_value_to_persist in tag_values_to_persist:
|
|
208
|
+
tag_value_to_persist.tick_time = highest_tick_time_to_persist
|
|
209
|
+
|
|
210
|
+
# Note: to store tag values, the run_id is needed
|
|
211
|
+
plot_log_repo.store_tag_values(engine_data.engine_id, engine_data.run_id, tag_values_to_persist)
|
|
212
|
+
engine_data.run_data.latest_persisted_tick_time = highest_tick_time_to_persist
|
|
213
|
+
engine_data.run_data.latest_tag_time = highest_tick_time_to_persist
|
|
214
|
+
|
|
215
|
+
def runlog_changed(self, engine_id: str, runlog: Mdl.RunLog):
|
|
216
|
+
try:
|
|
217
|
+
engine_data = self._engine_data_map[engine_id]
|
|
218
|
+
if engine_data.run_data.runlog != runlog:
|
|
219
|
+
engine_data.run_data.runlog = runlog
|
|
220
|
+
asyncio.create_task(self.publisher.publish_run_log_changed(engine_id))
|
|
221
|
+
except KeyError:
|
|
222
|
+
logger.error(f'No engine registered under id {engine_id} when trying to set run log.')
|
|
223
|
+
|
|
224
|
+
def control_state_changed(self, engine_id: str, control_state: Mdl.ControlState):
|
|
225
|
+
try:
|
|
226
|
+
engine_data = self._engine_data_map[engine_id]
|
|
227
|
+
if engine_data.control_state != control_state:
|
|
228
|
+
engine_data.control_state = control_state
|
|
229
|
+
asyncio.create_task(self.publisher.publish_control_state_changed(engine_id))
|
|
230
|
+
except KeyError:
|
|
231
|
+
logger.error(f'No engine registered under id {engine_id} when trying to set control state.')
|
|
232
|
+
|
|
233
|
+
def method_state_changed(self, engine_id: str, method_state: Mdl.MethodState):
|
|
234
|
+
try:
|
|
235
|
+
engine_data = self._engine_data_map[engine_id]
|
|
236
|
+
if engine_data.run_data.method_state != method_state:
|
|
237
|
+
engine_data.run_data.method_state = method_state
|
|
238
|
+
asyncio.create_task(self.publisher.publish_method_state_changed(engine_id))
|
|
239
|
+
except KeyError:
|
|
240
|
+
logger.error(f'No engine registered under id {engine_id} when trying to set control state.')
|
|
241
|
+
|
|
242
|
+
def error_log_changed(self, engine_id: str, error_log: Mdl.ErrorLog):
|
|
243
|
+
try:
|
|
244
|
+
engine_data = self._engine_data_map[engine_id]
|
|
245
|
+
engine_data.run_data.error_log.aggregate_with(error_log)
|
|
246
|
+
asyncio.create_task(self.publisher.publish_error_log_changed(engine_id))
|
|
247
|
+
except KeyError:
|
|
248
|
+
logger.error(f'No engine registered under id {engine_id} when trying to set error log.')
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class FromFrontend:
|
|
252
|
+
def __init__(self, engine_data_map: EngineDataMap, dispatcher: AggregatorDispatcher):
|
|
253
|
+
self._engine_data_map = engine_data_map
|
|
254
|
+
self.dispatcher = dispatcher
|
|
255
|
+
|
|
256
|
+
async def method_saved(self, engine_id: str, method: Mdl.Method, user_name: str) -> bool:
|
|
257
|
+
try:
|
|
258
|
+
response = await self.dispatcher.rpc_call(engine_id, message=AM.MethodMsg(method=method))
|
|
259
|
+
if isinstance(response, M.ErrorMessage):
|
|
260
|
+
logger.error(f"Failed to set method. Engine response: {response.message}")
|
|
261
|
+
return False
|
|
262
|
+
except Exception:
|
|
263
|
+
logger.error("Failed to set method", exc_info=True)
|
|
264
|
+
return False
|
|
265
|
+
|
|
266
|
+
# update local method state
|
|
267
|
+
engine_data = self._engine_data_map.get(engine_id)
|
|
268
|
+
if engine_data is not None:
|
|
269
|
+
engine_data.method = method
|
|
270
|
+
engine_data.contributors.add(user_name)
|
|
271
|
+
return True
|
|
272
|
+
|
|
273
|
+
async def request_cancel(self, engine_id, line_id: str, user_name: str) -> bool:
|
|
274
|
+
engine_data = self._engine_data_map.get(engine_id)
|
|
275
|
+
if engine_data is None:
|
|
276
|
+
logger.warning(f"Cannot request cancel, engine {engine_id} not found")
|
|
277
|
+
return False
|
|
278
|
+
try:
|
|
279
|
+
response = await self.dispatcher.rpc_call(engine_id, message=AM.CancelMsg(exec_id=line_id))
|
|
280
|
+
if isinstance(response, M.ErrorMessage):
|
|
281
|
+
logger.error(f"Cancel request failed. Engine response: {response.message}")
|
|
282
|
+
return False
|
|
283
|
+
except Exception:
|
|
284
|
+
logger.error("Cancel request failed with exception", exc_info=True)
|
|
285
|
+
return False
|
|
286
|
+
engine_data.contributors.add(user_name)
|
|
287
|
+
return True
|
|
288
|
+
|
|
289
|
+
async def request_force(self, engine_id, line_id: str, user_name: str) -> bool:
|
|
290
|
+
engine_data = self._engine_data_map.get(engine_id)
|
|
291
|
+
if engine_data is None:
|
|
292
|
+
logger.warning(f"Cannot request force, engine {engine_id} not found")
|
|
293
|
+
return False
|
|
294
|
+
try:
|
|
295
|
+
response = await self.dispatcher.rpc_call(engine_id, message=AM.ForceMsg(exec_id=line_id))
|
|
296
|
+
if isinstance(response, M.ErrorMessage):
|
|
297
|
+
logger.error(f"Force request failed. Engine response: {response.message}")
|
|
298
|
+
return False
|
|
299
|
+
except Exception:
|
|
300
|
+
logger.error("Force request failed with exception", exc_info=True)
|
|
301
|
+
return False
|
|
302
|
+
engine_data.contributors.add(user_name)
|
|
303
|
+
return True
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class Aggregator:
|
|
307
|
+
def __init__(self, dispatcher: AggregatorDispatcher, publisher: FrontendPublisher) -> None:
|
|
308
|
+
self._engine_data_map: EngineDataMap = {}
|
|
309
|
+
""" all client data except channels, indexed by engine_id """
|
|
310
|
+
self.dispatcher = dispatcher
|
|
311
|
+
self.from_frontend = FromFrontend(self._engine_data_map, dispatcher)
|
|
312
|
+
self.from_engine = FromEngine(self._engine_data_map, publisher)
|
|
313
|
+
|
|
314
|
+
def create_engine_id(self, register_engine_msg: EM.RegisterEngineMsg):
|
|
315
|
+
""" Defines the generation of the engine_id that is uniquely assigned to each engine.
|
|
316
|
+
|
|
317
|
+
TODO: Considerations:
|
|
318
|
+
- engine name should be machine name
|
|
319
|
+
- uod hash should probably be included
|
|
320
|
+
|
|
321
|
+
Implications of the registration process
|
|
322
|
+
- historical data; we should not corrupt historical data by accidentially reusing engine_id
|
|
323
|
+
- number of cards shown; we should not show many irrelevant cards in frontend of superseeded engine_ids
|
|
324
|
+
"""
|
|
325
|
+
return register_engine_msg.computer_name + "_" + register_engine_msg.uod_name
|
|
326
|
+
|
|
327
|
+
def get_registered_engine_data(self, engine_id: str):
|
|
328
|
+
return self._engine_data_map.get(engine_id)
|
|
329
|
+
|
|
330
|
+
def get_all_registered_engine_data(self) -> list[EngineData]:
|
|
331
|
+
return list(self._engine_data_map.values())
|
|
332
|
+
|
|
333
|
+
def has_registered_engine_id(self, engine_id: str) -> bool:
|
|
334
|
+
return engine_id in self._engine_data_map.keys()
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import openpectus.protocol.aggregator_messages as AM
|
|
4
|
+
import openpectus.protocol.engine_messages as EM
|
|
5
|
+
import openpectus.aggregator.models as Mdl
|
|
6
|
+
from openpectus.aggregator.aggregator import Aggregator
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AggregatorMessageHandlers:
|
|
12
|
+
def __init__(self, aggregator: Aggregator):
|
|
13
|
+
self.aggregator = aggregator
|
|
14
|
+
aggregator.dispatcher.set_register_handler(self.handle_RegisterEngineMsg)
|
|
15
|
+
aggregator.dispatcher.set_disconnect_handler(self.handle_EngineDisconnected)
|
|
16
|
+
aggregator.dispatcher.set_connect_handler(self.handle_EngineConnected)
|
|
17
|
+
aggregator.dispatcher.set_message_handler(EM.ReconnectedMsg, self.handle_ReconnectedMsg)
|
|
18
|
+
aggregator.dispatcher.set_message_handler(EM.UodInfoMsg, self.handle_UodInfoMsg)
|
|
19
|
+
aggregator.dispatcher.set_message_handler(EM.TagsUpdatedMsg, self.handle_TagsUpdatedMsg)
|
|
20
|
+
aggregator.dispatcher.set_message_handler(EM.RunLogMsg, self.handle_RunLogMsg)
|
|
21
|
+
aggregator.dispatcher.set_message_handler(EM.ControlStateMsg, self.handle_ControlStateMsg)
|
|
22
|
+
aggregator.dispatcher.set_message_handler(EM.MethodStateMsg, self.handle_MethodStateMsg)
|
|
23
|
+
aggregator.dispatcher.set_message_handler(EM.ErrorLogMsg, self.handle_ErrorLogMsg)
|
|
24
|
+
|
|
25
|
+
async def handle_RegisterEngineMsg(self, register_engine_msg: EM.RegisterEngineMsg) -> AM.RegisterEngineReplyMsg:
|
|
26
|
+
""" Registers engine """
|
|
27
|
+
engine_id = self.aggregator.create_engine_id(register_engine_msg)
|
|
28
|
+
if self.aggregator.dispatcher.has_connected_engine_id(engine_id):
|
|
29
|
+
logger.error(
|
|
30
|
+
f"""Registration failed for engine_id {engine_id}. An engine with that engine_id already
|
|
31
|
+
has a websocket connection. """
|
|
32
|
+
)
|
|
33
|
+
return AM.RegisterEngineReplyMsg(success=False, engine_id=engine_id)
|
|
34
|
+
|
|
35
|
+
# TODO consider how to handle registrations
|
|
36
|
+
# - disconnect/reconnect should work
|
|
37
|
+
# - client kill/reconnect should work
|
|
38
|
+
# - engine_id reused with "same uod" should take over session, else fail as misconfigured client
|
|
39
|
+
# - add machine name + uod secret
|
|
40
|
+
|
|
41
|
+
# initialize client data
|
|
42
|
+
if not self.aggregator.has_registered_engine_id(engine_id):
|
|
43
|
+
self.aggregator.from_engine.register_engine_data(
|
|
44
|
+
Mdl.EngineData(
|
|
45
|
+
engine_id=engine_id,
|
|
46
|
+
computer_name=register_engine_msg.computer_name,
|
|
47
|
+
uod_name=register_engine_msg.uod_name,
|
|
48
|
+
uod_author_name=register_engine_msg.uod_author_name,
|
|
49
|
+
uod_author_email=register_engine_msg.uod_author_email,
|
|
50
|
+
uod_filename=register_engine_msg.uod_filename,
|
|
51
|
+
location=register_engine_msg.location,
|
|
52
|
+
engine_version=register_engine_msg.engine_version
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
logger.info(f"Registration of engine {engine_id} successful")
|
|
57
|
+
return AM.RegisterEngineReplyMsg(success=True, engine_id=engine_id)
|
|
58
|
+
|
|
59
|
+
async def handle_EngineConnected(self, engine_id: str):
|
|
60
|
+
self.aggregator.from_engine.engine_connected(engine_id)
|
|
61
|
+
|
|
62
|
+
async def handle_EngineDisconnected(self, engine_id: str):
|
|
63
|
+
self.aggregator.from_engine.engine_disconnected(engine_id)
|
|
64
|
+
|
|
65
|
+
async def handle_ReconnectedMsg(self, msg: EM.ReconnectedMsg):
|
|
66
|
+
self.aggregator.from_engine.engine_reconnected(msg)
|
|
67
|
+
return AM.SuccessMessage()
|
|
68
|
+
|
|
69
|
+
def validate_msg(self, msg: EM.EngineMessage):
|
|
70
|
+
if not self.aggregator.has_registered_engine_id(msg.engine_id):
|
|
71
|
+
return AM.ErrorMessage(message=f'No engine registered under id {msg.engine_id}')
|
|
72
|
+
# possibly more validations...
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
async def handle_UodInfoMsg(self, msg: EM.UodInfoMsg) -> AM.SuccessMessage | AM.ErrorMessage:
|
|
76
|
+
validation_errors = self.validate_msg(msg)
|
|
77
|
+
if validation_errors is not None:
|
|
78
|
+
return validation_errors
|
|
79
|
+
|
|
80
|
+
logger.debug(f"Got UodInfo from client: {str(msg)}")
|
|
81
|
+
self.aggregator.from_engine.uod_info_changed(
|
|
82
|
+
msg.engine_id, msg.readings, msg.commands, msg.plot_configuration, msg.hardware_str, msg.required_roles)
|
|
83
|
+
return AM.SuccessMessage()
|
|
84
|
+
|
|
85
|
+
async def handle_TagsUpdatedMsg(self, msg: EM.TagsUpdatedMsg) -> AM.SuccessMessage | AM.ErrorMessage:
|
|
86
|
+
validation_errors = self.validate_msg(msg)
|
|
87
|
+
if validation_errors is not None:
|
|
88
|
+
return validation_errors
|
|
89
|
+
|
|
90
|
+
logger.debug(f"Got tags update from client: {str(msg)}")
|
|
91
|
+
self.aggregator.from_engine.tag_values_changed(msg.engine_id, msg.tags)
|
|
92
|
+
return AM.SuccessMessage()
|
|
93
|
+
|
|
94
|
+
async def handle_RunLogMsg(self, msg: EM.RunLogMsg) -> AM.SuccessMessage | AM.ErrorMessage:
|
|
95
|
+
validation_errors = self.validate_msg(msg)
|
|
96
|
+
if validation_errors is not None:
|
|
97
|
+
return validation_errors
|
|
98
|
+
|
|
99
|
+
logger.debug(f"Got run log from client: {str(msg)}")
|
|
100
|
+
self.aggregator.from_engine.runlog_changed(msg.engine_id, msg.runlog)
|
|
101
|
+
return AM.SuccessMessage()
|
|
102
|
+
|
|
103
|
+
async def handle_ControlStateMsg(self, msg: EM.ControlStateMsg) -> AM.SuccessMessage | AM.ErrorMessage:
|
|
104
|
+
validation_errors = self.validate_msg(msg)
|
|
105
|
+
if validation_errors is not None:
|
|
106
|
+
return validation_errors
|
|
107
|
+
|
|
108
|
+
logger.debug(f"Got control state from client: {str(msg)}")
|
|
109
|
+
self.aggregator.from_engine.control_state_changed(msg.engine_id, msg.control_state)
|
|
110
|
+
return AM.SuccessMessage()
|
|
111
|
+
|
|
112
|
+
async def handle_MethodStateMsg(self, msg: EM.MethodStateMsg):
|
|
113
|
+
validation_errors = self.validate_msg(msg)
|
|
114
|
+
if validation_errors is not None:
|
|
115
|
+
return validation_errors
|
|
116
|
+
|
|
117
|
+
logger.debug(f"Got method state from client: {str(msg)}")
|
|
118
|
+
self.aggregator.from_engine.method_state_changed(msg.engine_id, msg.method_state)
|
|
119
|
+
return AM.SuccessMessage()
|
|
120
|
+
|
|
121
|
+
async def handle_ErrorLogMsg(self, msg: EM.ErrorLogMsg):
|
|
122
|
+
validation_errors = self.validate_msg(msg)
|
|
123
|
+
if validation_errors is not None:
|
|
124
|
+
return validation_errors
|
|
125
|
+
|
|
126
|
+
logger.debug(f'error log message from engine: {msg.log}')
|
|
127
|
+
self.aggregator.from_engine.error_log_changed(msg.engine_id, msg.log)
|
|
128
|
+
return AM.SuccessMessage()
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
import uvicorn
|
|
5
|
+
from openpectus.aggregator.routers.auth import UserRolesDependency
|
|
6
|
+
from alembic.config import Config
|
|
7
|
+
from fastapi import FastAPI, APIRouter
|
|
8
|
+
from fastapi.routing import APIRoute
|
|
9
|
+
from openpectus.aggregator.aggregator_message_handlers import AggregatorMessageHandlers
|
|
10
|
+
from openpectus.aggregator.data import database
|
|
11
|
+
from openpectus.aggregator.deps import _create_aggregator
|
|
12
|
+
from openpectus.aggregator.frontend_publisher import FrontendPublisher
|
|
13
|
+
from openpectus.aggregator.routers import process_unit, recent_runs, auth, version
|
|
14
|
+
from openpectus.aggregator.spa import SinglePageApplication
|
|
15
|
+
from openpectus.protocol.aggregator_dispatcher import AggregatorDispatcher
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AggregatorServer:
|
|
19
|
+
default_title = "Pectus Aggregator"
|
|
20
|
+
default_frontend_dist_dir = os.path.join(os.path.dirname(__file__), "frontend-dist")
|
|
21
|
+
# default_frontend_dist_dir = ".\\openpectus\\frontend\\dist"
|
|
22
|
+
default_host = "127.0.0.1"
|
|
23
|
+
default_port = 9800
|
|
24
|
+
|
|
25
|
+
def __init__(self, title: str = default_title, host: str = default_host, port: int = default_port,
|
|
26
|
+
frontend_dist_dir: str = default_frontend_dist_dir):
|
|
27
|
+
self.title = title
|
|
28
|
+
self.host = host
|
|
29
|
+
self.port = port
|
|
30
|
+
self.frontend_dist_dir = frontend_dist_dir
|
|
31
|
+
self.dispatcher = AggregatorDispatcher()
|
|
32
|
+
self.publisher = FrontendPublisher()
|
|
33
|
+
self.aggregator = _create_aggregator(self.dispatcher, self.publisher)
|
|
34
|
+
_ = AggregatorMessageHandlers(self.aggregator)
|
|
35
|
+
self.setup_fastapi([self.dispatcher.router, self.publisher.router, version.router])
|
|
36
|
+
self.init_db()
|
|
37
|
+
|
|
38
|
+
def setup_fastapi(self, additional_routers: list[APIRouter] = []):
|
|
39
|
+
api_prefix = "/api"
|
|
40
|
+
|
|
41
|
+
def custom_generate_unique_id(route: APIRoute):
|
|
42
|
+
return f"{route.name}"
|
|
43
|
+
|
|
44
|
+
self.fastapi = FastAPI(title=self.title,
|
|
45
|
+
generate_unique_id_function=custom_generate_unique_id,
|
|
46
|
+
on_shutdown=[self.on_shutdown])
|
|
47
|
+
self.fastapi.include_router(process_unit.router, prefix=api_prefix, dependencies=[UserRolesDependency])
|
|
48
|
+
self.fastapi.include_router(recent_runs.router, prefix=api_prefix, dependencies=[UserRolesDependency])
|
|
49
|
+
self.fastapi.include_router(auth.router, prefix='/auth')
|
|
50
|
+
for route in additional_routers:
|
|
51
|
+
self.fastapi.include_router(route)
|
|
52
|
+
if not os.path.exists(self.frontend_dist_dir):
|
|
53
|
+
raise FileNotFoundError("frontend_dist_dir not found: " + self.frontend_dist_dir)
|
|
54
|
+
self.fastapi.mount("/", SinglePageApplication(directory=self.frontend_dist_dir))
|
|
55
|
+
|
|
56
|
+
def init_db(self):
|
|
57
|
+
alembic_ini_file_path = os.path.join(os.path.dirname(__file__), "alembic.ini")
|
|
58
|
+
sqlalchemy_url = Config(alembic_ini_file_path).get_main_option('sqlalchemy.url')
|
|
59
|
+
if sqlalchemy_url is None:
|
|
60
|
+
raise ValueError('sqlalchemy.url not set in alembic.ini file')
|
|
61
|
+
database.configure_db(sqlalchemy_url)
|
|
62
|
+
self.fastapi.add_middleware(database.DBSessionMiddleware)
|
|
63
|
+
|
|
64
|
+
def start(self):
|
|
65
|
+
print(f"Serving frontend at http://{self.host}:{self.port}")
|
|
66
|
+
uvicorn.run(self.fastapi, host=self.host, port=self.port, log_level=logging.WARNING)
|
|
67
|
+
|
|
68
|
+
async def on_shutdown(self):
|
|
69
|
+
await self.dispatcher.shutdown()
|