Compare commits

...

852 Commits
v0.4.0 ... main

Author SHA1 Message Date
Mose Müller
533826a398
Merge pull request #234 from tiqi-group/release-v0.10.15
updates version to 0.10.15
2025-05-22 16:42:37 +02:00
Mose Müller
982875dee6
Merge pull request #235 from tiqi-group/feat/adds_client_id_default
feat: adds client id default
2025-05-22 16:16:02 +02:00
Mose Müller
e54710cd4d tests: update client_id test 2025-05-22 16:12:38 +02:00
Mose Müller
f48f7aacfb docs: updates client_id description 2025-05-22 16:10:52 +02:00
Mose Müller
e97aab4f36 client: adds hostname of the client as client_id default 2025-05-22 16:07:52 +02:00
Mose Müller
015c66d5a6 updates version to 0.10.15 2025-05-22 16:03:13 +02:00
Mose Müller
9827d0747c
Merge pull request #233 from tiqi-group/fix/task_event_loop
fix: task event loop
2025-05-22 16:01:29 +02:00
Mose Müller
38a12fb72e fix: current_event_loop_exists should get the event loop which might not be running yet 2025-05-22 15:57:35 +02:00
Mose Müller
fb6ec16bf5 server: set event loop before initialising the state manager
As the server is run first, we don't have to check if any other event
loop is running.
2025-05-22 15:57:09 +02:00
Mose Müller
9ee498eb5c
Merge pull request #232 from tiqi-group/fix/nested-attribute-notification
fix: properly checking is attribute is nested
2025-05-22 15:37:02 +02:00
Mose Müller
d015333123 tests: property starting with dependency name 2025-05-22 15:34:42 +02:00
Mose Müller
c4e7fe66a8 fix: properly checking is attribute is nested
Properties whose names start with a dependency's name (e.g., my_int ->
my_int_2) were incorrectly skipped during change notification. This
fixes it by checking if the changing properties start with the
full_access_path start followed by either "." or "[".
2025-05-22 15:34:09 +02:00
Mose Müller
5f1451a1c1
Merge pull request #231 from tiqi-group/fix/property_dependency_function_argument
Fix: property dependency function argument
2025-05-22 15:15:23 +02:00
Mose Müller
4c28cbaf7d tests: updates tests s.t. timezones don't matter 2025-05-22 15:07:10 +02:00
Mose Müller
a97b8eb2b4 fix: exclude ( from regex, as well 2025-05-22 15:06:30 +02:00
Mose Müller
f6b5c1b567 tests: property dependency as function argument 2025-05-22 14:51:33 +02:00
Mose Müller
f92d525588 fix: fixes regex pattern to get property dependencies 2025-05-22 14:50:29 +02:00
Mose Müller
61b69d77cc
Merge pull request #229 from tiqi-group/release-v0.10.14
updates to version 0.10.14
2025-05-21 09:51:38 +02:00
Mose Müller
8abe9357cf updates to version 0.10.14 2025-05-21 09:51:17 +02:00
Mose Müller
0dace2a9f0
Merge pull request #228 from tiqi-group/fix/aiohttp_socks_dependency
fix: using client without aiohttp_socks dependency does not raise
2025-05-21 09:49:29 +02:00
Mose Müller
9992ade0ed chore: formatting 2025-05-21 09:48:11 +02:00
Mose Müller
6c2cebada2 fix: using client without aiohttp_socks dependency does not raise
When not specifying the proxy_url in `pydase.Client`, the aiohttp_socks
dependency is not required. This is now handled by putting the import
into the correct place, adding a descriptive log message when the import
fails.
2025-05-21 09:46:20 +02:00
Mose Müller
069a2b4696
Merge pull request #227 from tiqi-group/release-v0.10.13
updates to version 0.10.13
2025-05-20 20:46:13 +02:00
Mose Müller
38ed8d78de updates to version 0.10.13 2025-05-20 20:45:53 +02:00
Mose Müller
7ff6cab9b3
Merge pull request #226 from tiqi-group/feat/proxy_support
Feat: add SOCKS5 proxy support to pydase.Client
2025-05-20 20:44:24 +02:00
Mose Müller
cbd93fb166 fixes client initialisation 2025-05-20 20:43:16 +02:00
Mose Müller
83c30439b6 docs: adds SOCKS5 proxy section 2025-05-20 20:43:16 +02:00
Mose Müller
e596c50915 fix: updates python-package workflow
Adds --all-extras flag to `poetry sync` command to install aiohttp_socks
dependency.
2025-05-20 20:43:16 +02:00
Mose Müller
9920350753 refactoring: moving initialisation of socketio client into separate function 2025-05-20 20:43:16 +02:00
Mose Müller
5dec01d800 adds aiohttp-socks as optional dependency 2025-05-20 15:52:32 +02:00
Mose Müller
18c66a8318 feat: adds support for services behind a SOCKS5 proxy 2025-05-20 15:48:49 +02:00
Mose Müller
3d65240784
Merge pull request #225 from tiqi-group/chore/update_pyproject_toml
Chore: update pyproject toml
2025-05-20 15:44:13 +02:00
Mose Müller
0d698e803d reducing test duration 2025-05-20 15:38:28 +02:00
Mose Müller
02a8791b74 ruff: fix TC001 (move into type-checking block) 2025-05-20 15:38:28 +02:00
Mose Müller
35f658ce4d chore: formatting 2025-05-20 15:38:28 +02:00
Mose Müller
051e616280 fixes PYI063 errors (ruff) 2025-05-20 15:38:28 +02:00
Mose Müller
98e9791d09 suppresses KeyError when removing key from observable dict mapping
This error is thrown in some test cases when some object gets deleted
but it's not within the dict mapping for some reason.
2025-05-20 15:38:28 +02:00
Mose Müller
24ecbd1eb9 tests: fixes test_version 2025-05-20 15:38:28 +02:00
Mose Müller
cd78d01b04 updates pyproject.toml and poetry.lock 2025-05-20 15:38:28 +02:00
Mose Müller
6be27217cf
Merge pull request #224 from tiqi-group/fix/loop-cleanup
chore: properly closing event loops in client and server
2025-05-20 15:37:57 +02:00
Mose Müller
ae2c99b3ae add pytest configuration
Ignores certain warnings I cannot control
2025-05-20 15:34:40 +02:00
Mose Müller
c32b6a8694 tests: proper handling of event loops
Event loops have to be closed properly, otherwise pytest will throw a
warning / error. Disconnecting clients and closing the server is
important for that, as well as stopping tasks.
Additionally, I had to add loop_scope parameters to share the event loop
throughout modules which all use the same pydase.Server instance.
2025-05-20 15:25:58 +02:00
Mose Müller
57cfe45c76 introduces check if current event loop is closed
This introduces a check for if the event loop is actually closed
already.
2025-05-20 15:24:54 +02:00
Mose Müller
7c18d86e9c properly closing event loops in client and server 2025-05-20 15:12:48 +02:00
Mose Müller
c5eb5f80b4
Merge pull request #223 from tiqi-group/refactor/client-lifecycle
client: allow reconnecting by moving loop and thread initialization out of constructor
2025-05-20 15:11:50 +02:00
Mose Müller
18c64db826 client: allow reconnecting by moving loop and thread initialization out of constructor
- Refactored Client to delay event loop and thread creation until
connect() is called.
- Introduced _initialize_loop_and_thread() helper for consistent
loop/thread/proxy setup.
- Updated disconnect() to stop the loop and join the thread without
closing the loop immediately. This allows proper cleanup and supports
reconnecting a client instance after disconnection.
- Fixes issues with restarting closed event loops and improves lifecycle
control in testing and production.
2025-05-20 15:07:15 +02:00
Mose Müller
27f8e1b1bc
Merge pull request #222 from tiqi-group/fix/mkdocs_config_file
fix: update mkdocs.yml
2025-05-19 15:12:17 +02:00
Mose Müller
f8839f0e71 fix: update mkdocs.yml 2025-05-19 15:10:29 +02:00
Mose Müller
d31aff0b9b
Merge pull request #221 from tiqi-group/docs/restructuring
Docs: restructuring
2025-05-19 15:07:49 +02:00
Mose Müller
2c1db3fa45 docs: restructuring Interaction section in User Guide 2025-05-19 15:01:35 +02:00
Mose Müller
b9cec19b02 docs: updating and adding links 2025-05-19 15:00:45 +02:00
Mose Müller
6ba5193e9e docs: remove commented-out information 2025-05-19 15:00:45 +02:00
Mose Müller
bc0c69f9e1
Merge pull request #220 from tiqi-group/release-v0.10.12
updates to version v0.10.12
2025-05-09 11:01:20 +02:00
Mose Müller
b2314f7e33 updates to version v0.10.12 2025-05-09 10:58:37 +02:00
Mose Müller
eb43e7b380
Merge pull request #219 from tiqi-group/feat/improve-input-cursor-handling
Feat: improve input cursor handling
2025-05-09 10:56:50 +02:00
Mose Müller
5dc28b0b55 npm run build 2025-05-09 10:54:55 +02:00
Mose Müller
c327215b5f feat: selection range in NumberComponent can be changed using Shift and arrows
When pressing shift, the arrow keys can be used to change the selection
range. This was done by using a cursor position reference instead of a
state and adapting the default behaviour of the arrow keys instead of
writing them from scratch.
2025-05-09 10:54:36 +02:00
Mose Müller
04a3b225f8
Merge pull request #218 from tiqi-group/docs/acknowledgements
readme: adds acknowledgements section
2025-05-08 10:17:04 +02:00
Mose Müller
86c4514e1a readme: adds acknowledgements section 2025-05-05 08:14:08 +02:00
Mose Müller
cac74e90db
Merge pull request #217 from tiqi-group/release-v0.10.11
updates version to v0.10.11
2025-04-15 08:17:42 +02:00
Mose Müller
c24d63f4c0 updates version to v0.10.11 2025-04-15 08:17:21 +02:00
Mose Müller
b0dd5835a3
Merge pull request #216 from tiqi-group/config/changing_loading_behaviour
feat (config): changes web_port loading
2025-04-15 08:16:12 +02:00
Mose Müller
b0c8af0108 config: changes web_port loading
The web_port argument in the pydase.Server defaults to None now. If it
is None, the value from ServiceConfig().web_port will be used.
This fixes the issue where users might pass the web port dynamcially and
by passing None they want to use the default value.
2025-04-15 08:12:45 +02:00
Mose Müller
c0016673a8 fix: poetry lock 2025-04-11 14:50:11 +02:00
Mose Müller
eadc1df763
Merge pull request #215 from tiqi-group/docs/update_deps
Docs: update dependencies
2025-04-11 14:47:51 +02:00
Mose Müller
922fdf8fd0 mkdocs: replaces deprecated import key with inventories 2025-04-11 14:46:41 +02:00
Mose Müller
8b21c42ef7 updates python dependencies 2025-04-11 14:46:19 +02:00
Mose Müller
2399b3ca9f
Merge pull request #214 from tiqi-group/192-starting-a-task-on-a-dataservice-exposed-as-a-property-causes-the-button-to-spin-indefinitely
fix: correctly handle observable properties
2025-03-28 09:44:11 +01:00
Mose Müller
db43f5dbbb tests: adds test reproducing the read-only dict bug 2025-03-28 09:00:59 +01:00
Mose Müller
f2c0a94904 fix: adds observable to an observable object accessed via a property
When an observable is stored returned by a property, this adds the
parent object as an observer to the observable returned by the property.
2025-03-28 09:00:59 +01:00
Mose Müller
c36cebf17c
Merge pull request #213 from tiqi-group/release-v0.10.10
updates version to v0.10.10
2025-03-27 11:49:21 +01:00
Mose Müller
a96387b4d7 updates version to v0.10.10 2025-03-27 11:48:50 +01:00
Mose Müller
d1feff1a6a
Merge pull request #211 from tiqi-group/feat/add_logging_config_helper
Feat: add logging config helper
2025-03-27 11:47:08 +01:00
Mose Müller
95df2f1650 docs: updates logging documentation 2025-03-27 11:43:28 +01:00
Mose Müller
0565c82448 renames function, can now take the name of the logger (defaults to root logger) 2025-03-27 11:43:15 +01:00
Mose Müller
755265bf53
Merge pull request #212 from tiqi-group/44-manually-saving-current-state-of-all-objects
Feat: autosaving feature
2025-03-25 14:06:30 +01:00
Mose Müller
4c7b386ab4 docs: updates state manager docstring (adds DataServiceCache link) 2025-03-25 14:05:21 +01:00
Mose Müller
92b2326dfc tests: adds autosave feature test 2025-03-25 14:01:31 +01:00
Mose Müller
9e18783a05 adds default values to StateManager 2025-03-25 13:46:39 +01:00
Mose Müller
9be4aac988 docs: updates service persistence documentation 2025-03-25 13:41:57 +01:00
Mose Müller
f3d659670f feat: adds autosave feature
The pydase service automatically saves the current state to a file now.
The interval between automatic save events can be configured using the
`autosave_interval` argument passed to the pydase.Server.
2025-03-25 13:41:25 +01:00
Mose Müller
23f051d6f1 fixes pyright issue 2025-02-28 17:08:42 +01:00
Mose Müller
c8979ab2e6 adds logging tests and type hints 2025-02-28 16:56:40 +01:00
Mose Müller
bd33252775 log: adds helper function configure_root_logger_with_pydase_formatter 2025-02-28 16:56:16 +01:00
Mose Müller
1fbcbc72bf
Merge pull request #210 from tiqi-group/logging/updates_handlers
Feat: Updates logging handlers
2025-02-28 15:57:06 +01:00
Mose Müller
9a8628cfbd log: only write pydase.server to stdout, the rest to stderr 2025-02-28 15:53:14 +01:00
Mose Müller
3d13b20fda log: using filters to separate server and task logs from the rest 2025-02-28 15:43:24 +01:00
Mose Müller
f2183ec3e4 log: changing log level of trigger_method 2025-02-27 15:45:01 +01:00
Mose Müller
360aeb5574 log: pydase.server logs are written to stdout, the rest is written to stderr 2025-02-27 15:37:35 +01:00
Mose Müller
e85e93a1d9
Merge pull request #209 from tiqi-group/release-v0.10.9
updates version to v0.10.9
2025-02-20 17:34:00 +01:00
Mose Müller
ea5fd42919 updates version to v0.10.9 2025-02-20 17:33:33 +01:00
Mose Müller
247113f1db
Merge pull request #208 from tiqi-group/feat/add_client_id_header
client: adds X-Client-Id header to pydase.Client
2025-02-20 17:30:31 +01:00
Mose Müller
c76b0b0b6e updates test 2025-02-20 17:28:53 +01:00
Mose Müller
2d39c56e3d updates docs 2025-02-20 17:28:07 +01:00
Mose Müller
60287fef95 client: client_id arg defaults to None 2025-02-20 17:27:55 +01:00
Mose Müller
c5e1a08c54 client: adds X-Client-Id header to pydase.Client 2025-02-20 17:17:19 +01:00
Mose Müller
9424d4c412
Merge pull request #207 from tiqi-group/fix/removes_backtick_from_index_html
chore: removes backtick typo from index.html
2025-01-20 14:01:15 +01:00
Mose Müller
0a4c13c617 frontend: removes backtick typo from index.html 2025-01-20 14:00:39 +01:00
Mose Müller
5d72604199
Merge pull request #206 from tiqi-group/fix/serving_modified_html
fix: serves modified index.html (X-Forwarded-Proto) when X-Forwarded-Prefix is not set
2025-01-20 13:56:42 +01:00
Mose Müller
3479c511fe fix: serves modified index.html (X-Forwarded-Proto) when X-Forwarded-Prefix is not set
When X-Forwarded-Prefix was not set, the X-Forwarded-Proto was also not
updated on the index.html file.
2025-01-20 13:55:04 +01:00
Mose Müller
9bf3b28390
Merge pull request #205 from tiqi-group/frontend/update_packages
frontend: updates packages
2025-01-20 13:11:49 +01:00
Mose Müller
0195f9d6f6 frontend: updates packages 2025-01-20 13:07:44 +01:00
Mose Müller
197268255b fix: using new github action download-artifact version 2025-01-20 09:29:49 +01:00
Mose Müller
3698cb7f92
Merge pull request #186 from tiqi-group/175-add-support-for-enhanced-client-information-logging-in-socketio-server
feat: add support for enhanced client information logging in socketio server
2025-01-20 09:18:45 +01:00
Mose Müller
0625832457 tests: adds tests for socketio clients 2025-01-20 09:16:32 +01:00
Mose Müller
f35bcf3be6 fix: getting method in sio setup within try ... except block 2025-01-20 08:08:14 +01:00
Mose Müller
3fe77bb4e5 docs: adds logging user-guide 2025-01-20 07:32:54 +01:00
Mose Müller
9b2d181f4a refactor(logging): update header priority for client identification
Changed the priority of headers for client identification in logs:
Now prioritizing the 'Remote-User' header over the 'X-Client-ID' header.
2025-01-20 07:32:54 +01:00
Mose Müller
045334e51e fix: http endpoint trigger_method
The trigger_method endpoint was retrieving the access_path parameter as
a the query parameter. Instead, it should get it from the request body.
2025-01-20 06:24:45 +01:00
Mose Müller
1d8d17d715 tests: adds tests for restapi client logs 2025-01-20 06:18:05 +01:00
Mose Müller
4d84c9778f RestAPI: adds support for logging client information 2025-01-20 06:18:05 +01:00
Mose Müller
e3c144fa6e socketio: adds support for logging client information 2025-01-20 06:18:05 +01:00
Mose Müller
192075057f
Merge pull request #204 from tiqi-group/fix/task_finishing_gracefully
fix: return result of task after finishing gracefully
2025-01-20 06:13:31 +01:00
Mose Müller
053050a62c tasks: return result of task after finishing gracefully
Tasks that finished gracefully were restarted again. This fixes that.
2025-01-20 06:10:48 +01:00
Mose Müller
aacc69ae94 changes version to v0.10.8 2025-01-18 07:24:12 +01:00
Mose Müller
de1483bdc5
Merge pull request #203 from tiqi-group/feat/add_more_task_config_options
chore: adds task docs, renames restart_on_failure to restart_on_exception
2025-01-18 07:23:16 +01:00
Mose Müller
b24db00eda renames restart_on_failure to restart_on_exception 2025-01-18 07:19:04 +01:00
Mose Müller
36ee760610
Merge pull request #202 from tiqi-group/feat/add_more_task_config_options
Feat: add more task config options
2025-01-17 20:40:28 +01:00
Mose Müller
3a67c07bad docs: updates Task documentation 2025-01-17 20:37:37 +01:00
Mose Müller
b9a91e5ee2 removes timeout_start_sec
I misinterpreted this option as the time to wait before starting the
task. This is apparently not what it stands for in systemd.service
2025-01-17 20:32:44 +01:00
Mose Müller
f83bc0073b fix: tests were expecting linux-type signals 2025-01-17 20:23:45 +01:00
Mose Müller
c66b90c4e5 chore: refactoring Task 2025-01-17 20:21:00 +01:00
Mose Müller
d0b0803407 adds tests for new task options 2025-01-17 20:00:04 +01:00
Mose Müller
e25511768d task: removes check if function is bound (not used) 2025-01-17 19:59:51 +01:00
Mose Müller
303de82318 changes restart_on_failure default to True 2025-01-17 17:37:52 +01:00
Mose Müller
db559e8ada removes defaults in Task and PerInstanceTaskDescriptor
Removes overhead of keeping defaults the same everywhere.
2025-01-17 17:37:39 +01:00
Mose Müller
1b35dba64f task: adds exit_on_failure option 2025-01-17 17:33:53 +01:00
Mose Müller
8a8ac9d297 task: adds systemd-like keyword arguments to task decorator 2025-01-17 17:16:19 +01:00
Mose Müller
40a8863ecd
Merge pull request #201 from tiqi-group/200-trailing-zeros-removed-when-changing-numbers-with-arrow-keys-in-number-component
fix: Cursor jumps in NumberComponent when number is updated in the backend and frontend rerenders
2025-01-17 15:46:27 +01:00
Mose Müller
1dca04f693 npm run dev 2025-01-17 15:43:01 +01:00
Mose Müller
2b520834dc fix: overwrites left and right arrow key behaviour in NumberComponent
The cursor position was not stored when moving the cursor without
changing the number.
2025-01-17 15:42:33 +01:00
Mose Müller
d6bad37233
Merge pull request #197 from tiqi-group/fix/dict_key_normalization
Fix: dict key normalization
2024-12-20 14:43:04 +01:00
Mose Müller
53a2a3303f removes helper function normalize_full_access_path_string 2024-12-20 14:41:14 +01:00
Mose Müller
4f206bbae9 tests: adds test_nested_dict_property_changes 2024-12-20 14:40:01 +01:00
Mose Müller
090b8acd44 fix: replaces single quote with double quote in PropertyObserver
When collecting collection item property dependencies, the
PropertyObserver was adding dict keys in single quotes instead of double
quotes.
2024-12-20 14:18:45 +01:00
Mose Müller
17b2ad32e5 fix: remove string normalization to fix issues with nested dictionary property changes
- Removed normalization logic that replaced double quotes with single
quotes for attribute paths.
2024-12-20 14:03:25 +01:00
Mose Müller
3c99f3fe04 replaces logger.error with logger.exception to get stack trace 2024-12-20 10:35:13 +01:00
Mose Müller
2bcc6b9660 fix: removes aiohttp warnings (popping up when running pytest) 2024-12-19 13:31:35 +01:00
Mose Müller
c1ace54c78
Merge pull request #196 from tiqi-group/feat/log_trigger_method_exception
feat: log trace when exception occurs within trigger_method
2024-12-19 13:13:03 +01:00
Mose Müller
56af2a423b replaces logger.error with logger.exception when exception occurs inside function 2024-12-19 13:09:35 +01:00
Mose Müller
eba0eb83e6
Merge pull request #194 from tiqi-group/chore/update-github-actions-versions
chore: update artifact action versions
2024-12-19 10:11:19 +01:00
Mose Müller
b7818c0d8a
Merge pull request #195 from tiqi-group/chore/set_number_slider_types_to_any
chore: sets number slider type hints to Any
2024-12-19 10:11:07 +01:00
Mose Müller
a0c3882f35 chore: sets number slider type hints to Any
This removes mypy type errors when overwriting the properties in a
derived class.
2024-12-19 10:03:57 +01:00
Mose Müller
1d773ba09b chore: updates artifact action versions 2024-12-19 07:48:00 +01:00
Mose Müller
10f1b8691c docs: adds logging.basicConfig to logging section 2024-12-16 10:49:53 +01:00
Mose Müller
a99db6f053 updates bug report template 2024-12-03 16:08:54 +01:00
Mose Müller
36ab8ab68b
Merge pull request #191 from tiqi-group/fix/client_context_manager
Fix: client context manager
2024-12-02 15:07:07 +01:00
Mose Müller
27a832bbd1 client: adds ctx manager tests 2024-12-02 14:59:35 +01:00
Mose Müller
18df9e288a client: replaces __del__ with __exit__ method to properly define ctx manager 2024-12-02 14:59:35 +01:00
Mose Müller
7b786be892
Merge pull request #190 from tiqi-group/fix/async_functions
Fix: triggering async functions
2024-12-02 14:58:55 +01:00
Mose Müller
374a930745 reduces complexity of create_api_application method 2024-12-02 14:57:23 +01:00
Mose Müller
6d12e5c939 tests: adds client test for async method triggering 2024-12-02 14:07:14 +01:00
Mose Müller
bcf37067ad client: fixes async function handling 2024-12-02 14:04:23 +01:00
Mose Müller
a1ac0c2f88 tests: adds tests for trigger_method http endpoint 2024-12-02 13:53:25 +01:00
Mose Müller
cfe190ca5b updates http trigger_method endpoint to handle async methods, as well 2024-12-02 13:44:15 +01:00
Mose Müller
c002d04328 updates trigger_method sio event to handle async methods, as well 2024-12-02 13:41:21 +01:00
Mose Müller
0d1df4f9e5 adds endpoint function to trigger async function 2024-12-02 13:40:56 +01:00
Mose Müller
59cc834a81 fix: display banner in documentation (replaces html with md) 2024-11-26 16:50:18 +01:00
Mose Müller
dc54d9faef increase banner resolution 2024-11-26 16:50:18 +01:00
Mose Müller
89bf5cb3f1 Merge branch 'docs/add_logo' 2024-11-26 16:29:20 +01:00
Mose Müller
c72ea9eb20 fix: changes text colour in banner, using png instead of svg 2024-11-26 16:27:59 +01:00
Mose Müller
897387e39e
Merge pull request #185 from tiqi-group/feat/add_favicon
Feat: add favicon
2024-11-26 14:40:43 +01:00
Mose Müller
4454a10f78
Merge pull request #184 from tiqi-group/docs/add_logo
Docs: add logo
2024-11-26 14:40:27 +01:00
Mose Müller
c9814f7cdc updates Readme and docs 2024-11-26 14:04:34 +01:00
Mose Müller
187d8bcf28 makes favicon path configurable
By passing a path to another image to the `favicon_path` argument in
pydase.Server, the user can change the default favicon icon.
2024-11-26 14:04:34 +01:00
Mose Müller
204d426663 adds favicon route 2024-11-26 14:04:34 +01:00
Mose Müller
29e9afa47e frontend: adds logo as favicon 2024-11-26 14:04:34 +01:00
Mose Müller
a6943c027f adds logo to Readme 2024-11-26 14:04:23 +01:00
Mose Müller
70e4fa73e1 adds logo to documentation 2024-11-26 09:00:12 +01:00
Mose Müller
579fa4715b adds pydase logo (colour / bw) 2024-11-26 09:00:12 +01:00
Mose Müller
0100bab04f fix: default X-Forwarded-Proto to "http" 2024-11-26 08:59:46 +01:00
Mose Müller
bdf97fa181
Merge pull request #181 from tiqi-group/fix/mixed_content_protocols
Fix: mixed content protocols
2024-11-21 14:40:22 +01:00
Mose Müller
eb1587fa7d npm run build 2024-11-21 14:31:15 +01:00
Mose Müller
5827cda316 adds support for X-Forwarded-Proto 2024-11-21 14:30:33 +01:00
Mose Müller
0e9ec7a66a
Merge pull request #180 from tiqi-group/179-authentication-headers-and-cookies-are-not-passed-to-cross-origin-requests
fix: pass credentials to cross-origin fetch requests
2024-11-19 11:50:57 +01:00
Mose Müller
155957f0c5 fix: pass credentials to cross-origin fetch requests 2024-11-19 11:47:28 +01:00
Mose Müller
a8b46f191b
Merge pull request #167 from tiqi-group/feat/stripprefix_support
Feat: support for service deployments behind PathPrefix proxy rules
2024-11-18 10:20:57 +01:00
Mose Müller
3862ce3405 docs: adds section about deploying services behind a reverse proxy 2024-11-18 10:18:32 +01:00
Mose Müller
5403b51a5b escape the user input before including it in the HTML response 2024-11-18 09:34:58 +01:00
Mose Müller
1270400e95 updates pydase.Client to handle services behind PathPrefix proxy 2024-11-18 09:34:58 +01:00
Mose Müller
3d2bb1c528 updates comments in index.html 2024-11-18 09:34:58 +01:00
Mose Müller
7c68f02cfd npm run build 2024-11-18 09:34:58 +01:00
Mose Müller
ccd6447869 replaces all hostname:port usages with authority variable 2024-11-18 09:34:58 +01:00
Mose Müller
056c02c5a5 gets and uses forwarded prefix in socket.ts 2024-11-18 09:34:58 +01:00
Mose Müller
52a798e4c8 adds window.__FORWARDED_PREFIX__ to index.html 2024-11-18 09:34:58 +01:00
Mose Müller
fdfdef5837 gets X-Forwarded-Prefix from requests and adds it to index.html 2024-11-18 09:34:58 +01:00
Mose Müller
ff301f225c adds anyio dependency 2024-11-18 09:34:58 +01:00
Mose Müller
87f720f567
Merge pull request #178 from tiqi-group/177-docs-add-configuration-section-to-readthedocs
docs: add configuration section to readthedocs
2024-11-18 09:29:28 +01:00
Mose Müller
fecb46c02c docs: updates Readme (configuration section) 2024-11-18 09:22:21 +01:00
Mose Müller
cce2399b07 docs: udpates link to configuration section 2024-11-18 09:22:07 +01:00
Mose Müller
df1db99ec0 docs: adds Configuration section 2024-11-18 09:21:56 +01:00
Mose Müller
5f2619500b removes unused constants 2024-10-03 14:30:28 +02:00
Mose Müller
843675fa1e
Merge pull request #174 from tiqi-group/doc/fixes_docstring
fixes docstring of add_prefix_to_full_access_path
2024-10-03 11:00:26 +02:00
Mose Müller
2aa370c8ac updates to version v0.10.7 2024-10-03 11:00:15 +02:00
Mose Müller
c25ff4a3aa fixes docstring of add_prefix_to_full_access_path 2024-10-03 10:59:21 +02:00
Mose Müller
5e32a70c3e
Merge pull request #173 from tiqi-group/fix/remove_method_warning
Fix: removes warning message when initialising a Client
2024-10-03 10:57:56 +02:00
Mose Müller
3f6692a1cd DataService.__warn_if_not_observable does not warn if setting function 2024-10-03 10:55:28 +02:00
Mose Müller
eb32b34b59
Merge pull request #172 from tiqi-group/feat/update_client_reconnection
Feat: update client reconnection
2024-10-02 09:29:39 +02:00
Mose Müller
9eedf03c01 adds reconnection method to proxy class which is called when the sio client does not reconnect 2024-10-02 09:18:32 +02:00
Mose Müller
5ec7a8b530 docs: updates Python Client user guide 2024-10-02 08:24:29 +02:00
Mose Müller
f2f330dbd9 docs: adds python-socketio object inventory 2024-10-02 07:11:35 +02:00
Mose Müller
2e0e056489 adds sio_client_kwargs as pydase.Client keyword argument 2024-10-02 07:10:34 +02:00
Mose Müller
d8685fe9a0
Merge pull request #169 from tiqi-group/fix/proxy_class_representation
Fix: proxy class representation
2024-10-01 11:02:44 +02:00
Mose Müller
e52a019d5e fixes add_prefix_to_full_access_path, updates tests
The prefix does not contain a "." anymore. This will be added by the
function itself (to be able to distinguish empty full access paths).
2024-10-01 11:01:01 +02:00
Mose Müller
0d5cef1537 updates how Client handles (re-)connection with the server
The client will update the proxy class serialization directly on the
ProxyClass instance. this is the only time this get "updated".
Now, the client also notifies the observers directly with the proxy
object as this the serialization of the proxy class is now done through
its `serialize` method (which we have overwritten in a previous commit).
2024-10-01 10:55:29 +02:00
Mose Müller
e8f33eee4d updates ProxyClass serialize method
The ProxyClass will keep a copy of its serialized state s.t. it does not
have to call the remote service. This hangs the event loop if trying to
call asyncio.run_coroutine_threadsafe when already inside the thread.
2024-10-01 10:55:29 +02:00
Mose Müller
a3b71b174c fixes proxy class serialization (needs device connection methods and properties) 2024-10-01 08:25:39 +02:00
Mose Müller
e2ce0e9acb adds ProxyClass serialization support 2024-10-01 07:27:27 +02:00
Mose Müller
f47a183c11 adds add_prefix_to_full_access_path helper function 2024-10-01 07:13:22 +02:00
Mose Müller
a9ea237cf3 overrides serialize method in ProxyClass, getting it from remote service 2024-09-30 16:58:01 +02:00
Mose Müller
6db1652dd3 move ProxyClass into separate file 2024-09-30 16:57:12 +02:00
Mose Müller
e3b95a8076
Merge pull request #168 from tiqi-group/fix/logging_to_stdout
Fix: logging to stdout
2024-09-30 10:57:23 +02:00
Mose Müller
0fe2a8516f updates to version v0.10.6 2024-09-30 10:55:48 +02:00
Mose Müller
51bbaba162 writing to stdout instead of stderr 2024-09-30 10:54:58 +02:00
Mose Müller
77802da417
Merge pull request #166 from tiqi-group/fix/logging_settings
fix: removes basic configuration of logging system in task module
2024-09-25 19:50:10 +02:00
Mose Müller
3e21858cb7 removes basic configuration of logging system in task module 2024-09-25 19:48:32 +02:00
Mose Müller
2003f28fd1
Merge pull request #165 from tiqi-group/fix/client_usage_in_jupyter_notebook
Fix: client usage in jupyter notebook
2024-09-25 19:41:42 +02:00
Mose Müller
172b50bf77 updates to version v0.10.5 2024-09-25 19:38:28 +02:00
Mose Müller
ec5694fedf no need to check for RuntimeError as the loop is always new 2024-09-25 19:38:09 +02:00
Mose Müller
968f774092 always create a new event loop in the client and pass it to a new thread 2024-09-25 19:37:33 +02:00
Mose Müller
757dc9aa3c
Merge pull request #163 from tiqi-group/fix/removes_PerInstanceTaskDescriptor_warning
fix: removes inheritance warning for descriptors
2024-09-23 13:35:44 +02:00
Mose Müller
3d938562a6 updates to version v0.10.4 2024-09-23 13:35:33 +02:00
Mose Müller
964a62d4b4 removes inheritance warning for descriptors 2024-09-23 13:33:13 +02:00
Mose Müller
99aa38fcfe chore: removes unused code 2024-09-23 13:28:08 +02:00
Mose Müller
5658514c8a
Merge pull request #162 from tiqi-group/fix/normalize_full_access_path
Fix: normalize full access path (for dict keys)
2024-09-23 13:23:11 +02:00
Mose Müller
109ee7d5e1 updates version to v0.10.3 2024-09-23 13:16:50 +02:00
Mose Müller
f4fa02fe11 adds test enuring dict keys can be encoded with both single and double quotes 2024-09-23 13:16:50 +02:00
Mose Müller
487ef504a8 normalizes full access path strings containing dict keys with double quotes
Full access paths containing stringed dictionary keys are sent with
double quotes from the frontend. The quotes have to be changed to single
quotes s.t. the comparison with the property dependency dictionary
works.
2024-09-23 13:16:49 +02:00
Mose Müller
c98e407ed7
Merge pull request #161 from tiqi-group/160-control-of-tasks-in-instances-derived-from-same-class-only-controls-task-of-first-instance
fix: controlling tasks of instances derived from same class
2024-09-23 12:54:38 +02:00
Mose Müller
6b6ce1d43f adds test checking for multiple instances of a class containing a task 2024-09-23 09:44:43 +02:00
Mose Müller
e491ac7458 observable does not have to initialise descriptor objects anymore
The task decorator is not returning a Task object directly anymore, but
rather a descriptor which is returning the task. This is where the task
is initialised and this does not have to be done in the observable base
class, any more.
2024-09-23 09:27:15 +02:00
Mose Müller
e9d8cbafc2 adds PerInstanceTaskDescriptor class managing task objects for service class instances
When defining a task on a DataService class, formerly a task object was
created which replaced the decorated method as a class attribute. This
caused errors when using multiple instances of that class as each
instance was referring to the same task.
This descriptor class now handles the tasks per instance of the service
class.
2024-09-23 09:21:04 +02:00
Mose Müller
aa705592b2 removes code from Task meant to bind the passed function to the containing class instance
The task object will only receive bound methods, so there is no need to
keep the descriptor functionality anymore.
2024-09-23 09:15:42 +02:00
Mose Müller
008e1262bb updates PropertyObserver to support descriptors returning observables
If a class attribute is a descriptor, get its value before checking if
it is an Observable.
2024-09-23 08:57:00 +02:00
Mose Müller
91a71ad004 updates is_descriptor to exclude false positives for methods, functions and builtins 2024-09-23 08:55:44 +02:00
Mose Müller
bbf479a440
Merge pull request #159 from tiqi-group/158-defining-task-without-autostart-fails
fix: defining task without autostart fails
2024-09-21 11:43:31 +02:00
Mose Müller
983d392ba8 properly handle Task objects in autostart method
Tasks that are not autostart or are already running were passed to
autostart_nested_services. This caused the recursion as tasks have a
__self__ attribute pointing to the containing service.
2024-09-21 11:40:25 +02:00
Mose Müller
56dd9dd8aa adapts autostart to support nested lists in dicts and vice versa 2024-09-21 09:12:01 +02:00
Mose Müller
20028c379d test: updates task tests 2024-09-21 09:04:04 +02:00
Mose Müller
e48046795e updates version to v0.10.2 2024-09-21 08:37:34 +02:00
Mose Müller
1ac9e45c73 test: updates task test to catch recursion when defining without autostart 2024-09-21 08:36:47 +02:00
Mose Müller
488415436c fixes recursion when defining task without autostart 2024-09-21 08:32:54 +02:00
Mose Müller
d7c5c2cd6e updates to version v0.10.1 2024-09-17 16:52:39 +02:00
Mose Müller
5388fd0d2b
Merge pull request #157 from tiqi-group/fix/handle_minus_sign_input
fix: correctly handling minus sign input in NumberComponent
2024-09-17 16:52:08 +02:00
Mose Müller
e74b5c773a npm run build 2024-09-17 16:51:00 +02:00
Mose Müller
bb6cd159f1 frontend: refactoring minus sign handling in NumberComponent 2024-09-17 16:48:40 +02:00
Mose Müller
4a09f02882 docs: updates Readme
Trying to clarify the usage of ports in both server and clients.
2024-09-17 07:23:45 +02:00
Mose Müller
9180bb1d9e
Merge pull request #150 from tiqi-group/feat/task_decorator
Feat: Replace implicit async function tasks with task decorator
2024-09-16 15:51:52 +02:00
Mose Müller
ece68b4b99 docs: updates Task documentation
- updates Tasks.md
- updates docstrings
- adds api section
2024-09-16 15:30:47 +02:00
Mose Müller
0c95b5e3cb frontend: removes AsyncMethodComponent (replaced by Task) 2024-09-16 14:22:29 +02:00
Mose Müller
0450bb1570 updates version to v0.10.0 2024-09-16 14:18:10 +02:00
Mose Müller
2f5a640c4c chore: refactored task autostart 2024-09-16 14:17:20 +02:00
Mose Müller
78964be506 adds serialization and deserialization support for task objects 2024-09-16 13:58:58 +02:00
Mose Müller
fbdf6de63c npm run build 2024-09-16 13:58:16 +02:00
Mose Müller
9b04dcd41e frontend: ass Task component 2024-09-16 13:46:07 +02:00
Mose Müller
32e36d4962 adds task tests 2024-09-16 07:53:46 +02:00
Mose Müller
62f28f79db adds list and dictionary entries to task autostart 2024-09-16 07:53:44 +02:00
Mose Müller
e88965b69d fixes device connection test 2024-09-13 16:09:39 +02:00
Mose Müller
e422d627af adds docstring to autostart method 2024-09-13 16:07:30 +02:00
Mose Müller
2e31ebb7d9 fixes or removes task-related tests 2024-09-13 16:07:29 +02:00
Mose Müller
71adc8bea2 adds autostart to server 2024-09-13 12:37:29 +02:00
Mose Müller
bfa0acedab moves autostart from Task to separate autostart submodule 2024-09-13 12:37:18 +02:00
Mose Müller
416b9ee815 removes part of serializer for serializing start and stop methods of async methods 2024-09-13 11:27:30 +02:00
Mose Müller
d1d2ac2614 fixing circular import 2024-09-13 11:27:30 +02:00
Mose Müller
fa35fa53e2 removes TaskManager 2024-09-13 11:27:30 +02:00
Mose Müller
c0e5a77d6f simplifies @task decorator (updates types), moves task logic into Task's run_task() 2024-09-13 11:27:30 +02:00
Mose Müller
96cc7b31b4 updates documentation 2024-09-13 11:27:30 +02:00
Mose Müller
0d6d312f68 chore: fixes type hints for python 3.10 2024-09-13 11:27:30 +02:00
Mose Müller
be3011c565 adapt device connection component to use @task decorator 2024-09-13 11:27:30 +02:00
Mose Müller
09fae01985 adds warning when _bound_func has not been bound yet
This might arise when calling the start method of a task which is part of a class
that has not been instantiated yet.
2024-09-13 11:27:30 +02:00
Mose Müller
12c0c9763d delay task setup until called from class instance containing the task 2024-09-13 11:27:30 +02:00
Mose Müller
15322b742d using explicit loop to create task even if loop is not running yet 2024-09-13 11:27:30 +02:00
Mose Müller
85d6229aa6 updates DataService import to avoid circular import 2024-09-13 11:27:30 +02:00
Mose Müller
083fab0a29 Carefully setting up asyncio event loop 2024-09-13 11:27:30 +02:00
Mose Müller
2a1aff589d properly binding task method 2024-09-13 11:27:30 +02:00
Mose Müller
3cd7198747 task can only wrap async functions without arguments 2024-09-13 11:27:30 +02:00
Mose Müller
1e02f12794 adds autostart flag to task 2024-09-13 11:27:30 +02:00
Mose Müller
e4a3cf341f task can receive bound and unbound functions now 2024-09-13 11:27:30 +02:00
Mose Müller
7ddcd97f68 fixing ruff and mypy errors 2024-09-13 11:27:30 +02:00
Mose Müller
80da96657c tasks: don't start another task when it is already running 2024-09-13 11:27:30 +02:00
Mose Müller
861e89f37a task: using functools to get correct func name 2024-09-13 11:27:30 +02:00
Mose Müller
c00cf9a6ff updating property dependencies in PropertyObserver
As Task objects have to be class attributes, I have to loop through class attributes, as well
when calculating nested observables properties.
2024-09-13 11:27:30 +02:00
Mose Müller
ed7f3d8509 dont make descriptors attributes of the instance -> would loose functionality 2024-09-13 11:27:30 +02:00
Mose Müller
456090fee9 adds is_descriptor helper method 2024-09-13 11:27:30 +02:00
Mose Müller
e69ef376ae replaces some code with helper function 2024-09-13 11:27:30 +02:00
Mose Müller
5f78771f66 tasks: need to bind method as soon as instance is passed to __get__
I cannot keep a reference to the parent class as the Task class is a DataService, as well.
2024-09-13 11:27:30 +02:00
Mose Müller
09ceae90ec tasks: only care about async methods right now 2024-09-13 11:27:30 +02:00
Mose Müller
c34351270c feat: first Task implementation 2024-09-13 11:27:29 +02:00
Mose Müller
743c18bdd7 fix: need to compare with serialized value (for enums) 2024-09-13 11:27:29 +02:00
Mose Müller
12d7ddab08 updates to version v0.9.1 2024-08-29 08:57:45 +02:00
Mose Müller
e40646c664
Merge pull request #153 from tiqi-group/feat/overwritable_sio_client_manager
adds overwritable sio client_manager
2024-08-29 08:56:58 +02:00
Mose Müller
ab9b4257f2 adds overwritable sio client_manager 2024-08-28 12:37:56 +02:00
Mose Müller
a2effca2b0 fixes ruff errors 2024-08-20 13:14:03 +02:00
Mose Müller
f76703340c
Merge pull request #156 from tiqi-group/docs
Updates Docs
2024-08-20 13:01:17 +02:00
Mose Müller
dbc1fa00f7 adds autogenerated api documentation 2024-08-20 12:03:08 +02:00
Mose Müller
4ecc1a191f renames main.md to README.md 2024-08-20 11:50:27 +02:00
Mose Müller
4f8e3f845c fixes relative links 2024-08-20 11:50:27 +02:00
Mose Müller
132856a8f0 updates mkdocstrings dependency (adds python extra)
updates requirements.txt
2024-08-20 11:50:27 +02:00
Mose Müller
b1f75bb786 makes handle_server_shutdown a protected method 2024-08-20 11:50:27 +02:00
Mose Müller
0011a0f92e fix: uses logger instead of logging in sio events 2024-08-20 08:30:13 +02:00
Mose Müller
b7ab364aab adds "testing" operation mode 2024-08-20 08:29:54 +02:00
Mose Müller
52e4647433
Merge pull request #155 from tiqi-group/docs
Updating Docs
2024-08-19 16:35:40 +02:00
Mose Müller
b2b3d426ed updates license 2024-08-19 16:11:26 +02:00
Mose Müller
7ae3ff504d reference link to license 2024-08-19 16:03:37 +02:00
Mose Müller
50f3686c12 moves "Understanding Units" to docs 2024-08-19 15:56:57 +02:00
Mose Müller
b0c3c4cad9 moves "Validating Property Setters" to docs 2024-08-19 15:52:08 +02:00
Mose Müller
9b8279da85 moving "Understanding Tasks" into docs 2024-08-19 15:41:19 +02:00
Mose Müller
97e21b2ea8 docs: more reference links 2024-08-19 15:34:09 +02:00
Mose Müller
fb75de5b51 adds service persistence page to mkdocs.yml 2024-08-19 15:19:46 +02:00
Mose Müller
3eb9c6476b replaces inline links with reference links (can be overwritten in docs) 2024-08-19 15:17:31 +02:00
Mose Müller
c7ec929d05 moves state persistence section into docs, restructuring docs 2024-08-19 14:45:56 +02:00
Mose Müller
ca19fcc63f updates Readme (moving components guide to docs, removing TOC, updated features list,...) 2024-08-19 14:18:28 +02:00
Mose Müller
7904d0d7d9 updates Readme introduction 2024-08-19 13:19:30 +02:00
Mose Müller
8526e74aa7
Merge pull request #154 from tiqi-group/fixci-github-release
CI: fixing github-release ci job
2024-08-19 10:08:12 +02:00
Mose Müller
6e16d84ba4 fixes python sigstore action 2024-08-19 10:01:33 +02:00
Mose Müller
6765246231 fixing ruff formatting error 2024-08-19 09:53:54 +02:00
Mose Müller
f50976358b
Fixes python-package workflow 2024-08-19 09:52:54 +02:00
Mose Müller
aa37fa8533
Removes ruff github action with explicit steps 2024-08-19 09:40:34 +02:00
Mose Müller
2ebdb77433
Merge pull request #152 from tiqi-group/feat/client_context_manager
feat: adds a context manager to the client, fixes running loop issue
2024-08-13 07:16:05 +02:00
Mose Müller
5ce30cfeaa updates Readme and docs 2024-08-13 07:14:33 +02:00
Mose Müller
82d6a7f895 adds a context manager to the client, fixes running loop issue 2024-08-13 07:14:33 +02:00
Mose Müller
9aad9dfbc6
Merge pull request #151 from tiqi-group/breaking/client_arguments
Breaking: changing allowed client arguments
2024-08-13 07:04:10 +02:00
Mose Müller
86bac8f9e5 updates version to v0.9.0 2024-08-13 07:03:49 +02:00
Mose Müller
348ff092aa updates Readme with client instructions 2024-08-12 14:21:13 +02:00
Mose Müller
1ac08bf97d fixes client test 2024-08-12 13:19:45 +02:00
Mose Müller
42357d7901 breaking: client takes url instead of hostname and port
Connecting to secure services (with wss) was not possible.
The user has to provide the whole URL now, which makes it much more flexible and less bug-prone.
2024-08-12 13:15:17 +02:00
Mose Müller
014a7b9492 updates ruff dependency 2024-08-05 09:36:32 +02:00
Mose Müller
e0d710644b sio_setup: removes unused function 2024-08-05 05:27:17 +02:00
Mose Müller
4a9dba30d7
Merge pull request #149 from tiqi-group/feat/allow_monkey_patching_api_endpoints
feat: allow monkey patching serilization methods for api endpoints
2024-07-31 09:48:29 +02:00
Mose Müller
9663dea79d feat: allow monkey patching serilization methods for api endpoints 2024-07-31 08:44:23 +02:00
Mose Müller
81e40860df moves frontend customization from Readme into docs 2024-07-30 11:27:57 +02:00
Mose Müller
9021e3a903 updates Readme 2024-07-30 11:19:07 +02:00
Mose Müller
2136d1a157 remove requests from dev dependencies 2024-07-30 11:17:35 +02:00
Mose Müller
c894215ddc
Merge pull request #147 from tiqi-group/feat/add_http_api_endpoints
Feat: add http API endpoints
2024-07-30 10:47:59 +02:00
Mose Müller
22d836587e udpates to version v0.8.5 2024-07-30 10:40:11 +02:00
Mose Müller
9e852c17ac docs: updates documentation 2024-07-30 10:38:56 +02:00
Mose Müller
bd6220cb9e chore: refactoring state_manager 2024-07-30 10:17:43 +02:00
Mose Müller
940f7039d3 reflecting changes in openapi.yaml 2024-07-30 10:15:40 +02:00
Mose Müller
d45d2dba7d updates api tests 2024-07-30 10:15:34 +02:00
Mose Müller
1fb296c3c1 removes read-only check from state manager's set_service_attribute_value_by_path 2024-07-30 10:15:34 +02:00
Mose Müller
bfe2d82c0b api: getting value from service instead of cache 2024-07-30 09:30:28 +02:00
Mose Müller
5d8471fd47 disallows clients to add class attributes (through the state manager)
Note that adding dictionary keys still works. You can also append to lists.
2024-07-30 09:18:22 +02:00
Mose Müller
75e355faf9 pytest: changes fixture scopes 2024-07-30 08:36:59 +02:00
Mose Müller
f91be30ad0 adds tests for http api endpoints 2024-07-30 08:28:37 +02:00
Mose Müller
b148d6919a StateManager: replaces _data_service_cache with cache_manager
- _data_service_cache -> cache_manager
2024-07-30 08:28:37 +02:00
Mose Müller
74ebbc6223 http api: replaces post endpoints with put endpoints 2024-07-30 08:28:37 +02:00
Mose Müller
554d6f7daa changes http API (reflected in openapi specification) 2024-07-30 08:28:37 +02:00
Mose Müller
80243487cb fixing image link 2024-07-30 08:28:37 +02:00
Mose Müller
aeaf57331e updates docs python requirements 2024-07-30 08:28:37 +02:00
Mose Müller
baad1268e8 updates documentation
- using material theme instead of readthedocs
- introducing "Interacting with pydase Services" guide
    - restful api docs
    - auto-generated frontend
    - pydase.Client
2024-07-30 08:28:37 +02:00
Mose Müller
9ce0c93954 adds swagger-ui-tag python dep to render swagger ui, updates mkdocs to include new page 2024-07-30 08:28:37 +02:00
Mose Müller
95d29ee4e8 return method results over http 2024-07-30 08:28:37 +02:00
Mose Müller
6f4fcf52dd adds user guide for restful api 2024-07-30 08:28:37 +02:00
Mose Müller
0e73239d08 adds API versioning 2024-07-30 08:28:37 +02:00
Mose Müller
e659ca9d1c adds requests to dev group 2024-07-30 08:28:37 +02:00
Mose Müller
eaf76a7211 fixing logging for aiohttp and SocketIOHandler 2024-07-30 08:28:37 +02:00
Mose Müller
aa55ac772e using api application as web server api endpoint 2024-07-30 08:28:37 +02:00
Mose Müller
755a303239 creates api definition, using that in sio_setup 2024-07-30 08:28:37 +02:00
Mose Müller
7e63f34c0a pytest: using asyncio session scope to get rid of more warnings 2024-07-30 08:28:18 +02:00
Mose Müller
2364fc892c pytest: removes warning (could not join thread) 2024-07-30 08:28:18 +02:00
Mose Müller
a1da332dba pytest: configures logging through caplog fixture 2024-07-30 08:28:07 +02:00
Mose Müller
8bf0b771fa replace deprecated object from pint package 2024-07-30 06:56:59 +02:00
Mose Müller
fd73653433
Merge pull request #148 from tiqi-group/124-adding-keys-to-dictionary-through-pydaseclient
feat: clients can add keys to dictionaries
2024-07-29 15:17:01 +02:00
Mose Müller
d09675de6a updates client test 2024-07-29 15:06:54 +02:00
Mose Müller
36d3a7becc restructure StateManager to allow extending dictionaries through clients 2024-07-29 15:02:47 +02:00
Mose Müller
d54eed8a58 get_object_by_path_parts and get_object_attr_from_path do not catch exceptions any more 2024-07-29 15:02:47 +02:00
Mose Müller
817afc610a StateManager: replaces _data_service_cache with cache_manager
- _data_service_cache -> cache_manager
- removes cache property
- replaces get_nested_dict_by_path with cache_manager.get_value_dict_from_cache where possible
2024-07-29 15:02:47 +02:00
Mose Müller
ad0f9420d9 get_value_dict_from_cache does not catch exceptions any more 2024-07-29 14:59:02 +02:00
Mose Müller
6d786cd0f8 removes unused SerializationValueError exception 2024-07-29 14:59:02 +02:00
Mose Müller
de4270daa4
Merge pull request #146 from tiqi-group/feat/replace_fastapi_with_aiohttp
Replace fastapi with aiohttp
2024-07-25 08:39:01 +02:00
Mose Müller
7286017715 removes unused imports 2024-07-25 08:28:33 +02:00
Mose Müller
1a23206f42 adds example to helper function 2024-07-25 08:25:28 +02:00
Mose Müller
fdb17e44e2 replaces fastapi with aiohttp 2024-07-25 08:25:10 +02:00
Mose Müller
fc738e2743 using aiohttp as socketio async_mode 2024-07-25 08:22:12 +02:00
Mose Müller
9de4071120 updates logging to remove uvicorn dependency 2024-07-25 07:48:22 +02:00
Mose Müller
369d0b1126 adds handle for server shutdown, removes uvicorn dependency from server.py 2024-07-25 07:47:59 +02:00
Mose Müller
c396de75fb frontend: default export useRenderCount hook 2024-07-24 15:06:36 +02:00
Mose Müller
4ed8899708
Merge pull request #145 from tiqi-group/11-frontend-user-should-be-able-to-change-the-order-of-the-elements-in-the-frontend
adds support for altering component display order
2024-07-24 15:02:22 +02:00
Mose Müller
2fa3505310 updates Readme with displayOrder customization 2024-07-24 14:58:56 +02:00
Mose Müller
9d387944ef npm run build 2024-07-24 14:58:47 +02:00
Mose Müller
0d70b7492d frontend: adds support for displayOrder in web settings 2024-07-24 14:53:40 +02:00
Mose Müller
7bc12b340f updates vscode debugging config 2024-07-24 13:41:48 +02:00
Mose Müller
e996966388
Merge pull request #144 from tiqi-group/feat/useLocalStorage_hook
Feat: useLocalStorage hook
2024-07-22 07:09:04 +02:00
Mose Müller
c07efe056b npm run build 2024-07-22 07:05:16 +02:00
Mose Müller
9e0adba8dc introduces useLocalStorage hook 2024-07-22 07:05:09 +02:00
Mose Müller
1789a6ad7e
Merge pull request #143 from tiqi-group/fix/logging
configures pydase's logger only (not root logger anymore)
2024-07-12 07:46:32 +02:00
Mose Müller
0e5f1ede20 fixes logging test 2024-07-12 07:45:11 +02:00
Mose Müller
f8cae28128 fix tests: need to propagate logger when testing (due to pytest) 2024-07-11 19:49:55 +02:00
Mose Müller
e31af9ae31 moves log_config out of setup_logging method to make it configurable, removes argument from function 2024-07-11 17:04:11 +02:00
Mose Müller
60c671eb0d configures pydase's logger (not root logger anymore) 2024-07-11 16:53:12 +02:00
Mose Müller
203059822c
Merge pull request #142 from tiqi-group/feat/allow_monkey_patching_sio_setup
Allow monkey-patching of serialization methods in socketio setup
2024-07-09 16:27:35 +02:00
Mose Müller
e7f9ad799c uses loads instead of Deserializer, allows for monkey-patching loads and dump 2024-07-09 16:25:35 +02:00
Mose Müller
3e5a56446f
Merge pull request #141 from tiqi-group/feat/serialize_datetime
Adds support for datetime serialization
2024-07-09 15:55:00 +02:00
Mose Müller
cf0780b2ca adds support for datetime serialization 2024-07-09 15:41:30 +02:00
Mose Müller
8afee54c51
Merge pull request #140 from tiqi-group/feat/make_serializer_methods_properly_overridable
serializer: converting staticmethods to classmethods
2024-07-09 15:19:53 +02:00
Mose Müller
6e4e000c28 serializer: converting staticmethods to classmethods
This helps developers that want to add functionality to the serializer.
2024-07-09 15:15:30 +02:00
Mose Müller
b719684702
Merge pull request #139 from tiqi-group/chore/remove_pillow_dependency
removes Pillow dependency, updates Image component
2024-07-09 08:42:02 +02:00
Mose Müller
7254482b35 updates pint to fix numpy dependency issue 2024-07-09 08:39:35 +02:00
Mose Müller
44d5a98449 removes Pillow dependency, updates Image component 2024-07-09 07:52:18 +02:00
Mose Müller
29558758af
Merge pull request #138 from tiqi-group/fix/unnecessary_component_rendering
Fixes unnecessary component rendering
2024-07-08 15:24:54 +02:00
Mose Müller
f9be97a910 npm run build 2024-07-08 15:16:32 +02:00
Mose Müller
fa45ee566b fixes eslint error 2024-07-08 15:16:13 +02:00
Mose Müller
6e8ad98282 frontend: updates when slider notifications are shown 2024-07-08 15:15:42 +02:00
Mose Müller
c42872aad4 moves functions from component to the outside (to not cause re-rendering) 2024-07-08 15:15:12 +02:00
Mose Müller
34eb4a0e7c frontend: introduces propsAreEqual function passed to React.memo to reduce re-rendering
This function accepts the component’s previous props, and its new props.
It should return true if the old and new props are equal: that is, if the component will
render the same output and behave in the same way with the new props as with the old.
I need to use this function as state objects that are passed as props will always have different references.
2024-07-08 15:15:12 +02:00
Mose Müller
7d50bd5759 frontend: cast type instead of ignoring typescript error 2024-07-08 15:11:05 +02:00
Mose Müller
c98f191d20 frontend: updates EnumComponent
- replaces type with SerializedEnum from types.ts
- passing props instead of attribute directly
2024-07-08 15:10:37 +02:00
Mose Müller
b1e6663c66 frontend: introduces useRenderCount hook
The useRenderCount hook contains all the necessary logic to count the re-render events.
This reduces duplication and code complexity.
2024-07-08 15:10:37 +02:00
Mose Müller
a5a957d290 using tseslint.config in eslint config (for types) 2024-07-08 08:58:04 +02:00
Mose Müller
b856ed3a12 using tsParser in eslint config 2024-07-08 08:51:01 +02:00
Mose Müller
b83e241b32
Merge pull request #137 from tiqi-group/chore/update_eslint
chore: update eslint config
2024-07-08 08:32:53 +02:00
Mose Müller
fb251649a0 updates eslint config, fixes linting errors 2024-07-08 08:30:55 +02:00
Mose Müller
a2ee8d02d6
Merge pull request #136 from tiqi-group/fix/allow_unserializable_objects_on_priv_attrs
Fix: allow unserializable objects on priv attrs
2024-07-04 17:43:32 +02:00
Mose Müller
44d73c3b77 adds function testing if private attributes can take values that are not serializable 2024-07-04 17:37:44 +02:00
Mose Müller
cddb83451a observer: first check if full access path contains private or protected attributes
As protected and private attributes are not stored in the cache, it does not make sense to
compare the cached value against the new value.
2024-07-04 17:26:32 +02:00
Mose Müller
218dab1ade
Merge pull request #135 from tiqi-group/chore/move_to_vite
Replace unmaintained create-react-app with vite
2024-07-04 17:01:18 +02:00
Mose Müller
81af62dc6e frontend: updates set of packages 2024-07-04 16:53:44 +02:00
Mose Müller
6ffb068f47 npm run build 2024-07-04 16:45:05 +02:00
Mose Müller
73a3283a7d feat: moving from react-create-app to vite
- loads of type fixes
- configuration changes
2024-07-04 16:45:00 +02:00
Mose Müller
c0734d58ce updates package-lock.json 2024-07-04 12:53:19 +02:00
Mose Müller
b5a7d90d81
Merge pull request #134 from tiqi-group/fix/frontend_instant_string_update
Fix: frontend instant string update
2024-07-04 12:50:42 +02:00
Mose Müller
b91eaaaf90 npm run build 2024-07-04 12:49:28 +02:00
Mose Müller
4039d29f42 fix: instant string update through frontend 2024-07-04 12:48:56 +02:00
Mose Müller
e8428e4a31
Merge pull request #133 from tiqi-group/81-use-web-storage-api-to-store-client-settings
Use web storage api to store client settings
2024-07-04 12:48:25 +02:00
Mose Müller
25459949a0 npm run build 2024-07-04 12:44:46 +02:00
Mose Müller
9649f914ac feat: persist isInstantUpdate and showNotification state changes to localStorage 2024-07-04 12:44:46 +02:00
Mose Müller
4ecc44fdd8 feat: persist state of Collapse components on the client using localStorage 2024-07-04 12:44:46 +02:00
Mose Müller
4cea7eeb59
Merge pull request #132 from tiqi-group/feat/proper_frontend_title
Feat/proper frontend title
2024-07-04 12:27:18 +02:00
Mose Müller
3c48a23277 fix: ruff warning 2024-07-04 12:23:58 +02:00
Mose Müller
bfcf72fec7 npm run build 2024-07-04 12:20:07 +02:00
Mose Müller
639161d373 feat: showing service class name in browser tab and on top of the frontend page 2024-07-04 12:19:19 +02:00
Mose Müller
6f3910efd0 docs: fixing typos 2024-05-28 13:17:58 +02:00
Mose Müller
fe5d0eed2d
Merge pull request #131 from tiqi-group/85-optionally-call-getter-after-setter
Adding validate_set decorator to ensure values are set correctly
2024-05-28 13:12:39 +02:00
Mose Müller
a11ab1520f updates version to v0.8.4 2024-05-28 13:12:15 +02:00
Mose Müller
ae79150252 adds tests for validate_set timeout 2024-05-28 13:08:01 +02:00
Mose Müller
7fdd08021a ignore mypy error 2024-05-28 12:56:43 +02:00
Mose Müller
00c6d4c068 adds validate_set decorator precision test 2024-05-28 12:08:23 +02:00
Mose Müller
f49cdd87e4 updates Readme 2024-05-28 11:45:43 +02:00
Mose Müller
052bf79487 adds setattr validation to observable if validate_set decorator is used 2024-05-28 11:22:18 +02:00
Mose Müller
203cc0f0f5 adds validate_set decorator 2024-05-28 11:22:18 +02:00
Mose Müller
0c54c9d4b7
Merge pull request #130 from tiqi-group/chore/update_workflow_action_versions
Chore/update workflow action versions
2024-05-27 15:35:48 +02:00
Mose Müller
381e73d624 using latest versions of github actions 2024-05-27 15:33:15 +02:00
Mose Müller
9f27f07ccb adds python 3.12 to python package checks 2024-05-27 15:33:01 +02:00
Mose Müller
94cef50e03 combines two lines in _ObservableList.append 2024-05-27 15:22:30 +02:00
Mose Müller
9fa8f06280
Merge pull request #127 from tiqi-group/feature/ignore_coroutine
Skip coroutines with arguments instead of raising an exception
2024-05-27 15:10:46 +02:00
Mose Müller
84abd63d56 Merge branch 'main' into feature/ignore_coroutine 2024-05-27 15:08:14 +02:00
Mose Müller
999a6016ff using __future__.annotations instead of quoted types 2024-05-27 14:51:49 +02:00
Mose Müller
19f91b7cf3 removes TaskDefinitionError 2024-05-27 14:42:54 +02:00
Mose Müller
a0b7b92898 fixes test 2024-05-27 14:42:30 +02:00
Mose Müller
d7e604992d updates wording and formatting 2024-05-27 14:42:26 +02:00
Mose Müller
2d1d228c78
Merge pull request #128 from tiqi-group/refactor/remove_unused_attribute_key_from_observers_dict
Refactor: remove unused attribute key from observers dict
2024-05-21 14:08:48 +02:00
Mose Müller
9c3c92361b updates tests 2024-05-21 14:03:25 +02:00
Mose Müller
ba9dbc03f1 removes attribute key from observers dict if list of observers is empty 2024-05-21 14:03:21 +02:00
Mose Müller
f783d0b25c
Merge pull request #126 from tiqi-group/fix/memory_leak
Fix memory leak in ObservableObject
2024-05-21 13:51:03 +02:00
Mose Müller
8285a37a4c updates version to v0.8.3 2024-05-21 13:43:13 +02:00
Mose Müller
6a894b6154 adds test for dict/list garbage collection 2024-05-21 13:42:25 +02:00
Mose Müller
f9a5352efe moves lines adding weakref to mapping dict into _initialise_new_objects
This groups together all the lines that add elements to or get elements from the mapping dicts.
2024-05-21 13:42:25 +02:00
Mose Müller
9c5d133d65 fixes types 2024-05-21 10:51:13 +02:00
Martin Stadler
eacd5bc6b1 Skip coroutines with arguments instead of raising an exception 2024-05-20 17:41:57 +02:00
Martin Stadler
314e89ba38 Use weak references in dict/list mappping to avoid memory leak 2024-05-20 17:25:35 +02:00
Mose Müller
46868743c7
Merge pull request #123 from tiqi-group/36-feat-add-support-for-dictionaries
feat: adds support for dictionaries
2024-04-30 15:48:16 +02:00
Mose Müller
8203e3a498 updates version to v0.8.2 2024-04-30 15:47:50 +02:00
Mose Müller
82b9c14af3 ignores mypy overrides error 2024-04-30 15:46:50 +02:00
Mose Müller
b209ad75bb fixes serializer types and test
pydase dicts can only have stringed keys. This is now reflected in the serializer, as well.
2024-04-30 15:46:39 +02:00
Mose Müller
88a630518b updates ProxyDict types, ignores mypy error 2024-04-30 15:42:48 +02:00
Mose Müller
ed80c92b1f adds dict test for pydase.Client
The pop
2024-04-30 15:33:56 +02:00
Mose Müller
36e30970c5 adds dict.pop to pydase.Client
The pop method removes the element in the dictionary on the server, but it will not
return anything. This is because pop will delete the element on the server, and returned
proxy classes will not be meaningful.
2024-04-30 15:24:15 +02:00
Mose Müller
3384d1bebf adds dict.pop method to ObservableDict 2024-04-30 13:15:42 +02:00
Mose Müller
e2f94c8a28 updates Readme (mentions dict support under standard data types) 2024-04-30 13:02:58 +02:00
Mose Müller
4d442cfadc adds ProxyDict to pydase client 2024-04-30 11:50:15 +02:00
Mose Müller
2701a995e1 updates test_helpers (replacing float key with dotted string key) 2024-04-30 11:48:05 +02:00
Mose Müller
47a73ad55f moves _ObservableDict tests into separate file 2024-04-30 11:47:22 +02:00
Mose Müller
ad4f926472 replaces ObservableDict key type warning with exception 2024-04-30 11:44:07 +02:00
Mose Müller
208dee2b92 dictionaries can only take strings now
The object serializations are passed through json.dumps before they are emitted to the
clients. JSON, apparently, can only handle keys of type string, which is why I have to
limit the dictionary key types to strings, as well.
2024-04-30 11:12:45 +02:00
Mose Müller
02b2d4fb10 observer: ignoring __annotations__ class attribute 2024-04-30 11:09:54 +02:00
Mose Müller
b2f59dd447 updates serializer type (dictionary object) 2024-04-30 10:51:52 +02:00
Mose Müller
33aa8708fd frontend: fixes displayName for dotted dictionary keys 2024-04-30 10:02:46 +02:00
Mose Müller
37d698a1b2 fixes web settings (displayName for dotted dictionary keys) 2024-04-30 10:02:13 +02:00
Mose Müller
8fa91e8121 adds tests for generate_serialized_data_paths and get_data_paths_from_serialized_object 2024-04-30 10:02:13 +02:00
Mose Müller
b9131c9df2 updates generate_serialized_data_paths to handle dictionaries well 2024-04-30 10:02:13 +02:00
Mose Müller
1c1584c2cf fixes dictionary serialization for keys that are not strings 2024-04-30 10:02:13 +02:00
Mose Müller
bb3d6fcce1 updates _ObservableDict
- allows for strings and numbers now
- key will have double quotes (") instead of single quote (') when key is a string
- fixed some few things
- added/updated tests
2024-04-30 10:02:13 +02:00
Mose Müller
e9a7e785dd npm run build 2024-04-30 10:02:13 +02:00
Mose Müller
a214d6d85a using id as form name for number and string component
This removes errors saying that quotes within element name are not allowed.
2024-04-30 10:02:13 +02:00
Mose Müller
6eaf1a03d1 adds onChange prop to number component form field to remove console errors 2024-04-29 15:20:11 +02:00
Mose Müller
31f1c9a8ce adds "None" type to AttributeType type 2024-04-29 15:08:12 +02:00
Mose Müller
02f1dba0f3 frontend: updates stateUtils 2024-04-29 15:05:48 +02:00
Mose Müller
dc40fc299f adds test for failing get_object_by_path_parts 2024-04-26 09:49:27 +02:00
Mose Müller
348f8aac9b removes tests for get_object_attr_from_path (uses get_object_by_path_parts internally) 2024-04-26 09:49:01 +02:00
Mose Müller
b314ae7dec updates helper tests 2024-04-26 09:42:57 +02:00
Mose Müller
25e578fbba parse_full_access_path can match floats inside brackets now 2024-04-26 09:42:26 +02:00
Mose Müller
1ee6a299b2 updates tests for is_property_attribute 2024-04-26 09:25:11 +02:00
Mose Müller
f315cd62d6 moves render_in_frontend function to decorators module, removes unused update_value_if_change function 2024-04-26 09:10:05 +02:00
Mose Müller
87d172b94b simplifies, documents and tests parse_serialized_key helper function 2024-04-26 08:15:21 +02:00
Mose Müller
a2c60a9c40 chore: replacing old method, adding TODO 2024-04-25 17:44:16 +02:00
Mose Müller
66376e2e6c removes parse_keyed_attribute 2024-04-25 17:40:33 +02:00
Mose Müller
d1c00a2612 using parse_full_access_path instead of path.split(".") in state manager 2024-04-25 17:34:48 +02:00
Mose Müller
6dd878a062 uses helper method in serializer 2024-04-25 17:33:42 +02:00
Mose Müller
2898b62b9c adds parse_serialized_key and get_object_by_path_parts helper methods 2024-04-25 17:33:32 +02:00
Mose Müller
b29c86ac2c updates Serializer functions
- using parse_full_access_path instead of parse_keyed_attribute
- renames get_next_level_dict_by_key to get_container_item_by_key
- replaces ensure_exists and get_nested_value by get_or_create_item_in_container

This allows us to handle access paths like "dict_attr['key'][0].some_attr".
2024-04-25 16:52:40 +02:00
Mose Müller
c75b203c3d creates functions to split full access paths and combine the atomic parts back together 2024-04-25 16:30:20 +02:00
Mose Müller
036e80b920 removes warning when using dict as attribute 2024-04-25 14:23:59 +02:00
Mose Müller
de7badd007 fixes exception message 2024-04-25 14:22:49 +02:00
Mose Müller
7e06944018 updates tests for get_next_level_dict_by_key 2024-04-25 10:52:53 +02:00
Mose Müller
4e9e1384df updates get_next_level_dict_by_key to handle lists and dictionaries 2024-04-25 10:52:36 +02:00
Mose Müller
5f7cc7f671 fixes type 2024-04-25 10:51:53 +02:00
Mose Müller
768be76cc8 replaces parseListAttrAndIndex with parseKeyedAttribute inn stateUtils 2024-04-23 14:35:22 +02:00
Mose Müller
8fd83fbd7d updates get_object_attr_from_path to support dictionaries 2024-04-23 14:21:39 +02:00
Mose Müller
564eeeb433 adds dictionary support to state_manager (__update_attribute_by_path) 2024-04-22 19:32:09 +02:00
Mose Müller
216368571a fixes parse_keyed_attributes 2024-04-22 19:31:29 +02:00
Mose Müller
2df1a673ac adds DictComponent to GenericComponent 2024-04-22 19:11:23 +02:00
Mose Müller
d40d9c5e47 adds first version of DictComponent 2024-04-22 19:11:13 +02:00
Mose Müller
6cae76bde1 adds tests for parse_keyed_attribute 2024-04-22 19:11:02 +02:00
Mose Müller
32e2a8a4d1 replaces parse_list_attr_and_index with parse_keyed_attribute to support dictionaries 2024-04-22 19:11:02 +02:00
Mose Müller
0ac4049282
Merge pull request #122 from tiqi-group/fix/ListComponent_item_key
Fix: list component item key
2024-04-22 18:39:51 +02:00
Mose Müller
d24c66e522 npm run build 2024-04-22 18:38:27 +02:00
Mose Müller
9ae6895858 replaces undefined name by full_access_path in ListComponent item 2024-04-22 18:38:03 +02:00
Mose Müller
2b8e25f5f1
Merge pull request #121 from tiqi-group/feat/add_async_method_status_spinner
Feat: adds async method status spinner
2024-04-22 17:49:58 +02:00
Mose Müller
9cfcb1ba0c npm run build 2024-04-22 17:47:14 +02:00
Mose Müller
a73e721b73 adds spinner to async task when waiting for backend status update 2024-04-22 17:46:58 +02:00
Mose Müller
503240aeae
Merge pull request #120 from tiqi-group/documentation/coloured_enum
adds note that coloured enum values must be unique
2024-04-17 11:49:45 +02:00
Mose Müller
ba24deecb7 adds note that coloured enum values must be unique 2024-04-17 11:47:43 +02:00
Mose Müller
5333acd583 updates version number 2024-04-17 09:34:30 +02:00
Mose Müller
81c05d2e14
Merge pull request #119 from tiqi-group/fix/component_display
Fix/component display
2024-04-17 09:28:32 +02:00
Mose Müller
8832c879a1 npm run build 2024-04-17 09:21:51 +02:00
Mose Müller
ec1f68ae4a using fullAccessPath as Form name for NumberComponent fixing cursor jumps 2024-04-17 09:21:02 +02:00
Mose Müller
f5e108bbe5 fixes readonly coloured enum 2024-04-17 09:11:33 +02:00
Mose Müller
dfe543067f fixes frontend button 2024-04-17 09:07:24 +02:00
Mose Müller
a77dcfdfae
Merge pull request #118 from tiqi-group/feat/sio_server_client
Feat/sio server client
2024-04-16 11:48:27 +02:00
Mose Müller
fe01ada733 adds tab completion test for client 2024-04-16 11:29:44 +02:00
Mose Müller
16c1f966ab adds test for dynamically added attribute 2024-04-16 11:15:42 +02:00
Mose Müller
003ee95272 replaces test_image test image url 2024-04-16 11:01:52 +02:00
Mose Müller
dfbf1c61af updates Readme (replaces rpyc with pydase.Client) 2024-04-16 10:55:54 +02:00
Mose Müller
7233e5933b updates client documentation 2024-04-16 10:41:18 +02:00
Mose Müller
09e66400c3 removes rpyc dependency 2024-04-16 10:21:25 +02:00
Mose Müller
6977b795e5 updates pydase.Server docstring 2024-04-16 10:21:25 +02:00
Mose Müller
8911b860d7 updates client list testing 2024-04-16 10:08:42 +02:00
Mose Müller
245b1844c9 adds update_value method to reduce code duplication 2024-04-16 10:08:42 +02:00
Mose Müller
d48ae9f5ad adds trigger_method function to reduce code duplication 2024-04-16 09:59:39 +02:00
Mose Müller
cf637d19ae adds docstring to ProxyClass 2024-04-16 09:49:00 +02:00
Mose Müller
edfb7d0341 Using super() in proxy class constructor 2024-04-16 09:36:34 +02:00
Mose Müller
7b06786307 updates pydase.Client documentation and constructor arguments 2024-04-16 09:34:29 +02:00
Mose Müller
5eeaefdd63 refactors client sio event setup 2024-04-15 08:16:46 +02:00
Mose Müller
f65a0e31c3 udpates client tests 2024-04-09 13:52:41 +02:00
Mose Müller
fbada6d818 adds ProxyList methods 2024-04-09 13:43:43 +02:00
Mose Müller
507f286963 adds blocking kwarg to client
If blocking is true, client init will wait until it could connect to the server.
2024-04-09 13:27:55 +02:00
Mose Müller
c148eba5dd updates client tests 2024-04-09 09:25:47 +02:00
Mose Müller
61c7dc8333 client retries to connect if server is not available. Connection process is not blocking anymore 2024-04-09 09:25:01 +02:00
Mose Müller
a879b09e0b updates version to 0.8.0 2024-04-09 09:25:01 +02:00
Mose Müller
bba21e3241 adds Client tests 2024-04-09 09:25:01 +02:00
Mose Müller
16bd17f75c adds deserializer tests 2024-04-09 09:25:01 +02:00
Mose Müller
ad2800aaf6 improves exception deserialization
Tries to use builtins exceptions if possible.
2024-04-08 11:13:14 +02:00
Mose Müller
d792601663 removes out-dated tests 2024-04-08 10:23:24 +02:00
Mose Müller
166fc57877 adds property observer test 2024-04-08 10:23:24 +02:00
Mose Müller
5b762db535 fixes detection of property dependencies for classes inheriting from other observables
Inheriting from a class that itself has defined properties, you cannot get those properties by calling
vars(type(obj)). Instead, you have to go through all the classes' members and check if they are properties.
You can either do this using dir(type(obj)) and get the members using getattr or just use inspect.getmembers.
2024-04-08 10:23:24 +02:00
Mose Müller
73b2355d35 fixes Client specific errors when setting proxy attributes / methods
Also ignores mypy errors
2024-04-08 10:23:24 +02:00
Mose Müller
6335ea21ad fixes warnings in ProxyClassMixin 2024-04-04 16:30:26 +02:00
Mose Müller
690ecd7317 adds aiohttp to python deps (used for socketio.AsyncClient) 2024-04-04 16:26:28 +02:00
Mose Müller
9cb667581a removes exposed dump method (circular import apparently)
fixes test
2024-04-04 16:26:28 +02:00
Mose Müller
5936e7091e updates sio events
- adds disconnect event which marks the DeviceConnection as disconnected
- updates connect event to notify the observer about the new state and set connected to True
2024-04-04 16:20:31 +02:00
Mose Müller
ad0fd8e833 updates proxy class usage
- ProxyClass class is inheriting from DeviceConnection and is only used for topmost proxy
- classes of nested proxy objects are dynamically created to keep their component types
- adds _initialise method to ProxyClassMixin as I cannot pass sio_client and loop to each
component_class (initialising a class with multiple base classes will pass the arguments passed to
the constructor to each initialiser function)
2024-04-04 16:19:29 +02:00
Mose Müller
473c6660e6 fixes warnings 2024-04-04 11:41:12 +02:00
Mose Müller
5511ebc808 updates client proxy
- will now be changed in place (instead of being overwritten on reconnect, which was the only way
of adding or removing property getters / setters)
- replaces getters/setters and methods of proxy with __setattr__ and __getattribute__ functionality
- replaces ProxyClassFactory with ProxyClass and ProxyLoader. The latter updates the former on
reconnect
- client does not need to be a DataService anymore. It only establishes the connection and holds
the reference to the proxy class.
2024-04-04 11:31:14 +02:00
Mose Müller
439665177d removes unused attribute of ProxyConnection 2024-04-03 10:54:03 +02:00
Mose Müller
c0b25c0581 adds Client to default exports of pydase 2024-04-03 10:47:46 +02:00
Mose Müller
60a7dda60a restructures client to have separate thread for its asyncio loop 2024-04-03 10:28:06 +02:00
Mose Müller
381d98b078 updates is_property_attribute to accept the full_access_path instead of the attr_name only 2024-03-29 08:47:24 +01:00
Mose Müller
658fb13d9d improves Client._notify_changed by not emitting sio events when properties change 2024-03-29 08:44:48 +01:00
Mose Müller
a582dc23ac makes proxyclass reconnection wait time a float to not get warning 2024-03-29 08:44:20 +01:00
Mose Müller
19b24f3060 avoids notifying server when updates are pushed from the server itself 2024-03-28 18:41:01 +01:00
Mose Müller
d100bb5fea udpates Client and ProxyClassFactory
- Client:
  - inherits from DataService now
  - acts as an observer of the proxy class and sends updates to the sio server
- ProxyClassFactory
  - ProxyConnection is now a DeviceConnection -> users will see if the client is connected
2024-03-28 18:41:01 +01:00
Mose Müller
36a70badce fixes observable _construct_extended_attr_path
Passing an empty string resulted in an extended path ending with a "."
2024-03-28 18:13:44 +01:00
Mose Müller
9916d6df60 adds support for dynamically adding attributes to DataService instances 2024-03-28 14:30:09 +01:00
Mose Müller
b4c84da57e npm run build 2024-03-28 11:31:12 +01:00
Mose Müller
ecf0e99318 fixes units test 2024-03-28 11:30:18 +01:00
Mose Müller
10ac007a0c ignores complexity errors 2024-03-28 11:30:07 +01:00
Mose Müller
900017791a fixes loading of removed attributes. Prints debug log instead of raising exception 2024-03-28 11:27:16 +01:00
Mose Müller
edb06b1612 restructures StateManager
- Updates logic of loading the state
- set_service_attribut_value_by_path expects serialized object instead of the value now
- uses loads instead of __convert_value_if_needed now
2024-03-28 10:21:28 +01:00
Mose Müller
bb5205b2e4 get_object_attr_from_path expects string instead of list now 2024-03-28 10:18:43 +01:00
Mose Müller
c02c75aab5 prevents users to override nested ProxyClasses in sio client proxy 2024-03-28 10:09:08 +01:00
Mose Müller
cc3fdfbb27 makes sio client private on ProxyClass 2024-03-28 09:48:25 +01:00
Mose Müller
7d399df158 proxy class will raise exception raised on server when setting value 2024-03-28 09:29:37 +01:00
Mose Müller
92e2c0e8ef fixes deserialization of floats 2024-03-28 08:57:59 +01:00
Mose Müller
65f63e08ae fixes changing Quantity from frontend 2024-03-28 08:55:58 +01:00
Mose Müller
4eddf4b980 todos 2024-03-27 17:50:51 +01:00
Mose Müller
9d7099f116 updates socket.ts (passing access_path to backend) 2024-03-27 17:49:57 +01:00
Mose Müller
3f096bda96 fixes loading enum from json file
Loading from json file happens by name. The sio client will send the whole
enumeration and thus we have to handle both strings and enumerations.
2024-03-27 17:30:37 +01:00
Mose Müller
e56a6e0653 fixing tests 2024-03-27 17:13:40 +01:00
Mose Müller
e71186dce4 updates types 2024-03-27 17:13:37 +01:00
Mose Müller
d1007fad14 removes unused code 2024-03-27 16:52:48 +01:00
Mose Müller
6f2c1f8951 exports dump function from pydase.utils.serialization 2024-03-27 16:37:06 +01:00
Mose Müller
f18880abd5 moves serializer tests into separate module 2024-03-27 16:31:08 +01:00
Mose Müller
9851ccfcdf moves serializer file into serialization module 2024-03-27 16:30:15 +01:00
Mose Müller
f312ec1e51 moving deserializer into serialization module 2024-03-27 16:26:46 +01:00
Mose Müller
7405d2cafc adds serialization module, moves types into separate file 2024-03-27 16:26:24 +01:00
Mose Müller
e6251975b8 adds try...except blocks around update_value and get_value sio events 2024-03-27 16:18:59 +01:00
Mose Müller
780a2466d3 fixes updating a value through sio client 2024-03-27 16:18:04 +01:00
Mose Müller
8979a1885e fixes method execution from frontend, adds simple serialization methods 2024-03-27 16:00:54 +01:00
Mose Müller
fbc4af28ae removes debugging statements 2024-03-27 16:00:24 +01:00
Mose Müller
454b0fb7d1 adds start and stop methods for tasks in socketio client 2024-03-27 15:32:51 +01:00
Mose Müller
9d3264de1f fixes cache update of task status change 2024-03-27 15:32:51 +01:00
Mose Müller
2d6c681690 improves SerializedObject type hint 2024-03-27 15:32:51 +01:00
Mose Müller
612e62d06b updates ProxyClassFactory (go through handled types before components) 2024-03-27 15:20:50 +01:00
Mose Müller
31f280c9cb frontend components pass actual readOnly and docString values to backend 2024-03-27 15:20:50 +01:00
Mose Müller
e4f5374783 fixes docstring when setting nested value by path 2024-03-27 15:20:50 +01:00
Mose Müller
6397307690 restructuring EnumComponent (now for both Enum and ColouredEnum) 2024-03-27 15:20:50 +01:00
Mose Müller
2ce4c9ce9b using new runMethod function 2024-03-27 15:20:50 +01:00
Mose Müller
15cf0bd414 adapting components to new callback function 2024-03-27 15:20:23 +01:00
Mose Müller
ff3a509132 passing fullAccessPath instead of parentPath and name 2024-03-27 15:20:23 +01:00
Mose Müller
1a01222cb3 updates changeCallback and SerializedObject in GenericComponent.tsx 2024-03-27 12:08:10 +01:00
Mose Müller
2eb996b382 updates frontend socket to use new sio events 2024-03-27 12:08:10 +01:00
Mose Müller
8addcd26aa fixes state manager enum handlign 2024-03-27 12:08:10 +01:00
Mose Müller
4db15f2fe8 updates sio events in web server 2024-03-27 12:08:10 +01:00
Mose Müller
27f22d472d updates Deserializer (handle components at last) 2024-03-27 12:06:14 +01:00
Mose Müller
c1aa678384 clients will now receive updates from socketio server and notify the observer 2024-03-27 12:06:14 +01:00
Mose Müller
11670addc4 replaces ClientDeserializer with ProxyClassFactory 2024-03-27 12:06:14 +01:00
Mose Müller
1c663e9a2e updates Deserializer (type hints, adding keyword to argument) 2024-03-27 12:06:14 +01:00
Mose Müller
ada9dcce4a adds websocket-client package 2024-03-27 12:06:14 +01:00
Mose Müller
bd5c162148 adds socketio client code 2024-03-27 12:06:14 +01:00
Mose Müller
4e1ec90dee adds Deserializer, converting SerializedObject objects back to actual objects 2024-03-27 12:06:14 +01:00
Mose Müller
4406acf4dd adds support for serializing exceptions 2024-03-27 12:06:14 +01:00
Mose Müller
1ad917a423 removes rpyc 2024-03-27 12:06:14 +01:00
Mose Müller
57e7deb552 Serializer adds full_access_path to serialized object representation 2024-03-26 10:52:06 +01:00
Mose Müller
d9ea33abb6 adds enum name to serialized object representation 2024-03-26 10:50:16 +01:00
Mose Müller
75c5bc6877 updates to version v0.7.4 2024-03-19 08:28:13 +01:00
Mose Müller
a606194c48
Merge pull request #116 from tiqi-group/feat/customisable_frontend_src
feat: adds option for custom frontend_src directory
2024-03-14 16:45:04 +01:00
Mose Müller
5da7bdea78 updates Readme 2024-03-14 16:43:59 +01:00
Mose Müller
c6a52914c5 adds option for custom frontend_src directory 2024-03-14 16:30:57 +01:00
Mose Müller
ae68a89f48
Merge pull request #115 from tiqi-group/feat/add_custom_css_default_response
feat: add "custom.css" endpoint default Response
2024-03-12 14:41:09 +01:00
Mose Müller
386e69b048 custom.css endpoint defaults to empty Response now 2024-03-12 14:37:12 +01:00
Mose Müller
8310a51a74
Merge pull request #113 from tiqi-group/feat/frontend_display_toggle
Feat: frontend display toggle
2024-03-12 07:40:50 +01:00
Mose Müller
2a8cbf7a4a updates Readme 2024-03-12 07:35:45 +01:00
Mose Müller
857b81d213 updates tests 2024-03-11 15:37:56 +01:00
Mose Müller
25834534ad npm run build 2024-03-11 15:37:56 +01:00
Mose Müller
4a948f9155 adds "display" web settings support to frontend
Components with a "display" equals false in the web settings will not be displayed
in the frontend.
2024-03-11 15:37:56 +01:00
Mose Müller
df42f41f53 adds "display" key in web settings 2024-03-11 15:37:56 +01:00
Mose Müller
b8d421eb90 fix: readonly value is not overwritten anymore when changing attribute type 2024-03-11 15:37:26 +01:00
Mose Müller
877ab42905 fixes webserver (apparently FastAPI need the correct type hints...) 2024-03-07 17:52:03 +01:00
Mose Müller
51ffd8be4d simplifies serializer logic 2024-03-06 18:56:15 +01:00
Mose Müller
a88a0c6133 Updates python dependencies 2024-03-06 18:28:11 +01:00
Mose Müller
390a375777
Merge pull request #111 from tiqi-group/refactor/updates_serialized_object_type_hints
updates type hints for serialized objects
2024-03-06 18:27:21 +01:00
Mose Müller
4aee899dbe updates type hints for serialized objects 2024-03-06 18:23:26 +01:00
Mose Müller
c7d452d7db adds tests for Image component 2024-03-05 16:32:20 +01:00
Mose Müller
b7926b730d updates version to v0.7.3 2024-03-05 16:32:07 +01:00
Mose Müller
0c175fc706
Merge pull request #109 from tiqi-group/fix/task_disappears_after_changing_state
Fix/task disappears after changing state
2024-03-05 16:08:55 +01:00
Mose Müller
7d21bca8b1 adds test for changing task state 2024-03-05 16:05:09 +01:00
Mose Müller
d1628ae8c9 fixes updating task state 2024-03-05 16:05:01 +01:00
Mose Müller
441658ebc1
Merge pull request #108 from tiqi-group/fix/cache_update_on_type_change
Fix/cache update on type change
2024-03-05 14:44:19 +01:00
Mose Müller
99c7ad0ec8 updates serializer tests 2024-03-05 14:28:53 +01:00
Mose Müller
24a01c0982 removes keys from cache entry if they are not part of the new value serialization 2024-03-05 14:17:05 +01:00
Mose Müller
b8a52c2e6a only update cache and execute notification callbacks if attribute is public and has changed 2024-03-05 13:56:02 +01:00
Mose Müller
7aacc21010 removes processing of value from sio_callback (cached value is up-to-date already) 2024-03-05 13:54:24 +01:00
Mose Müller
8787cb0509 get cached value before executing custom notification callbacks 2024-03-05 13:53:41 +01:00
Mose Müller
8971cebfcd adds todos 2024-03-05 13:24:54 +01:00
Mose Müller
f2cf0d9c1a fixes update of cache when the type has changed
When an attribute changes from, say, a quantity to an enumeration, the enum key in the serialization was not added to the
cache, and thus the frontend was not able to render the enum.
2024-03-05 13:23:26 +01:00
Mose Müller
36c863e845
Merge pull request #107 from tiqi-group/fix/update_frontend_before_setting_state
Fix/update frontend before setting state
2024-03-05 13:20:54 +01:00
Mose Müller
836c1e14df npm run build 2024-03-05 13:19:10 +01:00
Mose Müller
dba036c6b3 do not try to update state if it is not yet set
This happens when the backend pushes updates before the frontend has received and set the state when loading the page, first.
2024-03-05 13:19:02 +01:00
Mose Müller
8b1f1ef1b1 updates to version v0.7.2 2024-03-04 17:46:44 +01:00
Mose Müller
698db4881b
Merge pull request #106 from tiqi-group/fix/enum_sio_callback
fixes sio callback when attribute changes to an enum which was not present before
2024-03-04 17:38:33 +01:00
Mose Müller
d709d43d75 ignores complexity of sio_server setup (will be changed anyway soon 2024-03-04 17:36:09 +01:00
Mose Müller
691bf809cb fixes sio callback when attribute changes to an enum which was not present before 2024-03-04 17:32:45 +01:00
Mose Müller
86ccdd77f1 updates to version v0.7.1 2024-03-04 11:52:06 +01:00
Mose Müller
f29fb87054
Merge pull request #105 from tiqi-group/fix/enum_rendering
Fix/enum rendering
2024-03-04 11:51:31 +01:00
Mose Müller
cf5bc1e4e6 npm run build 2024-03-04 11:48:22 +01:00
Mose Müller
af36ed6c43 changes rendering of enums 2024-03-04 11:48:01 +01:00
Mose Müller
853472be94 updates enumValue when backend value changes 2024-03-04 11:47:51 +01:00
Mose Müller
f97a138e65 updates version to v0.7.0 2024-02-28 11:37:07 +01:00
Mose Müller
e5d7f4709f
Merge pull request #103 from tiqi-group/90-display-the-functions-its-names-differently-in-the-ui
feat: updates functions and how they are rendered
2024-02-28 11:28:04 +01:00
Mose Müller
416ae6f0b4 updates Adding_Components.md to account for new component structure 2024-02-28 11:15:37 +01:00
Mose Müller
8f0a9ad21a npm run build 2024-02-28 11:01:23 +01:00
Mose Müller
6ed6fe5be1 cleanup: changing some frontend components 2024-02-28 10:59:28 +01:00
Mose Müller
9c6323d38f updates Readme 2024-02-28 09:12:34 +01:00
Mose Müller
5c11202e08 removes print statement 2024-02-27 18:04:09 +01:00
Mose Müller
e551af68f9 adds image to Readme 2024-02-27 17:44:08 +01:00
Mose Müller
e213931cb7 npm run build 2024-02-27 17:41:55 +01:00
Mose Müller
fe29530eb6 updates Readme 2024-02-27 17:38:39 +01:00
Mose Müller
151467b36f fixes tests 2024-02-27 17:38:09 +01:00
Mose Müller
990add216c moves frontend decorator into decorators module 2024-02-27 17:35:35 +01:00
Mose Müller
a05b703bb8 adds tests for methods exposed by DataService 2024-02-27 16:38:08 +01:00
Mose Müller
9616c57c38 changes exception raised by @frontend decorator 2024-02-27 16:37:43 +01:00
Mose Müller
a7ce321506 updates / fixes method serialization tests 2024-02-27 16:32:47 +01:00
Mose Müller
a72a551f54 fixes tests for DataServiceCache and TaskManager 2024-02-27 16:19:11 +01:00
Mose Müller
26689d8578 updates AsyncMethodComponent to work with backend 2024-02-27 16:07:54 +01:00
Mose Müller
74fc5d9aab updates task serialization 2024-02-27 16:07:29 +01:00
Mose Müller
da8d07a8b2 frontend decorator uses helper function (function_has_arguments) now 2024-02-27 15:59:35 +01:00
Mose Müller
ca2182c19b tasks are not allowed to have arguments anymore 2024-02-27 15:59:35 +01:00
Mose Müller
b2f828ff6f adds function_has_arguments helper function 2024-02-27 15:30:47 +01:00
Mose Müller
affc63219f removes name from function signature parameter serialization 2024-02-27 14:35:09 +01:00
Mose Müller
a01cf273fe fixes render_in_frontend function 2024-02-27 12:58:43 +01:00
Mose Müller
acd0c80316 updated use of method components 2024-02-27 12:58:28 +01:00
Mose Müller
2337aa9d6d only methods without arguments can be rendered 2024-02-27 12:58:08 +01:00
Mose Müller
b6f6b3058e updates render_in_frontend method (takes async functions into account) 2024-02-27 11:32:18 +01:00
Mose Müller
d33e9f9dbf method serialization contains signature instead of parameter key-value pair 2024-02-27 11:30:00 +01:00
Mose Müller
53676131a6 replaces no_frontend decorator with "frontend" decorator 2024-02-27 11:28:42 +01:00
Mose Müller
7f407ae6e7 extracts method to get default value of function keyword argument 2024-02-27 09:20:22 +01:00
Mose Müller
3c2f425dee adds "no_frontend" decorator for emitting frontend rendering of method
The method serialization now contains a "frontend_render" key with boolean value.
2024-02-27 08:25:11 +01:00
Mose Müller
ccc53c395e adds "name" key-value pair to DataService serialization 2024-02-27 08:13:09 +01:00
Mose Müller
c672989768
Merge pull request #104 from tiqi-group/update/dependencies
Updates fastapi and uvicorn dependenciees
2024-02-26 09:41:37 +01:00
Mose Müller
5ff279d5bd Updates fastapi and uvicorn dependenciees 2024-02-26 09:37:24 +01:00
Mose Müller
883ec6d6ae updates MethodComponent
Keyword arguments have a default value now which is displayed in the frontend. The following types can be rendered now:
- numbers (ints, floats, quantities)
- enums (including coloured enums)

I still have to fix the `convert_argument_to_hinted_types` method to make Quantity and Enums work.
2024-02-21 16:30:47 +01:00
Mose Müller
22fd2d099d stores enum value within component - now usable within method form 2024-02-21 16:20:58 +01:00
Mose Müller
f8926ea823 prevents Enter key within StringComponent to submit form in MethodComponent 2024-02-21 16:09:28 +01:00
Mose Müller
ceed62c8f2 merges NumberInputField back into NumberComponent 2024-02-21 15:46:27 +01:00
Mose Müller
5313ef6e8c fixes StringComponent for use as method argument (adds name to control form) 2024-02-21 15:46:14 +01:00
Mose Müller
2d98ba51f4 moves displayName and id to GenericComponent and pass them as props 2024-02-21 15:45:37 +01:00
Mose Müller
2f2544b978 removes unnecessary props from button 2024-02-21 09:36:29 +01:00
Mose Müller
fffe679bf0 defines changeCallback function in GenericComponent and passes it to components (instead of setAttribute)
The components do not use the setAttribute method themselves anymore. This way, you can provide
the changeCallback function if you want and thus reuse the components.
2024-02-21 08:32:59 +01:00
Mose Müller
2bb02a5558 separating out NumberInputField from NumberComponent (to be used in MethodComponent) 2024-02-20 17:20:20 +01:00
Mose Müller
1c029e301b updates types 2024-02-20 16:39:06 +01:00
Mose Müller
f0384b817c updates method serialization 2024-02-20 14:49:35 +01:00
Mose Müller
8042f9b390 removes card header of root component 2024-02-20 14:49:35 +01:00
Mose Müller
838145a778 allows to use .env file to configure ServiceConfig 2024-02-20 12:54:04 +01:00
Mose Müller
7d753b2fc6
Merge pull request #102 from tiqi-group/fix/dynamic_list_entry_with_property
Fix: dynamic list entry with property
2024-02-20 12:53:08 +01:00
Mose Müller
72f6a8ddee ignores some ruff rule 2024-02-20 12:51:52 +01:00
Mose Müller
dfb6f966aa adds test for dynamic list entries with properties 2024-02-20 12:29:44 +01:00
Mose Müller
dc42bfaa9b removes changed_attribute path after on_change method 2024-02-20 12:29:30 +01:00
Mose Müller
c0ba23b0b2 appending to a list now also triggers _notify_change_start
This helps in understanding if the list entries being added are "changing" themselves. Properties within
the added objects will trigger property changes when they are serialized, so we have to tell the observer
that he should not listen to them.
2024-02-20 12:28:34 +01:00
Mose Müller
bd7a46ddc1 changes are only registered if the containing object is not being changed as a whole 2024-02-20 12:26:43 +01:00
Mose Müller
5bea0892c7
Merge pull request #94 from tiqi-group/92-add-connection-component
feat: adds device connection component
2024-02-15 09:24:17 +01:00
Mose Müller
9631a7d467 adds device connection image 2024-02-15 09:23:14 +01:00
Mose Müller
1e8c7bd141
Merge pull request #101 from tiqi-group/fix/ruff_config_for_2.0
fixes pyproject.toml ruff configuration
2024-02-15 09:11:15 +01:00
Mose Müller
10dc1436d0 fixes pyproject.toml ruff configuration 2024-02-15 09:08:16 +01:00
Mose Müller
551b8f0158 udpates ruff configuration 2024-02-15 09:01:53 +01:00
Mose Müller
25139b3d4d adds device connection test 2024-02-15 08:56:13 +01:00
Mose Müller
6b1227fcbb fixes mypy error 2024-02-15 08:43:08 +01:00
Mose Müller
fd3338f99f updates DeviceConnection Readme section 2024-02-15 08:33:39 +01:00
Mose Müller
c23d0372a5 updates DeviceConnection Readme section 2024-02-14 16:03:09 +01:00
Mose Müller
b646acc994 updates device connection component
DeviceConnection is not an ABC anymore. I have updated the docstring to highlight that the
user should mostly just override the "connect" method, but the "connected" property can also
be overridden if necessary. The user is not required though to override any of those methods
and thus can make use of the "connected" frontend property only.
2024-02-14 15:50:47 +01:00
Mose Müller
9b31362f5b moving device connection component out of module 2024-02-14 14:39:49 +01:00
Mose Müller
63edcffe7e adds DeviceConnection section to Readme 2024-02-01 13:33:22 +01:00
Mose Müller
8c5c6d0f6d npm run build 2024-02-01 13:33:22 +01:00
Mose Müller
71b84525dd updates DeviceConnection docstring 2024-02-01 13:33:22 +01:00
Mose Müller
e78dc2defb moves device_connection.py to device_connection module 2024-02-01 13:33:22 +01:00
Mose Müller
529d61c77d fixes DeviceConnection overlay message when directly exposed 2024-02-01 13:33:22 +01:00
Mose Müller
c7c88178d4 npm run build 2024-02-01 13:33:22 +01:00
Mose Müller
7f082b6f95 fixes border radius of DeviceComponent when directly exposed 2024-02-01 13:33:22 +01:00
Mose Müller
30138bcb45 renaming file containing DeviceConnection, updating component 2024-02-01 13:33:22 +01:00
Mose Müller
1318bbc8a8 update Readme (autostart code) 2024-02-01 13:33:22 +01:00
Mose Müller
ae9761bd11 adds docstring to DeviceConnection 2024-02-01 13:33:22 +01:00
Mose Müller
04d19a853f renaming available to connected 2024-02-01 13:33:22 +01:00
Mose Müller
fc28b83bc5 adds handle_connection autostart task to DeviceConnection 2024-02-01 13:33:22 +01:00
Mose Müller
f1384b25a1 updates DeviceConnection component 2024-02-01 13:33:22 +01:00
Mose Müller
7ef82e61e5 frontend styling 2024-02-01 13:33:22 +01:00
Mose Müller
6d9191fe18 npm run build 2024-02-01 13:33:22 +01:00
Mose Müller
4f71633c5e adds backend DeviceConnection component 2024-02-01 13:33:22 +01:00
Mose Müller
2c95a2496c adds frontend DeviceConnection component 2024-02-01 13:33:22 +01:00
Mose Müller
aca5aab1ef removes unused attribute 2024-02-01 13:25:53 +01:00
Mose Müller
4f1cc4787d
Merge pull request #99 from tiqi-group/cleanup/removes_deprecated_code
Cleanup/removes deprecated code
2024-02-01 11:11:43 +01:00
Mose Müller
8efd67d9f3 fixes tests 2024-02-01 10:18:58 +01:00
Mose Müller
34fc0f8739 removes deprecated code 2024-02-01 10:18:49 +01:00
Mose Müller
e60880fd30
Merge pull request #98 from tiqi-group/refactor/passing_full_serialization_dict_to_frontend
Refactor: passing full serialization dict to frontend
2024-02-01 09:27:29 +01:00
Mose Müller
036b0c681a updates version to v0.6.0 (due to breaking changes) 2024-02-01 09:25:47 +01:00
Mose Müller
dd268a4f9b npm run build 2024-02-01 09:18:24 +01:00
Mose Müller
e8638f1f3a fixes tests 2024-02-01 08:45:40 +01:00
Mose Müller
7279fed2aa frontend will can now display any serialization dict 2024-02-01 08:45:40 +01:00
Mose Müller
a2518671da DataService's serialize method now returns whole serialization dict (also passed to frontend) 2024-02-01 08:45:40 +01:00
Mose Müller
bcabd2dc48
Merge pull request #95 from tiqi-group/fix/service_configuration
Fix/service configuration
2024-01-29 15:26:27 +01:00
Mose Müller
7ac9c557c2 updates version to v0.5.2 2024-01-29 15:24:13 +01:00
Mose Müller
656529d1fb fixes service configuration (allow all environment variables) 2024-01-29 15:23:27 +01:00
Mose Müller
14601105a7
Merge pull request #93 from tiqi-group/45-placing-the-explanation-question-mark-next-to-the-variable-instead-of-above
feat: placing the explanation question mark next to the variable instead of above
2024-01-16 14:16:38 +01:00
Mose Müller
484b5131e9 fixing enum serialization for python 3.10 2024-01-16 14:13:36 +01:00
Mose Müller
616a5cea21 npm run build 2024-01-16 13:44:37 +01:00
Mose Müller
300bd6ca9a updates Enum serialization 2024-01-16 13:37:39 +01:00
Mose Müller
3e1517e905 udpates dev-guide for adding components 2024-01-16 13:00:01 +01:00
Mose Müller
0ecaeac3fb replaces js interfaces with types 2024-01-16 12:57:35 +01:00
Mose Müller
0e9832e2f1 updates DocStringComponent placement 2024-01-16 12:55:18 +01:00
Mose Müller
0343abd0b0
Merge pull request #91 from tiqi-group/fix/load_from_file
Fix/load from file
2024-01-09 16:39:59 +01:00
Mose Müller
0c149b85b5 updates version to v0.5.1 2024-01-09 16:39:12 +01:00
Mose Müller
0e331e58ff adds tests for server to check if loading from file is working 2024-01-09 16:36:35 +01:00
Mose Müller
45135927e6 initialises observer before loading state from json file 2024-01-09 16:21:57 +01:00
Mose Müller
d3866010a8 updates version to v0.5.0 2024-01-09 10:01:48 +01:00
Mose Müller
3c0f019af8
Merge pull request #48 from tiqi-group/10-frontend-user-should-be-able-to-add-custom-display-names
Feat: adds web settings file containing display name configuration
2024-01-08 17:17:06 +01:00
Mose Müller
8aa7fd31f8 updates Adding_Components guide 2024-01-08 17:11:55 +01:00
Mose Müller
c9ff3db9e9 Updates Readme 2024-01-08 16:57:46 +01:00
Mose Müller
9e77bae5e7 renaming config option from generate_new_web_settings to generate_web_settings 2024-01-08 16:49:36 +01:00
Mose Müller
6a6d1b27aa updates sio_setup tests (removes mock objects) 2024-01-08 16:35:32 +01:00
Mose Müller
2d3e7d8c1b adds web_server tests 2024-01-08 16:32:36 +01:00
Mose Müller
c7b039beb7 replaces method with read-only property 2024-01-08 16:32:25 +01:00
Mose Müller
62e647c667 generate_new_web_settings will now append to existing config file (not overwrite entries) 2024-01-08 15:45:02 +01:00
Mose Müller
6382be5735 removes index from generated web settings file (move to other PR) 2024-01-08 15:27:46 +01:00
Mose Müller
ea158bf8de adds sio_setup tests 2024-01-08 15:11:03 +01:00
Mose Müller
63ad6d7b93 removes web_settings sio event 2024-01-08 15:10:40 +01:00
Mose Müller
b8e758e479 updates docstring 2024-01-08 15:09:23 +01:00
Mose Müller
a12a708385 udpates Readme 2023-12-21 16:10:44 +01:00
Mose Müller
edb24f5439 Server uses ServiceConfig for web/rpc port default values, configurable through env variables 2023-12-21 15:48:28 +01:00
Mose Müller
2a2b7b800d updates ServiceConfig class 2023-12-21 15:48:28 +01:00
Mose Müller
b6b20c21e4 updates WebServer options to directly default to config class values 2023-12-21 15:25:57 +01:00
Mose Müller
53be794a3c renaming service configuration dir parameter 2023-12-21 13:36:08 +01:00
Mose Müller
a303ba7f0b adds pytest-asyncio to dev dependencies 2023-12-21 13:25:54 +01:00
Mose Müller
2461f85ef0 adds test for starting and stopping tasks 2023-12-21 13:24:54 +01:00
Mose Müller
ca41e12014 updates server to use asyncio.run 2023-12-21 13:13:45 +01:00
Mose Müller
f69723dd58 updates some tests to have a running event loop 2023-12-21 13:11:49 +01:00
Mose Müller
c733026522 fixes task manager loop 2023-12-21 13:11:17 +01:00
Mose Müller
316ce5c7e7 updates type hints 2023-12-21 11:33:00 +01:00
Mose Müller
43c3f746fa npm run build 2023-12-21 11:00:23 +01:00
Mose Müller
fea96c044c removes start_task wrapper 2023-12-21 11:00:19 +01:00
Mose Müller
6543bc6b39 rewrites web server to hot-reload the web settings from the settings file 2023-12-21 10:32:37 +01:00
Mose Müller
ef36c01407 updates serializer and state_manager to deal with serialized methods
I need to get the access path from methods when generating the
web_settings.json file. Thus, methods will not be skipped anymore,
instead, the method checking if the attribute is loadable makes the
distinction.
2023-12-21 10:31:02 +01:00
Mose Müller
9d90fd2b81 displayName of components is now taken from WebSettingsContext 2023-12-21 10:30:21 +01:00
Mose Müller
9fc6d6f910 updates WebSettings.tsx 2023-12-21 10:04:55 +01:00
Mose Müller
805e270107 updates sio_setup to not expect DataService in the parent path 2023-12-21 10:04:55 +01:00
Mose Müller
8e3a1694ce updates frontend components to not have DataService in the fullAccessPath 2023-12-21 10:03:17 +01:00
Mose Müller
32a1d14a40 changes display_name to displayName in web settings 2023-12-21 07:48:03 +01:00
Mose Müller
8940a61d4e adds WebSettings context 2023-12-21 07:48:03 +01:00
Mose Müller
393bde3280 frontend: removes unused stateRef 2023-12-20 16:57:28 +01:00
Mose Müller
eb2da1c5dc adds index to web_settings 2023-12-20 16:52:28 +01:00
Mose Müller
e7b73a99da WebServer uses serializer method now to generate serialized data paths 2023-12-20 10:21:48 +01:00
Mose Müller
392831e0fd uses new serializer method to check if attribute is loadable 2023-12-20 10:16:01 +01:00
Mose Müller
32bda8d910 updates generate_serialized_data_paths method, adds tests 2023-12-20 10:15:25 +01:00
Mose Müller
e106cc4927 adds NumberSlider to state manager tests 2023-12-20 10:14:35 +01:00
Mose Müller
464478cda9 removes helper function to create config folder 2023-12-20 10:14:07 +01:00
Mose Müller
97c026afe0 adds function to initialise web settings (also creating settings if requested), creates web-settings fastapi endpoint 2023-12-19 16:38:46 +01:00
Mose Müller
2f5c415cd5 updates webserver docstring 2023-12-19 16:21:03 +01:00
Mose Müller
728eea09f6 adds configs to WebServer (can also be passed to constructor) 2023-12-19 16:16:13 +01:00
Mose Müller
e3eaf5ffe2 adds ServiceConfig and WebServerConfig 2023-12-19 16:11:32 +01:00
Mose Müller
1dc3b62060 removes usage of rpyc-specific method in WebServer 2023-12-19 14:57:06 +01:00
Mose Müller
8214faf5cb removes ForkingServer rpyc configuration 2023-12-19 13:07:25 +01:00
Mose Müller
232eb53249 renames file 2023-12-19 12:59:18 +01:00
Mose Müller
439f514ea5 fixes WebServer 2023-12-19 12:58:32 +01:00
Mose Müller
c7d63f5139 replaces SioServerWrapper with setup function 2023-12-19 12:58:32 +01:00
Mose Müller
f64b5c35ab renaming sio_server file 2023-12-19 12:56:03 +01:00
Mose Müller
bb4de988e9 updates Server docstring 2023-12-19 11:44:50 +01:00
Mose Müller
36a8e916f6 updates kwargs passed to servers 2023-12-19 11:44:36 +01:00
Mose Müller
1a00f37372 fixes exception emission to web clients 2023-12-19 11:43:29 +01:00
Mose Müller
6630173cec fixes mypy issue 2023-12-19 11:43:29 +01:00
Mose Müller
08a62b2119 updates WebServer docstring 2023-12-19 11:43:29 +01:00
Mose Müller
37ae34ecc0 makes WebServer functions protected 2023-12-19 11:42:39 +01:00
Mose Müller
8b78099178 udpates AdditionalServerProtocol and WebServer
updates WebServer
2023-12-19 11:42:39 +01:00
Mose Müller
3186e04cc1 creates web_server module with WebServer complying with AdditionalServerProtocol 2023-12-19 10:59:24 +01:00
Mose Müller
055acbe591 using get_running_loop instead of soon-deprecated get_event_loop 2023-12-19 10:55:38 +01:00
Mose Müller
0d08c2ce0d removes unnecessary condition check 2023-12-19 10:55:07 +01:00
Mose Müller
68cc5b693e adds socketio event for web_settings 2023-12-18 12:04:33 +01:00
Mose Müller
4fcd5b4d44 adds helper function to create config folder 2023-12-18 12:04:31 +01:00
Mose Müller
9cbc639d0f updates vscode settings 2023-12-18 12:03:20 +01:00
Mose Müller
a48cce32e4 chore: formatting tests 2023-12-18 11:59:20 +01:00
Mose Müller
8c24f5dd67 updates version number 2023-12-13 11:29:28 +01:00
Mose Müller
1c4a878aa8
Merge pull request #86 from tiqi-group/9-add-units-support-for-numberslider
updates Readme explaining how to use units with number sliders
2023-12-13 11:25:58 +01:00
Mose Müller
31967d0d43 updates Readme explaining how to use units with number sliders 2023-12-13 11:23:44 +01:00
Mose Müller
b4edc31030
Merge pull request #84 from tiqi-group/75-numberslider-component-is-not-working
75 numberslider component is not working
2023-12-13 11:12:56 +01:00
Mose Müller
ff7c92547e updates Readme 2023-12-13 11:09:18 +01:00
Mose Müller
fab91f3221 updates number slider test file 2023-12-13 10:39:26 +01:00
Mose Müller
bd77995d96 npm run build 2023-12-13 10:36:00 +01:00
Mose Müller
729f375901 adds support for quantities in slider component (passing object instead of number) 2023-12-13 10:35:28 +01:00
Mose Müller
e643dd6f5c adds number object types to NumberComponent 2023-12-13 10:34:32 +01:00
Mose Müller
53f4cf6690 removes setters for min, max and step_size in NumberSlider, updates docstring 2023-12-13 09:30:21 +01:00
Mose Müller
c0c8591fc4 updates number slider component 2023-12-11 17:46:08 +01:00
Mose Müller
13fba6d3d6 npm run build 2023-12-11 17:30:12 +01:00
Mose Müller
dc4c9ff58f removes unused customEmitUpdate prop from NumberComponent 2023-12-11 17:30:12 +01:00
Mose Müller
83cd07feee updates SliderComponent to emit attribute updates (instead of full state dict) 2023-12-11 17:30:12 +01:00
Mose Müller
09f73a2b1d
Merge pull request #83 from tiqi-group/feat/improve_data_service_serialization
fixed serialization of class deriving from class which derives from DataService
2023-12-11 17:28:11 +01:00
Mose Müller
88886e3fd6 fixed serialization of class deriving from class which derives from DataService 2023-12-11 17:25:03 +01:00
181 changed files with 21292 additions and 24589 deletions

View File

@ -18,7 +18,10 @@ Provide steps to reproduce the behaviour, including a minimal code snippet (if a
## Expected behaviour
A clear and concise description of what you expected to happen.
## Screenshot/Video
## Actual behaviour
Describe what you see instead of the expected behaviour.
### Screenshot/Video
If applicable, add visual content that helps explain your problem.
## Additional context

View File

@ -22,7 +22,7 @@ jobs:
- name: Build a binary wheel and a source tarball
run: python3 -m build
- name: Store the distribution packages
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: python-package-distributions
path: dist/
@ -44,7 +44,7 @@ jobs:
steps:
- name: Download all the dists
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: python-package-distributions
path: dist/
@ -65,14 +65,14 @@ jobs:
steps:
- name: Download all the dists
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: python-package-distributions
path: dist/
- name: Sign the dists with Sigstore
uses: sigstore/gh-action-sigstore-python@v1.2.3
uses: sigstore/gh-action-sigstore-python@v3.0.0
with:
inputs: >-
inputs: |
./dist/*.tar.gz
./dist/*.whl
- name: Upload artifact signatures to GitHub Release
@ -85,27 +85,3 @@ jobs:
gh release upload
'${{ github.ref_name }}' dist/**
--repo '${{ github.repository }}'
# publish-to-testpypi:
# name: Publish Python 🐍 distribution 📦 to TestPyPI
# needs:
# - build
# runs-on: ubuntu-latest
#
# environment:
# name: testpypi
# url: https://test.pypi.org/p/pydase
#
# permissions:
# id-token: write # IMPORTANT: mandatory for trusted publishing
#
# steps:
# - name: Download all the dists
# uses: actions/download-artifact@v3
# with:
# name: python-package-distributions
# path: dist/
# - name: Publish distribution 📦 to TestPyPI
# uses: pypa/gh-action-pypi-publish@release/v1
# with:
# repository-url: https://test.pypi.org/legacy/

View File

@ -16,22 +16,25 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11"]
python-version: ["3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v3
- uses: chartboost/ruff-action@v1
with:
src: "./src"
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install poetry
poetry install --with dev
poetry install --with dev --all-extras
- name: Check with ruff
run: |
poetry run ruff check src
- name: Check formatting with ruff
run: |
poetry run ruff format --check src
- name: Test with pytest
run: |
poetry run pytest

View File

@ -2,6 +2,7 @@
"recommendations": [
"charliermarsh.ruff",
"ms-python.python",
"ms-python.vscode-pylance"
"ms-python.vscode-pylance",
"ms-python.mypy-type-checker"
]
}

4
.vscode/launch.json vendored
View File

@ -6,7 +6,7 @@
"type": "python",
"request": "launch",
"module": "foo",
"justMyCode": true,
"justMyCode": false,
"env": {
"ENVIRONMENT": "development"
}
@ -25,7 +25,7 @@
"type": "firefox",
"request": "launch",
"name": "react: firefox",
"url": "http://localhost:3000",
"url": "http://localhost:5173",
"webRoot": "${workspaceFolder}/frontend"
}
]

View File

@ -8,8 +8,8 @@
"editor.tabSize": 4,
"editor.detectIndentation": false,
"editor.codeActionsOnSave": {
"source.organizeImports": true,
"source.fixAll": true
"source.organizeImports": "explicit",
"source.fixAll": "explicit"
}
},
"[yaml]": {
@ -23,7 +23,7 @@
"editor.formatOnType": false,
"editor.formatOnSaveMode": "file",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
"source.fixAll.eslint": "explicit"
}
}
}

View File

@ -1,6 +1,4 @@
MIT License
Copyright (c) 2023 Mose Müller <mosemueller@gmail.com>
Copyright (c) 2023-2024 Mose Müller <mosemueller@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

568
README.md
View File

@ -1,55 +1,36 @@
# pydase (Python Data Service) <!-- omit from toc -->
<!--introduction-start-->
![pydase Banner](./docs/images/logo-with-text.png)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Documentation Status](https://readthedocs.org/projects/pydase/badge/?version=latest)](https://pydase.readthedocs.io/en/latest/?badge=latest)
[![Version](https://img.shields.io/pypi/v/pydase?style=flat)](https://pypi.org/project/pydase/)
[![Python Versions](https://img.shields.io/pypi/pyversions/pydase)](https://pypi.org/project/pydase/)
[![Documentation Status](https://readthedocs.org/projects/pydase/badge/?version=stable)](https://pydase.readthedocs.io/en/stable/)
[![License: MIT](https://img.shields.io/github/license/tiqi-group/pydase)][License]
`pydase` is a Python library for creating data service servers with integrated web and RPC servers. It's designed to handle the management of data structures, automated tasks, and callbacks, and provides built-in functionality for serving data over different protocols.
`pydase` is a Python library that simplifies the creation of remote control interfaces for Python objects. It exposes the public attributes of a user-defined class via a [Socket.IO](https://python-socketio.readthedocs.io/en/stable/) web server, ensuring they are always in sync with the service state. You can interact with these attributes using an RPC client, a RESTful API, or a web browser. The web browser frontend is auto-generated, displaying components that correspond to each public attribute of the class for direct interaction.
`pydase` implements an [observer pattern][Observer Pattern] to provide the real-time updates, ensuring that changes to the class attributes are reflected across all clients.
- [Features](#features)
- [Installation](#installation)
- [Usage](#usage)
- [Defining a DataService](#defining-a-dataservice)
- [Running the Server](#running-the-server)
- [Accessing the Web Interface](#accessing-the-web-interface)
- [Connecting to the Service using rpyc](#connecting-to-the-service-using-rpyc)
- [Understanding the Component System](#understanding-the-component-system)
- [Built-in Type and Enum Components](#built-in-type-and-enum-components)
- [Method Components](#method-components)
- [DataService Instances (Nested Classes)](#dataservice-instances-nested-classes)
- [Custom Components (`pydase.components`)](#custom-components-pydasecomponents)
- [`Image`](#image)
- [`NumberSlider`](#numberslider)
- [`ColouredEnum`](#colouredenum)
- [Extending with New Components](#extending-with-new-components)
- [Customizing Web Interface Style](#customizing-web-interface-style)
- [Understanding Service Persistence](#understanding-service-persistence)
- [Controlling Property State Loading with `@load_state`](#controlling-property-state-loading-with-load_state)
- [Understanding Tasks in pydase](#understanding-tasks-in-pydase)
- [Understanding Units in pydase](#understanding-units-in-pydase)
- [Changing the Log Level](#changing-the-log-level)
- [Documentation](#documentation)
- [Contributing](#contributing)
- [License](#license)
Whether you're managing lab sensors, network devices, or any abstract data entity, `pydase` facilitates service development and deployment.
## Features
<!-- no toc -->
- [Simple data service definition through class-based interface](#defining-a-dataService)
- [Integrated web interface for interactive access and control of your data service](#accessing-the-web-interface)
- [Support for `rpyc` connections, allowing for programmatic control and interaction with your service](#connecting-to-the-service-using-rpyc)
- [Component system bridging Python backend with frontend visual representation](#understanding-the-component-system)
- [Customizable styling for the web interface through user-defined CSS](#customizing-web-interface-style)
- [Saving and restoring the service state for service persistence](#understanding-service-persistence)
- [Automated task management with built-in start/stop controls and optional autostart](#understanding-tasks-in-pydase)
- [Support for units](#understanding-units-in-pydase)
<!-- * Event-based callback functionality for real-time updates
- Support for additional servers for specific use-cases -->
- [Simple service definition through class-based interface][Defining DataService]
- [Auto-generated web interface for interactive access and control of your service][Web Interface Access]
- [Python RPC client][Short RPC Client]
- [Customizable web interface][Customizing Web Interface]
- [Saving and restoring the service state][Service Persistence]
- [Automated task management with built-in start/stop controls and optional autostart][Task Management]
- [Support for units][Units]
- [Validating Property Setters][Property Validation]
<!--introduction-end-->
<!--getting-started-start-->
## Installation
<!--installation-start-->
Install pydase using [`poetry`](https://python-poetry.org/):
Install `pydase` using [`poetry`](https://python-poetry.org/):
```bash
poetry add pydase
@ -61,25 +42,27 @@ or `pip`:
pip install pydase
```
<!--installation-end-->
## Usage
<!--usage-start-->
Using `pydase` involves three main steps: defining a `DataService` subclass, running the server, and then connecting to the service either programmatically using `rpyc` or through the web interface.
Using `pydase` involves three main steps: defining a `pydase.DataService` subclass, running the server, and then connecting to the service either programmatically using `pydase.Client` or through the web interface.
### Defining a DataService
To use pydase, you'll first need to create a class that inherits from `DataService`. This class represents your custom data service, which will be exposed via RPC (using rpyc) and a web server. Your class can implement class / instance attributes and synchronous and asynchronous tasks.
To use `pydase`, you'll first need to create a class that inherits from `pydase.DataService`.
This class represents your custom service, which will be exposed via a web server.<br>
Your class can implement synchronous and asynchronous methods, some [built-in types](https://docs.python.org/3/library/stdtypes.html) (like `int`, `float`, `str`, `bool`, `list` or `dict`) and [other components][Custom Components] as attributes.
For more information, please refer to the [components guide][Components].
Here's an example:
```python
from pydase import DataService, Server
import pydase
from pydase.utils.decorators import frontend
class Device(DataService):
class Device(pydase.DataService):
_current = 0.0
_voltage = 0.0
_power = False
@ -114,6 +97,7 @@ class Device(DataService):
# run code to set power state
self._power = value
@frontend
def reset(self) -> None:
self.current = 0.0
self.voltage = 0.0
@ -121,26 +105,30 @@ class Device(DataService):
if __name__ == "__main__":
service = Device()
Server(service).run()
pydase.Server(service=service, web_port=8001).run()
```
In the above example, we define a Device class that extends DataService. We define a few properties (current, voltage, power) and their getter and setter methods.
In the above example, we define a `Device` class that inherits from `pydase.DataService`.
We define a few properties (current, voltage, power) and their getter and setter methods.
### Running the Server
Once your DataService is defined, you can create an instance of it and run the server:
Once your service class is defined, you can create an instance of it and run the server:
```python
from pydase import Server
import pydase
# ... defining the Device class ...
if __name__ == "__main__":
service = Device()
Server(service).run()
pydase.Server(service=service, web_port=8001).run()
```
This will start the server, making your Device service accessible via RPC and a web server at [http://localhost:8001](http://localhost:8001).
This will start the server, making your `Device` service accessible on
[http://localhost:8001](http://localhost:8001). The port number for the web server can
be customised in the server constructor or through environment variables and defaults
to `8001`.
### Accessing the Web Interface
@ -150,436 +138,136 @@ Once the server is running, you can access the web interface in a browser:
In this interface, you can interact with the properties of your `Device` service.
### Connecting to the Service using rpyc
### Connecting to the Service via Python RPC Client
You can also connect to the service using `rpyc`. Here's an example on how to establish a connection and interact with the service:
```python
import rpyc
# Connect to the service
conn = rpyc.connect("<ip_addr>", 18871)
client = conn.root
# Interact with the service
client.voltage = 5.0
print(client.voltage) # prints 5.0
```
In this example, replace `<ip_addr>` with the IP address of the machine where the service is running. After establishing a connection, you can interact with the service attributes as if they were local attributes.
<!--usage-end-->
## Understanding the Component System
<!-- Component User Guide Start -->
In `pydase`, components are fundamental building blocks that bridge the Python backend logic with frontend visual representation and interactions. This system can be understood based on the following categories:
### Built-in Type and Enum Components
`pydase` automatically maps standard Python data types to their corresponding frontend components:
- `str`: Translated into a `StringComponent` on the frontend.
- `int` and `float`: Manifested as the `NumberComponent`.
- `bool`: Rendered as a `ButtonComponent`.
- `list`: Each item displayed individually, named after the list attribute and its index.
- `enum.Enum`: Presented as an `EnumComponent`, facilitating dropdown selection.
### Method Components
Methods within the `DataService` class have frontend representations:
- Regular Methods: These are rendered as a `MethodComponent` in the frontend, allowing users to execute the method via an "execute" button.
- Asynchronous Methods: These are manifested as the `AsyncMethodComponent` with "start"/"stop" buttons to manage the execution of [tasks](#understanding-tasks-in-pydase).
### DataService Instances (Nested Classes)
Nested `DataService` instances offer an organized hierarchy for components, enabling richer applications. Each nested class might have its own attributes and methods, each mapped to a frontend component.
Here is an example:
```python
from pydase import DataService, Server
class Channel(DataService):
def __init__(self, channel_id: int) -> None:
self._channel_id = channel_id
self._current = 0.0
super().__init__()
@property
def current(self) -> float:
# run code to get current
result = self._current
return result
@current.setter
def current(self, value: float) -> None:
# run code to set current
self._current = value
class Device(DataService):
def __init__(self) -> None:
self.channels = [Channel(i) for i in range(2)]
super().__init__()
if __name__ == "__main__":
service = Device()
Server(service).run()
```
![Nested Classes App](docs/images/Nested_Class_App.png)
**Note** that defining classes within `DataService` classes is not supported (see [this issue](https://github.com/tiqi-group/pydase/issues/16)).
### Custom Components (`pydase.components`)
The custom components in `pydase` have two main parts:
- A **Python Component Class** in the backend, implementing the logic needed to set, update, and manage the component's state and data.
- A **Frontend React Component** that renders and manages user interaction in the browser.
Below are the components available in the `pydase.components` module, accompanied by their Python usage:
#### `Image`
This component provides a versatile interface for displaying images within the application. Users can update and manage images from various sources, including local paths, URLs, and even matplotlib figures.
The component offers methods to load images seamlessly, ensuring that visual content is easily integrated and displayed within the data service.
```python
import matplotlib.pyplot as plt
import numpy as np
import pydase
from pydase.components.image import Image
class MyDataService(pydase.DataService):
my_image = Image()
if __name__ == "__main__":
service = MyDataService()
# loading from local path
service.my_image.load_from_path("/your/image/path/")
# loading from a URL
service.my_image.load_from_url("https://cataas.com/cat")
# loading a matplotlib figure
fig = plt.figure()
x = np.linspace(0, 2 * np.pi)
plt.plot(x, np.sin(x))
plt.grid()
service.my_image.load_from_matplotlib_figure(fig)
pydase.Server(service).run()
```
![Image Component](docs/images/Image_component.png)
#### `NumberSlider`
This component provides an interactive slider interface for adjusting numerical values on the frontend. It supports both floats and integers. The values adjusted on the frontend are synchronized with the backend in real-time, ensuring consistent data representation.
The slider can be customized with initial values, minimum and maximum limits, and step sizes to fit various use cases.
You can connect to the service using the `pydase.Client`. Below is an example of how to establish a connection to a service and interact with it:
```python
import pydase
from pydase.components import NumberSlider
# Replace the hostname and port with the IP address and the port of the machine where
# the service is running, respectively
client_proxy = pydase.Client(url="ws://<ip_addr>:<web_port>").proxy
# client_proxy = pydase.Client(url="wss://your-domain.ch").proxy # if your service uses ssl-encryption
class MyService(pydase.DataService):
slider = NumberSlider(value=3.5, min=0, max=10, step_size=0.1, type="float")
if __name__ == "__main__":
service = MyService()
pydase.Server(service).run()
# After the connection, interact with the service attributes as if they were local
client_proxy.voltage = 5.0
print(client_proxy.voltage) # Expected output: 5.0
```
![Slider Component](docs/images/Slider_component.png)
This example demonstrates setting and retrieving the `voltage` attribute through the client proxy.
The proxy acts as a local representative of the remote service, enabling straightforward interaction.
#### `ColouredEnum`
The proxy class dynamically synchronizes with the server's exposed attributes. This synchronization allows the proxy to be automatically updated with any attributes or methods that the server exposes, essentially mirroring the server's API. This dynamic updating enables users to interact with the remote service as if they were working with a local object.
This component provides a way to visually represent different states or categories in a data service using colour-coded options. It behaves similarly to a standard `Enum`, but the values encode colours in a format understood by CSS. The colours can be defined using various methods like Hexadecimal, RGB, HSL, and more.
The RPC client also supports tab completion support in the interpreter, can be used as a context manager and integrates very well with other pydase services. For more information, please refer to the [documentation][Python RPC Client].
If the property associated with the `ColouredEnum` has a setter function, the keys of the enum will be rendered as a dropdown menu, allowing users to interact and select different options. Without a setter function, the selected key will simply be displayed as a coloured box with text inside, serving as a visual indicator.
### RESTful API
The `pydase` RESTful API allows for standard HTTP-based interactions and provides access to various functionalities through specific routes.
For example, you can get a value like this:
```python
import pydase
import pydase.components as pyc
import json
import requests
class MyStatus(pyc.ColouredEnum):
PENDING = "#FFA500" # Hexadecimal colour (Orange)
RUNNING = "#0000FF80" # Hexadecimal colour with transparency (Blue)
PAUSED = "rgb(169, 169, 169)" # RGB colour (Dark Gray)
RETRYING = "rgba(255, 255, 0, 0.3)" # RGB colour with transparency (Yellow)
COMPLETED = "hsl(120, 100%, 50%)" # HSL colour (Green)
FAILED = "hsla(0, 100%, 50%, 0.7)" # HSL colour with transparency (Red)
CANCELLED = "SlateGray" # Cross-browser colour name (Slate Gray)
class StatusTest(pydase.DataService):
_status = MyStatus.RUNNING
@property
def status(self) -> MyStatus:
return self._status
@status.setter
def status(self, value: MyStatus) -> None:
# do something ...
self._status = value
# Modifying or accessing the status value:
my_service = StatusExample()
my_service.status = MyStatus.FAILED
response = requests.get(
"http://<hostname>:<web_port>/api/v1/get_value?access_path=<full_access_path>"
)
serialized_value = json.loads(response.text)
```
![ColouredEnum Component](docs/images/ColouredEnum_component.png)
For more information, see [here][RESTful API].
#### Extending with New Components
<!--getting-started-end-->
Users can also extend the library by creating custom components. This involves defining the behavior on the Python backend and the visual representation on the frontend. For those looking to introduce new components, the [guide on adding components](https://pydase.readthedocs.io/en/latest/dev-guide/Adding_Components/) provides detailed steps on achieving this.
## Configuring pydase via Environment Variables
<!-- Component User Guide End -->
`pydase` services work out of the box without requiring any configuration. However, you
might want to change some options, such as the web server port or logging level. To
accommodate such customizations, `pydase` allows configuration through environment
variables, such as:
## Customizing Web Interface Style
- **`ENVIRONMENT`**:
Defines the operation mode (`"development"` or `"production"`), which influences
behaviour such as logging (see [Logging in pydase](#logging-in-pydase)).
`pydase` allows you to enhance the user experience by customizing the web interface's appearance. You can apply your own styles globally across the web interface by passing a custom CSS file to the server during initialization.
- **`SERVICE_CONFIG_DIR`**:
Specifies the directory for configuration files (e.g., `web_settings.json`). Defaults
to the `config` folder in the service root. Access this programmatically using:
Here's how you can use this feature:
```python
import pydase.config
pydase.config.ServiceConfig().config_dir
```
1. Prepare your custom CSS file with the desired styles.
- **`SERVICE_WEB_PORT`**:
Defines the web servers port. Ensure each service on the same host uses a unique
port. Default: `8001`.
2. When initializing your server, use the `css` parameter of the `Server` class to specify the path to your custom CSS file.
- **`GENERATE_WEB_SETTINGS`**:
When `true`, generates or updates the `web_settings.json` file. Existing entries are
preserved, and new entries are appended.
```python
from pydase import Server, DataService
For more information, see [Configuring pydase](https://pydase.readthedocs.io/en/stable/user-guide/Configuration/).
class Device(DataService):
# ... your service definition ...
## Customizing the Web Interface
if __name__ == "__main__":
service = MyService()
server = Server(service, css="path/to/your/custom.css").run()
```
`pydase` allows you to enhance the user experience by customizing the web interface's appearance through
This will apply the styles defined in `custom.css` to the web interface, allowing you to maintain branding consistency or improve visual accessibility.
1. a custom CSS file, and
2. a custom favicon image, and
3. tailoring the frontend component layout and display style.
Please ensure that the CSS file path is accessible from the server's running location. Relative or absolute paths can be used depending on your setup.
You can also provide a custom frontend source if you need even more flexibility.
## Understanding Service Persistence
`pydase` allows you to easily persist the state of your service by saving it to a file. This is especially useful when you want to maintain the service's state across different runs.
To save the state of your service, pass a `filename` keyword argument to the constructor of the `pydase.Server` class. If the file specified by `filename` does not exist, the state manager will create this file and store its state in it when the service is shut down. If the file already exists, the state manager will load the state from this file, setting the values of its attributes to the values stored in the file.
Here's an example:
```python
from pydase import DataService, Server
class Device(DataService):
# ... defining the Device class ...
if __name__ == "__main__":
service = Device()
Server(service, filename="device_state.json").run()
```
In this example, the state of the `Device` service will be saved to `device_state.json` when the service is shut down. If `device_state.json` exists when the server is started, the state manager will restore the state of the service from this file.
### Controlling Property State Loading with `@load_state`
By default, the state manager only restores values for public attributes of your service. If you have properties that you want to control the loading for, you can use the `@load_state` decorator on your property setters. This indicates to the state manager that the value of the property should be loaded from the state file.
Here is how you can apply the `@load_state` decorator:
```python
from pydase import DataService
from pydase.data_service.state_manager import load_state
class Device(DataService):
_name = "Default Device Name"
@property
def name(self) -> str:
return self._name
@name.setter
@load_state
def name(self, value: str) -> None:
self._name = value
```
With the `@load_state` decorator applied to the `name` property setter, the state manager will load and apply the `name` property's value from the file storing the state upon server startup, assuming it exists.
Note: If the service class structure has changed since the last time its state was saved, only the attributes and properties decorated with `@load_state` that have remained the same will be restored from the settings file.
## Understanding Tasks in pydase
In `pydase`, a task is defined as an asynchronous function contained in a class that inherits from `DataService`. These tasks usually contain a while loop and are designed to carry out periodic functions.
For example, a task might be used to periodically read sensor data, update a database, or perform any other recurring job. The core feature of `pydase` is its ability to automatically generate start and stop functions for these tasks. This allows you to control task execution via both the frontend and an `rpyc` client, giving you flexible and powerful control over your service's operation.
Another powerful feature of `pydase` is its ability to automatically start tasks upon initialization of the service. By specifying the tasks and their arguments in the `_autostart_tasks` dictionary in your service class's `__init__` method, `pydase` will automatically start these tasks when the server is started. Here's an example:
```python
from pydase import DataService, Server
class SensorService(DataService):
def __init__(self):
self.readout_frequency = 1.0
self._autostart_tasks = {"read_sensor_data": ()} # args passed to the function go there
super().__init__()
def _process_data(self, data: ...) -> None:
...
def _read_from_sensor(self) -> Any:
...
async def read_sensor_data(self):
while True:
data = self._read_from_sensor()
self._process_data(data) # Process the data as needed
await asyncio.sleep(self.readout_frequency)
if __name__ == "__main__":
service = SensorService()
Server(service).run()
```
In this example, `read_sensor_data` is a task that continuously reads data from a sensor. The readout frequency can be updated using the `readout_frequency` attribute.
By listing it in the `_autostart_tasks` dictionary, it will automatically start running when `Server(service).run()` is executed.
As with all tasks, `pydase` will also generate `start_read_sensor_data` and `stop_read_sensor_data` methods, which can be called to manually start and stop the data reading task.
## Understanding Units in pydase
`pydase` integrates with the [`pint`](https://pint.readthedocs.io/en/stable/) package to allow you to work with physical quantities within your service. This enables you to define attributes with units, making your service more expressive and ensuring consistency in the handling of physical quantities.
You can define quantities in your `DataService` subclass using `pydase`'s `units` functionality. These quantities can be set and accessed like regular attributes, and `pydase` will automatically handle the conversion between floats and quantities with units.
Here's an example:
```python
from typing import Any
from pydase import DataService, Server
import pydase.units as u
class ServiceClass(DataService):
voltage = 1.0 * u.units.V
_current: u.Quantity = 1.0 * u.units.mA
@property
def current(self) -> u.Quantity:
return self._current
@current.setter
def current(self, value: Any) -> None:
self._current = value
if __name__ == "__main__":
service = ServiceClass()
# You can just set floats to the Quantity objects. The DataService __setattr__ will
# automatically convert this
service.voltage = 10.0
service.current = 1.5
Server(service).run()
```
In the frontend, quantities are rendered as floats, with the unit displayed as additional text. This allows you to maintain a clear and consistent representation of physical quantities across both the backend and frontend of your service.
![Web interface with rendered units](./docs/images/Units_App.png)
Should you need to access the magnitude or the unit of a quantity, you can use the `.m` attribute or the `.u` attribute of the variable, respectively. For example, this could be necessary to set the periodicity of a task:
```python
import asyncio
from pydase import DataService, Server
import pydase.units as u
class ServiceClass(DataService):
readout_wait_time = 1.0 * u.units.ms
async def read_sensor_data(self):
while True:
print("Reading out sensor ...")
await asyncio.sleep(self.readout_wait_time.to("s").m)
if __name__ == "__main__":
service = ServiceClass()
Server(service).run()
```
For more information about what you can do with the units, please consult the documentation of [`pint`](https://pint.readthedocs.io/en/stable/).
For details, please see [here](https://pydase.readthedocs.io/en/stable/user-guide/interaction/main/#customization-options).
## Logging in pydase
The `pydase` library organizes its loggers on a per-module basis, mirroring the Python package hierarchy. This structured approach allows for granular control over logging levels and behaviour across different parts of the library.
The `pydase` library provides structured, per-module logging with support for log level configuration, rich formatting, and optional client identification in logs.
### Changing the Log Level
To configure logging in your own service, you can use:
You have two primary ways to adjust the log levels in `pydase`:
```python
from pydase.utils.logging import configure_logging_with_pydase_formatter
```
1. directly targeting `pydase` loggers
You can set the log level for any `pydase` logger directly in your code. This method is useful for fine-tuning logging levels for specific modules within `pydase`. For instance, if you want to change the log level of the main `pydase` logger or target a submodule like `pydase.data_service`, you can do so as follows:
```python
# <your_script.py>
import logging
# Set the log level for the main pydase logger
logging.getLogger("pydase").setLevel(logging.INFO)
# Optionally, target a specific submodule logger
# logging.getLogger("pydase.data_service").setLevel(logging.DEBUG)
# Your logger for the current script
logger = logging.getLogger(__name__)
logger.info("My info message.")
```
This approach allows for specific control over different parts of the `pydase` library, depending on your logging needs.
2. using the `ENVIRONMENT` environment variable
For a more global setting that affects the entire `pydase` library, you can utilize the `ENVIRONMENT` environment variable. Setting this variable to "production" will configure all `pydase` loggers to only log messages of level "INFO" and above, filtering out more verbose logging. This is particularly useful for production environments where excessive logging can be overwhelming or unnecessary.
```bash
ENVIRONMENT="production" python -m <module_using_pydase>
```
In the absence of this setting, the default behavior is to log everything of level "DEBUG" and above, suitable for development environments where more detailed logs are beneficial.
**Note**: It is recommended to avoid calling the `pydase.utils.logging.setup_logging` function directly, as this may result in duplicated logging messages.
For more information, see the [full guide](https://pydase.readthedocs.io/en/stable/user-guide/Logging/).
## Documentation
The full documentation provides more detailed information about `pydase`, including advanced usage examples, API references, and tips for troubleshooting common issues. See the [full documentation](https://pydase.readthedocs.io/en/latest/) for more information.
The full documentation provides more detailed information about `pydase`, including advanced usage examples, API references, and tips for troubleshooting common issues. See the [full documentation](https://pydase.readthedocs.io/en/stable/) for more information.
## Contributing
We welcome contributions! Please see [contributing.md](https://pydase.readthedocs.io/en/latest/about/contributing/) for details on how to contribute.
We welcome contributions! Please see [contributing.md](https://pydase.readthedocs.io/en/stable/about/contributing/) for details on how to contribute.
## Acknowledgements
This work was funded by the [ETH Zurich-PSI Quantum Computing Hub](https://www.psi.ch/en/lnq/qchub).
The main idea behind `pydase` is based on a previous project called `tiqi-plugin`, which
was developed within the same research group. While the concept was inspired by that
project, `pydase` was implemented from the ground up with a new architecture and design.
## License
`pydase` is licensed under the [MIT License](https://github.com/tiqi-group/pydase/blob/main/LICENSE).
`pydase` is licensed under the [MIT License][License].
[pydase Banner]: ./docs/images/logo-with-text.png
[License]: ./LICENSE
[Observer Pattern]: https://pydase.readthedocs.io/en/docs/dev-guide/Observer_Pattern_Implementation/
[Service Persistence]: https://pydase.readthedocs.io/en/stable/user-guide/Service_Persistence
[Defining DataService]: #defining-a-dataService
[Web Interface Access]: #accessing-the-web-interface
[Short RPC Client]: #connecting-to-the-service-via-python-rpc-client
[Customizing Web Interface]: #customizing-the-web-interface
[Task Management]: https://pydase.readthedocs.io/en/stable/user-guide/Tasks/
[Units]: https://pydase.readthedocs.io/en/stable/user-guide/Understanding-Units/
[Property Validation]: https://pydase.readthedocs.io/en/stable/user-guide/Validating-Property-Setters/
[Custom Components]: https://pydase.readthedocs.io/en/stable/user-guide/Components/#custom-components-pydasecomponents
[Components]: https://pydase.readthedocs.io/en/stable/user-guide/Components/
[RESTful API]: https://pydase.readthedocs.io/en/stable/user-guide/interaction/main/#restful-api
[Python RPC Client]: https://pydase.readthedocs.io/en/stable/user-guide/interaction/main/#python-rpc-client

View File

@ -18,7 +18,7 @@ For example, for a `Image` component, create a file named `image.py`.
### Step 2: Define the Backend Class
Within the newly created file, define a Python class representing the component. This class should inherit from `DataService` and contains the attributes that the frontend needs to render the component. Every public attribute defined in this class will synchronise across the clients. It can also contain methods which can be used to interact with the component from the backend.
Within the newly created file, define a Python class representing the component. This class should inherit from `DataService` and contains the attributes that the frontend needs to render the component. Every public attribute defined in this class will synchronise across the clients. It can also contain (public) methods which you can provide for the user to interact with the component from the backend (or python clients).
For the `Image` component, the class may look like this:
@ -31,21 +31,25 @@ from pydase.data_service.data_service import DataService
class Image(DataService):
def __init__(
self,
image_representation: bytes = b"",
) -> None:
self.image_representation = image_representation
super().__init__()
self._value: str = ""
self._format: str = ""
# need to decode the bytes
def __setattr__(self, __name: str, __value: Any) -> None:
if __name == "value":
if isinstance(__value, bytes):
__value = __value.decode()
return super().__setattr__(__name, __value)
@property
def value(self) -> str:
return self._value
@property
def format(self) -> str:
return self._format
def load_from_path(self, path: Path | str) -> None:
# changing self._value and self._format
...
```
So, changing the `image_representation` will push the updated value to the browsers connected to the service.
So, calling `load_from_path` will push the updated value and format to the browsers clients connected to the service.
### Step 3: Register the Backend Class
@ -85,10 +89,11 @@ def test_Image(capsys: CaptureFixture) -> None:
class ServiceClass(DataService):
image = Image()
service = ServiceClass()
# ...
```
service_instance = ServiceClass()
service_instance.image.load_from_path("<path/to/image>.png")
assert service_instance.image.format == "PNG"
```
## Adding a Frontend Component to `pydase`
@ -106,35 +111,42 @@ Write the React component code, following the structure and patterns used in exi
For example, for the `Image` component, a template could look like this:
```tsx
import { setAttribute, runMethod } from '../socket'; // use this when your component should sets values of attributes
// or runs a method, respectively
import { DocStringComponent } from './DocStringComponent';
```ts
import React, { useEffect, useRef, useState } from 'react';
import { Card, Collapse, Image } from 'react-bootstrap';
import { DocStringComponent } from './DocStringComponent';
import { ChevronDown, ChevronRight } from 'react-bootstrap-icons';
import { getIdFromFullAccessPath } from '../utils/stringUtils';
import { LevelName } from './NotificationsComponent';
interface ImageComponentProps {
name: string;
parentPath: string;
readOnly: boolean;
docString: string;
type ImageComponentProps = {
name: string; // needed to create the fullAccessPath
parentPath: string; // needed to create the fullAccessPath
readOnly: boolean; // component changable through frontend?
docString: string; // contains docstring of your component
displayName: string; // name defined in the web_settings.json
id: string; // unique identifier - created from fullAccessPath
addNotification: (message: string, levelname?: LevelName) => void;
// Define your component specific props here
changeCallback?: ( // function used to communicate changes to the backend
value: unknown,
attributeName?: string,
prefix?: string,
callback?: (ack: unknown) => void
) => void;
// component-specific properties
value: string;
format: string;
}
};
export const ImageComponent = React.memo((props: ImageComponentProps) => {
const { name, parentPath, value, docString, format, addNotification } = props;
const { value, docString, format, addNotification, displayName, id } = props;
const renderCount = useRef(0);
const [open, setOpen] = useState(true); // add this if you want to expand/collapse your component
const fullAccessPath = parentPath.concat('.' + name);
const id = getIdFromFullAccessPath(fullAccessPath);
const fullAccessPath = [props.parentPath, props.name]
.filter((element) => element)
.join('.');
// Your component logic here
useEffect(() => {
renderCount.current++;
@ -142,13 +154,11 @@ export const ImageComponent = React.memo((props: ImageComponentProps) => {
// This will trigger a notification if notifications are enabled.
useEffect(() => {
addNotification(`${parentPath}.${name} changed to ${value}.`);
addNotification(`${fullAccessPath} changed.`);
}, [props.value]);
// Your component logic here
return (
<div className={'imageComponent'} id={id}>
<div className="component imageComponent" id={id}>
{/* Add the Card and Collapse components here if you want to be able to expand and
collapse your component. */}
<Card>
@ -156,14 +166,15 @@ export const ImageComponent = React.memo((props: ImageComponentProps) => {
onClick={() => setOpen(!open)}
style={{ cursor: 'pointer' }} // Change cursor style on hover
>
{name} {open ? <ChevronDown /> : <ChevronRight />}
{displayName}
<DocStringComponent docString={docString} />
{open ? <ChevronDown /> : <ChevronRight />}
</Card.Header>
<Collapse in={open}>
<Card.Body>
{process.env.NODE_ENV === 'development' && (
<p>Render count: {renderCount.current}</p>
)}
<DocStringComponent docString={docString} />
{/* Your component TSX here */}
</Card.Body>
</Collapse>
@ -175,56 +186,97 @@ export const ImageComponent = React.memo((props: ImageComponentProps) => {
### Step 3: Emitting Updates to the Backend
React components in the frontend often need to send updates to the backend, particularly when user interactions modify the component's state or data. In `pydase`, we use `socketio` for smooth communication of these changes. To handle updates, we primarily use two events: `setAttribute` for updating attributes, and `runMethod` for executing backend methods. Below is a detailed guide on how to emit these events from your frontend component:
React components in the frontend often need to send updates to the backend, particularly when user interactions modify the component's state or data. In `pydase`, we use `socketio` for communicating these changes.<br>
There are two different events a component might want to trigger: updating an attribute or triggering a method. Below is a guide on how to emit these events from your frontend component:
1. **Setup for emitting events**:
First, ensure you've imported the necessary functions from the `socket` module for both updating attributes and executing methods:
1. **Updating Attributes**
```tsx
import { setAttribute, runMethod } from '../socket';
```
Updating the value of an attribute or property in the backend is a very common requirement. However, we want to define components in a reusable way, i.e. they can be linked to the backend but also be used without emitting change events.<br>
This is why we pass a `changeCallback` function as a prop to the component which it can use to communicate changes. If no function is passed, the component can be used in forms, for example.
2. **Event Parameters**:
The `changeCallback` function takes the following arguments:
- When using **`setAttribute`**, we send three main pieces of data:
- `name`: The name of the attribute within the `DataService` instance to update.
- `parentPath`: The access path for the parent object of the attribute to be updated.
- `value`: The new value for the attribute, which must match the backend attribute type.
- For **`runMethod`**, the parameters are slightly different:
- `name`: The name of the method to be executed in the backend.
- `parentPath`: Similar to `setAttribute`, it's the access path to the object containing the method.
- `kwargs`: A dictionary of keyword arguments that the method requires.
3. **Implementation**:
For illustation, take the `ButtonComponent`. When the button state changes, we want to send this update to the backend:
```tsx
import { setAttribute } from '../socket';
// ... (other imports)
- `value`: the new value for the attribute, which must match the backend attribute type.
- `attributeName`: the name of the attribute within the `DataService` instance to update. Defaults to the `name` prop of the component.
- `prefix`: the access path for the parent object of the attribute to be updated. Defaults to the `parentPath` prop of the component.
- `callback`: the function that will be called when the server sends an acknowledgement. Defaults to `undefined`
For illustration, take the `ButtonComponent`. When the button state changes, we want to send this update to the backend:
```ts title="frontend/src/components/ButtonComponent.tsx"
// ... (import statements)
type ButtonComponentProps = {
// ...
changeCallback?: (
value: unknown,
attributeName?: string,
prefix?: string,
callback?: (ack: unknown) => void
) => void;
};
export const ButtonComponent = React.memo((props: ButtonComponentProps) => {
// ...
const { name, parentPath, value } = props;
const {
// ...
changeCallback = () => {},
} = props;
const setChecked = (checked: boolean) => {
setAttribute(name, parentPath, checked);
changeCallback(checked);
};
return (
<ToggleButton
checked={value}
value={parentPath}
// ... other props
onChange={(e) => setChecked(e.currentTarget.checked)}>
<p>{name}</p>
{/* component TSX */}
</ToggleButton>
);
});
```
In this example, whenever the button's checked state changes (`onChange` event), we invoke the `setChecked` method, which in turn emits the new state to the backend using `setAttribute`.
In this example, whenever the button's checked state changes (`onChange` event), we invoke the `setChecked` method, which in turn emits the new state to the backend using `changeCallback`.
2. **Triggering Methods**
To trigger method through your component, you can either use the `MethodComponent` (which will render a button in the frontend), or use the low-level `runMethod` function. Its parameters are slightly different to the `changeCallback` function:
- `name`: the name of the method to be executed in the backend.
- `parentPath`: the access path to the object containing the method.
- `kwargs`: a dictionary of keyword arguments that the method requires.
To see how to use the `MethodComponent` in your component, have a look at the `DeviceConnection.tsx` file. Here is an example that demonstrates the usage of the `runMethod` function (also, have a look at the `MethodComponent.tsx` file):
```ts title="frontend/src/components/_YourComponent_.tsx"
import { runMethod } from '../socket';
// ... (other imports)
type ComponentProps = {
name: string;
parentPath: string;
// ...
};
export const Component = React.memo((props: ComponentProps) => {
const {
name,
parentPath,
// ...
} = props;
// ...
const someFunction = () => {
// ...
runMethod(name, parentPath, {});
};
return (
{/* component TSX */}
);
});
```
### Step 4: Add the New Component to the GenericComponent
@ -234,9 +286,7 @@ The `GenericComponent` is responsible for rendering different types of component
At the beginning of the `GenericComponent` file, import the newly created `ImageComponent`:
```tsx
// file: frontend/src/components/GenericComponent.tsx
```ts title="frontend/src/components/GenericComponent.tsx"
import { ImageComponent } from './ImageComponent';
```
@ -246,7 +296,7 @@ Update the `AttributeType` type definition to include the new type for the `Imag
For example, if the new attribute type is `'Image'` (which should correspond to the name of the backend component class), you can add it to the union:
```tsx
```ts
type AttributeType =
| 'str'
| 'bool'
@ -265,21 +315,23 @@ type AttributeType =
Inside the `GenericComponent` function, add a new conditional branch to render the `ImageComponent` when the attribute type is `'Image'`:
```tsx
```ts
} else if (attribute.type === 'Image') {
return (
<ImageComponent
name={name}
parentPath={parentPath}
readOnly={attribute.readonly}
docString={attribute.doc}
docString={attribute.value['value'].doc}
displayName={displayName}
id={id}
addNotification={addNotification}
changeCallback={changeCallback}
// Add any other specific props for the ImageComponent here
value={attribute.value['value']['value'] as string}
format={attribute.value['format']['value'] as string}
/>
);
} else {
} else if (...) {
// other code
```
@ -293,13 +345,15 @@ For example, updating an `Image` component corresponds to setting a very long st
To create a custom notification message, you can update the message passed to the `addNotification` method in the `useEffect` hook in the component file file. For the `ImageComponent`, this could look like this:
```tsx
```ts
const fullAccessPath = [parentPath, name].filter((element) => element).join('.');
useEffect(() => {
addNotification(`${parentPath}.${name} changed.`);
addNotification(`${fullAccessPath} changed.`);
}, [props.value]);
```
However, you might want to use the `addNotification` at different places. For an example, see the [MethodComponent](../../frontend/src/components/MethodComponent.tsx).
However, you might want to use the `addNotification` at different places. For an example, see the `MethodComponent`.
**Note**: you can specify the notification level by passing a string of type `LevelName` (one of 'CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG'). The default value is 'DEBUG'.
### Step 6: Write Tests for the Component (TODO)

View File

@ -2,7 +2,7 @@
## Overview
The Observer Pattern is a fundamental design pattern in the `pydase` package, serving as the central communication mechanism for state updates to clients connected to a service.
The [Observer Pattern](https://en.wikipedia.org/wiki/Observer_pattern) is a fundamental design pattern in the `pydase` package, serving as the central communication mechanism for state updates to clients connected to a service.
## How it Works

View File

@ -0,0 +1,57 @@
::: pydase.data_service
handler: python
::: pydase.data_service.data_service_cache
handler: python
::: pydase.data_service.data_service_observer
handler: python
::: pydase.data_service.state_manager
handler: python
::: pydase.server.server
handler: python
::: pydase.server.web_server
handler: python
::: pydase.client
handler: python
::: pydase.components
handler: python
::: pydase.task
handler: python
options:
inherited_members: false
show_submodules: true
::: pydase.utils.serialization.serializer
handler: python
::: pydase.utils.serialization.deserializer
handler: python
options:
show_root_heading: true
show_root_toc_entry: false
show_symbol_type_heading: true
show_symbol_type_toc: true
::: pydase.utils.serialization.types
handler: python
::: pydase.utils.decorators
handler: python
options:
filters: ["!render_in_frontend"]
::: pydase.utils.logging
handler: python
::: pydase.units
handler: python
::: pydase.config
handler: python

View File

@ -1,14 +1,11 @@
# Getting Started
## Installation
{%
include-markdown "../README.md"
start="<!--installation-start-->"
end="<!--installation-end-->"
start="<!--getting-started-start-->"
end="<!--getting-started-end-->"
%}
## Usage
{%
include-markdown "../README.md"
start="<!--usage-start-->"
end="<!--usage-end-->"
%}
[RESTful API]: ./user-guide/interaction/RESTful-API.md
[Python RPC Client]: ./user-guide/interaction/Python-Client.md
[Custom Components]: ./user-guide/Components.md#custom-components-pydasecomponents
[Components]: ./user-guide/Components.md

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 13 KiB

BIN
docs/images/logo-bw.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

11
docs/images/logo-bw.svg Normal file
View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" width="300.000000pt" height="319.000000pt" viewBox="0 0 300.000000 319.000000" preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.10, written by Peter Selinger 2001-2011
</metadata>
<g transform="translate(0.000000,319.000000) scale(0.050000,-0.050000)" fill="#000000" stroke="none">
<path d="M3177 6315 c-73 -26 -181 -83 -240 -128 -87 -67 -137 -88 -270 -115 -1259 -251 -2314 -1289 -2589 -2550 -380 -1734 1006 -3502 2746 -3502 1092 0 1819 261 2376 852 1117 1187 1046 2893 -171 4102 l-265 263 107 71 c65 43 127 106 160 161 68 116 87 115 287 -19 279 -187 300 -77 30 157 l-58 51 115 116 c149 152 167 320 22 199 -224 -185 -335 -226 -354 -131 -34 168 -137 227 -683 390 l-380 114 -350 7 c-326 8 -359 5 -483 -38z m1193 -245 c505 -152 550 -179 550 -322 0 -95 -184 -206 -559 -337 -556 -193 -887 -224 -1121 -104 -71 37 -173 89 -224 115 -221 112 -188 499 57 673 129 91 215 106 577 98 l340 -7 380 -116z m-1647 -319 c-8 -214 19 -324 119 -480 33 -53 57 -98 54 -100 -3 -2 -127 -48 -276 -100 -789 -280 -1197 -648 -1468 -1325 -250 -626 -230 -1189 69 -1886 56 -132 112 -304 130 -400 66 -348 238 -672 518 -975 150 -162 145 -163 -142 -18 -751 378 -1266 1020 -1501 1873 -52 189 -51 877 2 1120 230 1058 1019 1971 2012 2329 129 46 450 147 480 150 6 1 7 -84 3 -188z m2304 -993 c914 -980 1033 -2150 325 -3215 -572 -860 -1720 -1295 -2645 -1002 -560 178 -831 366 -986 683 -223 458 -232 753 -33 1064 175 273 284 290 1082 163 853 -135 1190 -74 1545 280 91 90 165 157 165 148 0 -244 -303 -619 -632 -782 l-174 -86 -374 -11 c-447 -12 -521 -40 -624 -238 -142 -271 -52 -462 244 -518 216 -42 300 -46 464 -24 1202 161 1849 1357 1347 2490 -29 66 -75 226 -101 356 -48 244 -131 451 -249 622 l-61 89 235 80 c306 104 276 110 472 -99z m-772 -195 c280 -415 191 -1010 -208 -1383 -252 -236 -463 -295 -1137 -322 -822 -32 -1036 -94 -1249 -361 -107 -134 -113 -133 -82 7 172 759 472 1031 1191 1078 240 16 342 31 410 61 363 159 379 624 29 795 -99 49 -122 41 451 160 553 116 490 120 595 -35z m-1895 -84 c39 -11 192 -47 340 -80 518 -114 681 -237 592 -446 -67 -156 -155 -191 -550 -215 -782 -47 -1105 -339 -1352 -1226 -37 -131 -53 -128 -89 18 -134 554 57 1165 509 1623 309 313 404 369 550 326z m2342 -1942 c-167 -657 -704 -1119 -1359 -1169 -320 -24 -563 50 -563 173 0 188 127 259 508 282 802 48 1231 374 1375 1048 60 282 66 286 73 41 4 -166 -4 -255 -34 -375z"/>
<path d="M3858 5922 c-62 -62 -78 -92 -78 -151 0 -307 422 -382 501 -88 70 262 -231 432 -423 239z m245 -95 c45 -41 48 -113 6 -156 -43 -42 -101 -39 -149 9 -97 97 41 239 143 147z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
docs/images/logo-colour.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

153
docs/images/logo-colour.svg Normal file
View File

@ -0,0 +1,153 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
width="588px"
height="626px"
viewBox="0 0 588 626"
preserveAspectRatio="xMidYMid meet"
id="svg184"
sodipodi:docname="pydase-logo-colour-3.svg"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
inkscape:export-filename="pydase-logo-colour-3.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs184" />
<sodipodi:namedview
id="namedview184"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.70710678"
inkscape:cx="48.083261"
inkscape:cy="74.953318"
inkscape:window-width="2048"
inkscape:window-height="1243"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg184"
showgrid="false" />
<g
fill="#041b31"
id="g1"
style="display:inline"
inkscape:label="Contour">
<path
d="m 249,624.5 c -0.8,-0.2 -4.9,-0.8 -9,-1.5 -23.8,-3.7 -65.4,-19 -91,-33.5 C 115.5,570.6 81,540.3 58.3,510 41.3,487.2 23.6,454.3 16.2,431.5 8.8,408.8 8.3,406.8 4.9,387.5 1.9,370.5 1.8,368 1.6,342 1.5,313.2 1.4,314 7.1,282.6 18.3,221.6 48.7,167 100.4,115.5 116.6,99.3 126.7,90.8 142.5,80.1 158.5,69.3 182.9,56 199.5,49 210.6,44.4 240.6,34.4 252,31.5 c 7.3,-1.8 22.4,-4.5 25.5,-4.5 0.2,0 2.7,-2.1 5.7,-4.6 C 301.8,6.5 318.4,1 348,0.9 c 17.1,0 36.4,1.4 46,3.2 3,0.6 14.7,4 26,7.4 11.3,3.5 27.3,8.2 35.5,10.4 17.5,4.8 27.3,9.3 33.4,15.3 5.5,5.5 8.1,10.7 8.8,17.4 0.3,3 0.9,5.4 1.4,5.4 4,0 19.5,-9.6 30.7,-19 8.1,-6.9 9.3,-6.9 11.3,-0.1 2,6.6 -0.6,10 -19,25.9 l -3.5,2.9 10.6,10.4 c 13.4,13.2 17.8,21.1 12.4,22.5 -2.9,0.7 -4.8,-0.3 -15.2,-7.8 C 516.1,87.4 503.2,80 500.5,80 c -1.6,0 -2.9,1.5 -5,6.1 -3.8,7.9 -13.7,17.7 -22.6,22.4 l -6.8,3.6 4.7,4.2 c 18.1,16.2 30.1,28 40.8,40 15.1,16.9 22.8,27 32.1,42.4 6.9,11.4 22.2,41.2 23.8,46.3 0.4,1.4 1.6,4.3 2.6,6.5 4.9,10.7 10.9,34.8 14.6,58.5 2.7,17.9 2.5,58.7 -0.5,77.8 -5.3,33.5 -9.2,47.1 -21.3,73.7 -12.6,27.8 -24.1,46.3 -40.8,65.6 -19.2,22.3 -38.5,39.4 -60.5,53.8 -10.2,6.6 -43.5,23 -54.7,26.9 -16.2,5.7 -44,11 -69.1,13.2 -6.9,0.6 -17.5,1.7 -23.5,2.5 -9.4,1.3 -59.9,2 -65.3,1 z m 99.5,-135.4 c 36.7,-9.2 67.4,-29.4 87.4,-57.6 7.2,-10.3 17.8,-31.2 21.6,-42.9 5.7,-17.8 7,-26.5 7,-48.3 0,-18 -0.4,-22.7 -2,-21.2 -0.2,0.3 -1.1,5 -2,10.4 -5.4,34.9 -14.4,55.5 -32.5,74.8 -16.6,17.7 -36.73987,31.75263 -59.4,38.2 -7.25764,2.06498 -18.96791,3.46589 -37.2,4.4 -35.48106,1.81785 -36.6,1.6 -43.6,5.3 -12.5,6.7 -18.3,17.8 -14.4,27.3 2,4.7 6.3,7.1 17.1,9.5 12.5,2.8 13.8,2.9 33,2.5 12.8,-0.3 19,-0.8 25,-2.4 z M 134.4,385.8 c 0.8,-2.9 2.5,-8.9 3.6,-13.3 7.9,-29.5 14.4,-45.5 25.2,-62 7.4,-11.4 12,-16.1 27,-27.5 8.1,-6.1 13.6,-9.4 23.3,-13.6 18.4,-8.1 23.2,-9 48.5,-9.8 36.8,-1.2 44.6,-2.8 53.9,-11.2 9.4,-8.5 10.8,-20 3.7,-30.6 -7.7,-11.7 -15.4,-15.1 -50.6,-22.2 -24.8,-5.1 -30,-6.3 -40.9,-9.7 l -7.3,-2.3 -5.5,2.9 c -9.6,5 -25.36942,18.22759 -38.5,31.3 -19.59963,19.51281 -30.17386,36.16842 -42.7,67.6 -4.80076,12.04646 -7.8,26.5 -9.2,37.8 -1.6,13.7 -0.7,38.8 2,50.6 2.7,12.1 4.2,17.2 5.2,17.2 0.4,0 1.4,-2.4 2.3,-5.2 z"
id="path1"
sodipodi:nodetypes="ccccccccccccscccccccscccccccsccccccccccccccccccccscccsscccccccccccccccccssccsc"
style="fill:#041b31;fill-opacity:1" />
</g>
<g
fill="#003051"
id="g84"
style="display:inline"
inkscape:label="Very Dark Blue">
<path
d="M 230.4,602 C 175.34835,591.74645 169.18046,579.19949 127.38046,537.39949 126.28656,507.06066 124.35047,466.6837 125.4,421 c 3.1,7.5 6.91046,19.16537 8.35973,29.56569 3.51031,25.1907 16.4289,65.12981 36.44027,90.93431 22.43047,28.92391 69.16433,55.53771 88.55235,64.93033 C 249.09029,604.75095 241.4,604.1 230.4,602 Z"
id="path70"
sodipodi:nodetypes="cccsacc" />
<path
d="m 319.4,193.4 c -9.8,-5.8 -14.5,-7.1 -48.4,-14 -18.7,-3.7 -29,-4.8 -29,-6.5 0,-1.7 4.92805,-2.87104 12.5,-5.4 12.8566,-4.29398 19.24892,-5.98769 27.1,-7.9 24.01253,-5.84879 36.7,-8.7 48.4,-10.5 25.2,-4 35.7,-5.4 42.5,-5.5 6.2,-0.1 7.9,0.3 14.6,3.6 9.7,4.8 15.5,10 26.3,24 -32.58707,9.22703 -69.37398,17.37018 -94,22.2 z"
id="path77"
sodipodi:nodetypes="ccsssccccc" />
</g>
<g
fill="#033f64"
id="g97"
style="display:inline"
inkscape:label="Dark Blue">
<path
d="m 152.17414,396.63217 c 0.38601,-2.81096 5.82243,-25.08009 21.18483,-38.15736 33.76966,-28.74649 155.07007,-22.31003 192.71893,-28.8897 C 388.43397,313.23279 413.02792,214.49976 425.1,189.5 c 7.4,15 16.15078,54.97811 10.64936,81.97944 -4.26433,20.9296 -15.49967,42.2641 -32.45863,55.24972 -23.8158,18.23596 -36.39069,23.58236 -86.79073,23.77084 -83.29996,0.31152 -95.44833,-4.42471 -136.27417,16.21161 -12.20115,6.16734 -21.45976,18.1207 -28.05169,29.92056 z"
id="path118"
sodipodi:nodetypes="csccaasac"
style="display:inline;fill:#18759e;fill-opacity:1;fill-rule:nonzero" />
<path
d="M 183.5,588.1 C 115.8931,558.47699 107.64772,492.94457 88.1,430.2335 79,400.6335 76.84251,387.87492 75,366.15 c -1.824643,-21.51425 -3.417479,-43.86578 2.1,-64.7404 8.432657,-31.90379 27.29188,-60.49473 46.1,-87.6096 11.8141,-17.03188 24.95272,-33.78473 41.4,-46.4 13.29518,-10.19757 29.7308,-15.48328 44.9,-22.6 23.68008,-11.10966 63.61618,-31.81861 71.93442,-31.35243 3.81558,6.62743 29.05267,18.5147 28.43398,19.68762 0.31235,2.20322 -15.49372,-1.71368 -93.0684,32.46481 -30.64541,13.50201 -57.7,42.3 -74.5,67.4 -13.2,19.7 -23.8,43.8 -29.8,67.5 -5.2,20.6 -5.8,26.4 -5.2,45.7 0.8,25.7 4.5,42 15.4,68.8 l 5.5,13.5 0.3,13 c 0.1,7.1 0.6,15.1 1,17.6 0.4,2.6 1.31647,9.84975 0.81647,10.14975 -1.3,0.8 -0.71647,10.65025 1.78353,20.75025 2.9,11.9 13.6,43.4 17,50.1 9.51543,25.08025 19.6983,31.17451 34.4,48 z"
id="path92"
sodipodi:nodetypes="ccaaaaaccsccccccccccc"
style="fill:#18759e;fill-opacity:1" />
<path
d="M 336.53336,126.11775 C 326.2422,124.21015 287.27262,118.19694 281.1,72.4 398.98512,97.839775 428.5705,92.736362 481.94363,60.277903 c 0.3,15.65 -0.24934,17.091747 -5.11226,23.440508 -12.11958,15.82266 -34.57733,20.119399 -53.08407,27.518149 -15.89858,6.35605 -32.39842,11.77707 -49.33154,14.31356 -12.48954,1.87087 -28.16017,2.36977 -37.8824,0.56763 z"
id="path121"
sodipodi:nodetypes="sccaaas"
style="display:inline;fill:#18759e;fill-opacity:1" />
</g>
<g
fill="#c88700"
id="g133"
style="display:inline"
inkscape:label="Orange">
<path
d="M387.4 69.6 c-2.7 -2.7 -3.4 -4.2 -3.4 -7.4 0 -4.7 2.9 -8.8 7.6 -10.8 5.2 -2.2 7.3 -1.7 11.5 2.5 5.2 5.1 5.4 10.3 0.8 15.6 -2.8 3.1 -3.6 3.5 -8.1 3.5 -4.4 0 -5.4 -0.4 -8.4 -3.4z"
id="path125" />
<path
d="m 319.5,603.3 c -20.3,-1 -47.80327,-8.953 -69.9,-18.6 -12.64521,-5.52065 -23.8619,-13.95619 -35,-22.1 -5.09897,-3.72819 -9.99476,-7.77262 -14.5,-12.2 -8.10524,-7.96518 -17.7,-18.1 -22.4,-25.7 -13.9,-22.6 -23.4,-49.7 -26.7,-76.3 -1,-7.8 -0.9,-10.1 0.5,-15.5 3.5,-13.8 17.6,-39 26.3,-47.1 2.7,-2.6 8.1,-6.2 11.9,-8.1 8.6,-4.4 24.6,-9.3 33.8,-10.4 7.3,-0.9 66.1,-0.8 73,0.1 2.2,0.3 13.7,0.8 25.7,1.2 22.9,0.7 34.8,-0.2 49.2,-3.5 0,0 49.54914,-16.12943 68.7,-52.4 l 3.8,-7.2 0.1,6 c 0,8.5 -4.5,35.3 -7.5,44.2 -5.06001,15.02512 -12.78595,28.02413 -23.26042,39.12091 -9.81203,10.39498 -22.03592,19.12073 -36.73958,26.27909 -17.6,8.5 -16.2,8.2 -52,8.4 -30.6,0.1 -32.3,0.2 -37.6,2.3 -16.6,6.6 -26.4,18.6 -29.5,36.3 -1.6,8.9 -1.1,16.5 1.1,20.9 1.8,3.3 8.2,9.4 12.2,11.4 4.3,2.1 18.7,5.2 31.3,6.7 20.6,2.4 50,-1.8 71.5,-10.1 22.9,-8.9 41.8,-21.2 59,-38.4 18.5,-18.5 31.2,-39.3 39.5,-64.5 12.2,-37.2 12.4,-66.6 0.5,-107.7 -3.2,-11.2 -4.6,-14.9 -12,-30.8 -2.7,-6 -4.1,-11.8 -7,-30.5 -0.9,-5.7 -2.6,-13.8 -3.6,-18 -2.3,-9 -12.8,-31.1 -18.8,-39.6 -5.9,-8.4 -18.1,-21.5 -25.2,-27.1 -3.3,-2.6 -5.6,-5.1 -5.2,-5.5 0.4,-0.4 5.1,-1.9 10.3,-3.3 17.7,-5 26.1,-7.9 29.6,-10.2 1.9,-1.3 4.3,-2.4 5.2,-2.4 5,0.1 36,27 53.9,46.9 46.2,51.1 71.3,114.2 71.3,178.9 0,60.4 -17.3,114.5 -51.4,160.6 -14.1,19.3 -42.2,45.5 -64.6,60.6 -12.3,8.3 -21.8,13.2 -36.1,18.9 -40.2,15.9 -63.3,20.2 -99.4,18.4 z"
id="path131"
sodipodi:nodetypes="caaacccccccccccccsccccccccscccccccsccccscccc" />
</g>
<g
fill="#38b3d3"
id="g162"
style="display:inline"
inkscape:label="Blue">
<path
d="m 152.17414,396.63217 c -2.38519,-1.38132 9.27416,-52.79756 19.37815,-68.90898 16.15254,-24.81116 34.25689,-40.51929 62.0508,-48.64318 22.03094,-6.43944 64.62509,-4.00901 74.27424,-7.22545 5.13056,-1.80515 13.30143,-6.84069 18.81201,-11.68624 5.89061,-5.1305 11.1162,-14.91656 12.63633,-23.84749 1.71019,-9.88108 -0.47111,-21.90723 -6.47249,-30.32083 -2.85318,-4 -7.4141,-11.9156 -29.3718,-19.44781 19.92351,-5.56647 53.71798,-12.0993 80.70491,-17.53799 7.71528,-1.55487 13.91102,-2.63422 23.21371,-4.3142 21.30966,22.8642 21.21637,35.77338 26.93252,55.92264 0.0584,24.34066 -3.50141,45.36921 -16.09946,65.67248 -23.04998,44.93326 -65.30711,57.83541 -113.96611,59.38228 -72.68272,0.94776 -90.43688,2.59826 -116.76278,14.72068 -22.87446,10.53312 -33.71226,36.8281 -35.33003,36.23409 z"
id="path159"
sodipodi:nodetypes="csscccscacssssc"
style="display:inline;stroke-width:0.999987" />
<path
d="M 59.627728,486.61872 C 26.249201,426.79436 20.062286,396.1054 18.4,359.3 17.560667,340.71596 17.7,316.6 19.4,303.5 23.8,271.6 35.4,236 51.1,206 75.3,160.1 119.7,111.9 162.1,85.7 194.42327,64.457719 225.27293,54.821946 268,43 c -4.38883,35.093545 0.24301,53.781332 18.43033,75.35581 -16.19179,5.17933 -38.68025,13.24334 -44.53566,15.38169 -16.14313,5.89535 -49.89323,20.65189 -79.79467,47.7625 -27.4732,24.909 -59.81413,81.60725 -65.712627,143.66935 -4.157076,43.73944 6.451807,84.86847 34.031537,142.43409 3.43378,24.64602 9.97891,73.87903 71.35443,127.63575 C 125.61659,570.1535 67.391777,500.53423 59.627728,486.61872 Z"
id="path160"
sodipodi:nodetypes="sscccccsssccs"
style="display:inline" />
<path
d="m 332,111.5 c -7.6,-1.9 -19.1,-6.8 -24.2,-10.3 -5.6,-3.9 -14.42556,-11.925563 -21.72556,-10.225563 -0.59944,-1.638563 -2.45486,-5.992204 -3.00412,-8.525 C 277.37032,64.949437 281.9,46.6 294.8,33.2 c 6.5,-6.8 14.5,-10.9 27.7,-14.4 7,-1.9 10.6,-2.1 29,-2.1 28.2,0.1 42.1,2.5 71.2,12.3 6.8,2.2 19.1,5.8 27.4,8 16.6,4.4 23.6,7.6 28,12.9 2.6,3.2 3.87429,4.2 3.87429,11.2 v 7.7 L 473.8,73.7 c -4,2.8 -9.8,6.4 -12.8,8.1 -15.5,8.6 -69.4,26.1 -91.5,29.7 -11,1.8 -30.1,1.8 -37.5,0 z m 74.6,-27.4 c 8,-3.6 13.4,-13.3 13.4,-24 0,-7.1 -2.5,-12.5 -7.8,-17.3 -6.2,-5.6 -15.4,-7.3 -24.6,-4.6 -5.8,1.7 -14.1,10.2 -15.6,15.9 -3.2,11.9 3.1,25.6 14,30.3 4.9,2.1 15.5,2 20.6,-0.3 z"
id="path162"
sodipodi:nodetypes="ccccccccscsccccccsccccc" />
</g>
<g
fill="#fccd00"
id="g165"
style="display:inline"
inkscape:label="Yellow">
<path
d="M 290.57843,579.73223 C 262.53343,574.09041 238.11479,563.08508 212.75,550.7 189.86762,538.42339 184.68162,535.3415 175.4,519.55 c -7.00993,-11.92651 -30.58414,-55.74044 -23.8,-86.25 4.0198,-18.07777 14.86881,-43.99552 38.1,-55.6 16.46843,-0.10091 32.45479,1.52207 48.61284,3.12963 26.00767,2.58749 51.5763,9.85418 77.70491,10.47812 23.17389,0.55338 47.87531,2.89829 69.28278,-5.99304 22.20756,-9.22363 37.89511,-23.97358 55.12824,-46.53102 -2.5563,14.26912 -7.95593,45.65799 -44.98524,71.69133 -11.14814,7.83767 -23.62107,14.42481 -36.84575,17.7139 -10.72566,2.66757 -18.69625,1.20562 -33.13151,1.30575 C 310.59858,429.5978 291.1,429.3 281.1,434.3 c -12.2,6 -20.6,17.5 -23.7,32.3 -3.2,15.3 0.11875,24.31875 9.51875,31.11875 4.9,3.6 9.48125,5.48125 25.58125,8.38125 10.2,1.8 14.5,2 29,1.6 19.3,-0.6 27.7,-2.1 45,-7.8 65,-21.6 108.32042,-74.69846 114.2483,-146.4 0.5433,-6.57154 0.51635,-11.00098 0.35824,-16.5 -0.12685,-4.41201 -0.53376,-8.81617 -1.04757,-13.2 -0.31035,-2.64783 -0.73303,-5.28343 -1.22803,-7.90303 -1.04804,-5.54641 -2.17688,-11.08849 -3.68486,-16.52789 -3.8173,-13.76923 -7.04718,-27.944 -13.54608,-40.66908 -8.57845,-16.79692 -6.03317,-32.79012 -12.7776,-53.20969 -5.4006,-16.35095 -14.13511,-31.22562 -25.45092,-47.68672 9.20262,-3.00968 42.04296,-13.97755 50.15501,-17.80255 10.28756,9.39474 26.84483,25.52589 38.78601,40.81146 30.4959,39.03695 51.65187,83.78847 56.2875,132.1875 4.21372,43.99397 -0.37701,62.58021 -7.1,82.25 -6.8,20.7 -14.2,35.95 -22.6,53.65 -14.8,30.9 -37.8,59.1 -65.1,79.7 -34.6,26.2 -53.59209,36.03122 -84.7,43.9 -28.19212,7.13123 -69.76059,13.01808 -98.52157,7.23223 z"
id="path163"
sodipodi:nodetypes="sssscaaacsasccccccsaaaassccsscccss"
style="display:inline" />
<path
d="M 391.3,71.5 C 387.8,70 384,64.8 384,61.4 c 0,-2.7 3.4,-7 7,-8.9 4.9,-2.5 7.8,-1.9 12.2,2.5 3.6,3.5 4,4.4 3.5,7.5 -0.7,4.5 -3.5,8.2 -7.1,9.5 -3.7,1.3 -4.4,1.2 -8.3,-0.5 z"
id="path165" />
</g>
<g
fill="#fafcfc"
id="g183"
inkscape:label="White"
style="display:inline">
<path
d="M 292.22204,510.87608 C 280.22101,508.20541 268.81402,500.34672 263.69227,494.9842 275.64093,505.5687 304.1,508.3 321.5,507.7 c 21.55,-2.225 49.37501,-6.43114 86.62589,-28.91732 22.61919,-13.65389 51.87112,-50.42418 60.53015,-75.76929 6.66561,-19.51032 10.07957,-35.4123 12.39396,-53.90714 3.1459,18.64649 1.15198,36.57617 -1.3,46.46875 -2.9,11.1 -6.35,24.125 -11.95,34.225 -8.3,15.1 -27.2,38.1 -39.1,47.8 -25.5,20.5 -61.64365,33.01311 -92.85,36.3 -15.06775,1.58705 -35.15198,-1.1377 -43.62796,-3.02392 z"
id="path174"
sodipodi:nodetypes="sccssccccss" />
<path
d="M 28.4,416.5 C 15.349709,374.67557 18.014551,365.86291 17.43688,340.1625 17.048048,322.86353 19.119484,305.4699 22.5,288.5 c 10.62259,-53.3245 29.9,-91.9 61.3,-131 11,-13.7 40.9,-44 52.7,-53.5 C 166.2,80.3 209,59.4 252,47.5 c 8.5,-2.3 15.6,-4.2 15.7,-4.1 0.1,0.1 -0.4,3.8 -1.2,8.1 -0.8,4.4 -1.4,8.1 -1.5,8.3 0,0.1 -0.8,0.2 -1.9,0.2 -1,0 -6.3,1.4 -11.7,3 -41.6,12.8 -72.7,28.3 -103.6,51.7 -24.8,18.7 -39.9,34 -59.6,60 C 63.3,207.6 42.3,251 34.6,285.5 29.2,309.4 26.825886,327.09972 25.755456,348.16934 24.598916,370.93392 24.8,389.7 28.4,416.5 Z"
id="path178"
sodipodi:nodetypes="cascccsccsccccac" />
<path
d="m 208.22773,289.96967 c 9.51882,-5.66851 21.67237,-10.67386 30.98163,-12.63033 5.43202,-1.14162 18.645,-2.6057 32.04905,-3.10711 14.85841,-0.5558 26.43935,0.0727 34.62618,-2.66291 17.29397,-5.77872 28.56982,-17.26767 32.18039,-30.34042 1.49085,-5.3979 2.16985,-10.98219 1.55113,-16.06452 -0.70068,-5.7556 -3.89365,-15.38399 -6.46854,-18.70034 7.65573,3.55244 13.50421,17.23897 13.20338,31.10442 -0.37371,17.22406 -13.0606,32.1577 -24.74645,38.26377 -9.47406,4.95038 -29.08518,7.77124 -44.57677,8.07938 -10.95355,0.21788 -20.76029,0.67236 -31.82773,2.18839 -11.53232,1.57971 -30.58589,8.52074 -45.60676,17.46672 -7.81866,4.65656 -18.21827,12.44919 -21.26902,14.46609 4.45077,-6.22439 16.85283,-20.2914 29.90351,-28.06314 z"
id="path181"
sodipodi:nodetypes="sssssscssssscs" />
<path
d="m 282.3,76.8 c -1.6,-2.7 -0.6,-19.1 1.6,-25.2 4.3,-12 13.6,-22.7 23.4,-27.1 12.6,-5.5 18.3,-6.7 36.2,-7.2 29.7,-0.9 49.3,2 77,11.3 7.2,2.4 19.8,6.1 28.2,8.3 19.3,5.1 26.3,8.5 30.5,14.9 1.6,2.4 2.5,8.2 1.3,8.2 -0.3,0 -2.6,-1.3 -5.2,-2.9 C 470.5,54.2 463.8,51.9 442,46 435.7,44.2 426,41.1 420.5,39 415,36.9 408.9,34.7 407,34.1 c -12,-3.7 -49.7,-5.9 -71.3,-4.2 -11,0.8 -13.8,1.4 -19.4,4.1 -12.9,6 -24.1,20.5 -27.6,35.9 -1.7,7.2 -4.3,10.1 -6.4,6.9 z"
id="path183" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -0,0 +1,145 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="84.373627mm"
height="29.06181mm"
viewBox="0 0 84.373627 29.06181"
version="1.1"
id="svg1"
xml:space="preserve"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
sodipodi:docname="logo-with-text.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="1.6080267"
inkscape:cx="230.09568"
inkscape:cy="46.019136"
inkscape:window-width="1920"
inkscape:window-height="1011"
inkscape:window-x="26"
inkscape:window-y="23"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" /><defs
id="defs1" /><g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-27.646074,-133.9691)"><g
id="g2"
transform="matrix(0.04656788,0,0,0.04656788,27.572788,133.92718)"
style="stroke-width:5.68167"><g
fill="#041b31"
id="g1"
style="display:inline;stroke-width:5.68167"
inkscape:label="Contour"><path
d="m 249,624.5 c -0.8,-0.2 -4.9,-0.8 -9,-1.5 -23.8,-3.7 -65.4,-19 -91,-33.5 C 115.5,570.6 81,540.3 58.3,510 41.3,487.2 23.6,454.3 16.2,431.5 8.8,408.8 8.3,406.8 4.9,387.5 1.9,370.5 1.8,368 1.6,342 1.5,313.2 1.4,314 7.1,282.6 18.3,221.6 48.7,167 100.4,115.5 116.6,99.3 126.7,90.8 142.5,80.1 158.5,69.3 182.9,56 199.5,49 210.6,44.4 240.6,34.4 252,31.5 c 7.3,-1.8 22.4,-4.5 25.5,-4.5 0.2,0 2.7,-2.1 5.7,-4.6 C 301.8,6.5 318.4,1 348,0.9 c 17.1,0 36.4,1.4 46,3.2 3,0.6 14.7,4 26,7.4 11.3,3.5 27.3,8.2 35.5,10.4 17.5,4.8 27.3,9.3 33.4,15.3 5.5,5.5 8.1,10.7 8.8,17.4 0.3,3 0.9,5.4 1.4,5.4 4,0 19.5,-9.6 30.7,-19 8.1,-6.9 9.3,-6.9 11.3,-0.1 2,6.6 -0.6,10 -19,25.9 l -3.5,2.9 10.6,10.4 c 13.4,13.2 17.8,21.1 12.4,22.5 -2.9,0.7 -4.8,-0.3 -15.2,-7.8 C 516.1,87.4 503.2,80 500.5,80 c -1.6,0 -2.9,1.5 -5,6.1 -3.8,7.9 -13.7,17.7 -22.6,22.4 l -6.8,3.6 4.7,4.2 c 18.1,16.2 30.1,28 40.8,40 15.1,16.9 22.8,27 32.1,42.4 6.9,11.4 22.2,41.2 23.8,46.3 0.4,1.4 1.6,4.3 2.6,6.5 4.9,10.7 10.9,34.8 14.6,58.5 2.7,17.9 2.5,58.7 -0.5,77.8 -5.3,33.5 -9.2,47.1 -21.3,73.7 -12.6,27.8 -24.1,46.3 -40.8,65.6 -19.2,22.3 -38.5,39.4 -60.5,53.8 -10.2,6.6 -43.5,23 -54.7,26.9 -16.2,5.7 -44,11 -69.1,13.2 -6.9,0.6 -17.5,1.7 -23.5,2.5 -9.4,1.3 -59.9,2 -65.3,1 z m 99.5,-135.4 c 36.7,-9.2 67.4,-29.4 87.4,-57.6 7.2,-10.3 17.8,-31.2 21.6,-42.9 5.7,-17.8 7,-26.5 7,-48.3 0,-18 -0.4,-22.7 -2,-21.2 -0.2,0.3 -1.1,5 -2,10.4 -5.4,34.9 -14.4,55.5 -32.5,74.8 -16.6,17.7 -36.73987,31.75263 -59.4,38.2 -7.25764,2.06498 -18.96791,3.46589 -37.2,4.4 -35.48106,1.81785 -36.6,1.6 -43.6,5.3 -12.5,6.7 -18.3,17.8 -14.4,27.3 2,4.7 6.3,7.1 17.1,9.5 12.5,2.8 13.8,2.9 33,2.5 12.8,-0.3 19,-0.8 25,-2.4 z M 134.4,385.8 c 0.8,-2.9 2.5,-8.9 3.6,-13.3 7.9,-29.5 14.4,-45.5 25.2,-62 7.4,-11.4 12,-16.1 27,-27.5 8.1,-6.1 13.6,-9.4 23.3,-13.6 18.4,-8.1 23.2,-9 48.5,-9.8 36.8,-1.2 44.6,-2.8 53.9,-11.2 9.4,-8.5 10.8,-20 3.7,-30.6 -7.7,-11.7 -15.4,-15.1 -50.6,-22.2 -24.8,-5.1 -30,-6.3 -40.9,-9.7 l -7.3,-2.3 -5.5,2.9 c -9.6,5 -25.36942,18.22759 -38.5,31.3 -19.59963,19.51281 -30.17386,36.16842 -42.7,67.6 -4.80076,12.04646 -7.8,26.5 -9.2,37.8 -1.6,13.7 -0.7,38.8 2,50.6 2.7,12.1 4.2,17.2 5.2,17.2 0.4,0 1.4,-2.4 2.3,-5.2 z"
id="path1"
sodipodi:nodetypes="ccccccccccccscccccccscccccccsccccccccccccccccccccscccsscccccccccccccccccssccsc"
style="fill:#041b31;fill-opacity:1;stroke-width:5.68167" /></g><g
fill="#003051"
id="g84"
style="display:inline;stroke-width:5.68167"
inkscape:label="Very Dark Blue"><path
d="M 230.4,602 C 175.34835,591.74645 169.18046,579.19949 127.38046,537.39949 126.28656,507.06066 124.35047,466.6837 125.4,421 c 3.1,7.5 6.91046,19.16537 8.35973,29.56569 3.51031,25.1907 16.4289,65.12981 36.44027,90.93431 22.43047,28.92391 69.16433,55.53771 88.55235,64.93033 C 249.09029,604.75095 241.4,604.1 230.4,602 Z"
id="path70"
sodipodi:nodetypes="cccsacc"
style="stroke-width:5.68167" /><path
d="m 319.4,193.4 c -9.8,-5.8 -14.5,-7.1 -48.4,-14 -18.7,-3.7 -29,-4.8 -29,-6.5 0,-1.7 4.92805,-2.87104 12.5,-5.4 12.8566,-4.29398 19.24892,-5.98769 27.1,-7.9 24.01253,-5.84879 36.7,-8.7 48.4,-10.5 25.2,-4 35.7,-5.4 42.5,-5.5 6.2,-0.1 7.9,0.3 14.6,3.6 9.7,4.8 15.5,10 26.3,24 -32.58707,9.22703 -69.37398,17.37018 -94,22.2 z"
id="path77"
sodipodi:nodetypes="ccsssccccc"
style="stroke-width:5.68167" /></g><g
fill="#033f64"
id="g97"
style="display:inline;stroke-width:5.68167"
inkscape:label="Dark Blue"><path
d="m 152.17414,396.63217 c 0.38601,-2.81096 5.82243,-25.08009 21.18483,-38.15736 33.76966,-28.74649 155.07007,-22.31003 192.71893,-28.8897 C 388.43397,313.23279 413.02792,214.49976 425.1,189.5 c 7.4,15 16.15078,54.97811 10.64936,81.97944 -4.26433,20.9296 -15.49967,42.2641 -32.45863,55.24972 -23.8158,18.23596 -36.39069,23.58236 -86.79073,23.77084 -83.29996,0.31152 -95.44833,-4.42471 -136.27417,16.21161 -12.20115,6.16734 -21.45976,18.1207 -28.05169,29.92056 z"
id="path118"
sodipodi:nodetypes="csccaasac"
style="display:inline;fill:#18759e;fill-opacity:1;fill-rule:nonzero;stroke-width:5.68167" /><path
d="M 183.5,588.1 C 115.8931,558.47699 107.64772,492.94457 88.1,430.2335 79,400.6335 76.84251,387.87492 75,366.15 c -1.824643,-21.51425 -3.417479,-43.86578 2.1,-64.7404 8.432657,-31.90379 27.29188,-60.49473 46.1,-87.6096 11.8141,-17.03188 24.95272,-33.78473 41.4,-46.4 13.29518,-10.19757 29.7308,-15.48328 44.9,-22.6 23.68008,-11.10966 63.61618,-31.81861 71.93442,-31.35243 3.81558,6.62743 29.05267,18.5147 28.43398,19.68762 0.31235,2.20322 -15.49372,-1.71368 -93.0684,32.46481 -30.64541,13.50201 -57.7,42.3 -74.5,67.4 -13.2,19.7 -23.8,43.8 -29.8,67.5 -5.2,20.6 -5.8,26.4 -5.2,45.7 0.8,25.7 4.5,42 15.4,68.8 l 5.5,13.5 0.3,13 c 0.1,7.1 0.6,15.1 1,17.6 0.4,2.6 1.31647,9.84975 0.81647,10.14975 -1.3,0.8 -0.71647,10.65025 1.78353,20.75025 2.9,11.9 13.6,43.4 17,50.1 9.51543,25.08025 19.6983,31.17451 34.4,48 z"
id="path92"
sodipodi:nodetypes="ccaaaaaccsccccccccccc"
style="fill:#18759e;fill-opacity:1;stroke-width:5.68167" /><path
d="M 336.53336,126.11775 C 326.2422,124.21015 287.27262,118.19694 281.1,72.4 398.98512,97.839775 428.5705,92.736362 481.94363,60.277903 c 0.3,15.65 -0.24934,17.091747 -5.11226,23.440508 -12.11958,15.82266 -34.57733,20.119399 -53.08407,27.518149 -15.89858,6.35605 -32.39842,11.77707 -49.33154,14.31356 -12.48954,1.87087 -28.16017,2.36977 -37.8824,0.56763 z"
id="path121"
sodipodi:nodetypes="sccaaas"
style="display:inline;fill:#18759e;fill-opacity:1;stroke-width:5.68167" /></g><g
fill="#c88700"
id="g133"
style="display:inline;stroke-width:5.68167"
inkscape:label="Orange"><path
d="m 387.4,69.6 c -2.7,-2.7 -3.4,-4.2 -3.4,-7.4 0,-4.7 2.9,-8.8 7.6,-10.8 5.2,-2.2 7.3,-1.7 11.5,2.5 5.2,5.1 5.4,10.3 0.8,15.6 -2.8,3.1 -3.6,3.5 -8.1,3.5 -4.4,0 -5.4,-0.4 -8.4,-3.4 z"
id="path125"
style="stroke-width:5.68167" /><path
d="m 319.5,603.3 c -20.3,-1 -47.80327,-8.953 -69.9,-18.6 -12.64521,-5.52065 -23.8619,-13.95619 -35,-22.1 -5.09897,-3.72819 -9.99476,-7.77262 -14.5,-12.2 -8.10524,-7.96518 -17.7,-18.1 -22.4,-25.7 -13.9,-22.6 -23.4,-49.7 -26.7,-76.3 -1,-7.8 -0.9,-10.1 0.5,-15.5 3.5,-13.8 17.6,-39 26.3,-47.1 2.7,-2.6 8.1,-6.2 11.9,-8.1 8.6,-4.4 24.6,-9.3 33.8,-10.4 7.3,-0.9 66.1,-0.8 73,0.1 2.2,0.3 13.7,0.8 25.7,1.2 22.9,0.7 34.8,-0.2 49.2,-3.5 0,0 49.54914,-16.12943 68.7,-52.4 l 3.8,-7.2 0.1,6 c 0,8.5 -4.5,35.3 -7.5,44.2 -5.06001,15.02512 -12.78595,28.02413 -23.26042,39.12091 -9.81203,10.39498 -22.03592,19.12073 -36.73958,26.27909 -17.6,8.5 -16.2,8.2 -52,8.4 -30.6,0.1 -32.3,0.2 -37.6,2.3 -16.6,6.6 -26.4,18.6 -29.5,36.3 -1.6,8.9 -1.1,16.5 1.1,20.9 1.8,3.3 8.2,9.4 12.2,11.4 4.3,2.1 18.7,5.2 31.3,6.7 20.6,2.4 50,-1.8 71.5,-10.1 22.9,-8.9 41.8,-21.2 59,-38.4 18.5,-18.5 31.2,-39.3 39.5,-64.5 12.2,-37.2 12.4,-66.6 0.5,-107.7 -3.2,-11.2 -4.6,-14.9 -12,-30.8 -2.7,-6 -4.1,-11.8 -7,-30.5 -0.9,-5.7 -2.6,-13.8 -3.6,-18 -2.3,-9 -12.8,-31.1 -18.8,-39.6 -5.9,-8.4 -18.1,-21.5 -25.2,-27.1 -3.3,-2.6 -5.6,-5.1 -5.2,-5.5 0.4,-0.4 5.1,-1.9 10.3,-3.3 17.7,-5 26.1,-7.9 29.6,-10.2 1.9,-1.3 4.3,-2.4 5.2,-2.4 5,0.1 36,27 53.9,46.9 46.2,51.1 71.3,114.2 71.3,178.9 0,60.4 -17.3,114.5 -51.4,160.6 -14.1,19.3 -42.2,45.5 -64.6,60.6 -12.3,8.3 -21.8,13.2 -36.1,18.9 -40.2,15.9 -63.3,20.2 -99.4,18.4 z"
id="path131"
sodipodi:nodetypes="caaacccccccccccccsccccccccscccccccsccccscccc"
style="stroke-width:5.68167" /></g><g
fill="#38b3d3"
id="g162"
style="display:inline;stroke-width:5.68167"
inkscape:label="Blue"><path
d="m 152.17414,396.63217 c -2.38519,-1.38132 9.27416,-52.79756 19.37815,-68.90898 16.15254,-24.81116 34.25689,-40.51929 62.0508,-48.64318 22.03094,-6.43944 64.62509,-4.00901 74.27424,-7.22545 5.13056,-1.80515 13.30143,-6.84069 18.81201,-11.68624 5.89061,-5.1305 11.1162,-14.91656 12.63633,-23.84749 1.71019,-9.88108 -0.47111,-21.90723 -6.47249,-30.32083 -2.85318,-4 -7.4141,-11.9156 -29.3718,-19.44781 19.92351,-5.56647 53.71798,-12.0993 80.70491,-17.53799 7.71528,-1.55487 13.91102,-2.63422 23.21371,-4.3142 21.30966,22.8642 21.21637,35.77338 26.93252,55.92264 0.0584,24.34066 -3.50141,45.36921 -16.09946,65.67248 -23.04998,44.93326 -65.30711,57.83541 -113.96611,59.38228 -72.68272,0.94776 -90.43688,2.59826 -116.76278,14.72068 -22.87446,10.53312 -33.71226,36.8281 -35.33003,36.23409 z"
id="path159"
sodipodi:nodetypes="csscccscacssssc"
style="display:inline;stroke-width:5.68161" /><path
d="M 59.627728,486.61872 C 26.249201,426.79436 20.062286,396.1054 18.4,359.3 17.560667,340.71596 17.7,316.6 19.4,303.5 23.8,271.6 35.4,236 51.1,206 75.3,160.1 119.7,111.9 162.1,85.7 194.42327,64.457719 225.27293,54.821946 268,43 c -4.38883,35.093545 0.24301,53.781332 18.43033,75.35581 -16.19179,5.17933 -38.68025,13.24334 -44.53566,15.38169 -16.14313,5.89535 -49.89323,20.65189 -79.79467,47.7625 -27.4732,24.909 -59.81413,81.60725 -65.712627,143.66935 -4.157076,43.73944 6.451807,84.86847 34.031537,142.43409 3.43378,24.64602 9.97891,73.87903 71.35443,127.63575 C 125.61659,570.1535 67.391777,500.53423 59.627728,486.61872 Z"
id="path160"
sodipodi:nodetypes="sscccccsssccs"
style="display:inline;stroke-width:5.68167" /><path
d="m 332,111.5 c -7.6,-1.9 -19.1,-6.8 -24.2,-10.3 -5.6,-3.9 -14.42556,-11.925563 -21.72556,-10.225563 -0.59944,-1.638563 -2.45486,-5.992204 -3.00412,-8.525 C 277.37032,64.949437 281.9,46.6 294.8,33.2 c 6.5,-6.8 14.5,-10.9 27.7,-14.4 7,-1.9 10.6,-2.1 29,-2.1 28.2,0.1 42.1,2.5 71.2,12.3 6.8,2.2 19.1,5.8 27.4,8 16.6,4.4 23.6,7.6 28,12.9 2.6,3.2 3.87429,4.2 3.87429,11.2 v 7.7 L 473.8,73.7 c -4,2.8 -9.8,6.4 -12.8,8.1 -15.5,8.6 -69.4,26.1 -91.5,29.7 -11,1.8 -30.1,1.8 -37.5,0 z m 74.6,-27.4 c 8,-3.6 13.4,-13.3 13.4,-24 0,-7.1 -2.5,-12.5 -7.8,-17.3 -6.2,-5.6 -15.4,-7.3 -24.6,-4.6 -5.8,1.7 -14.1,10.2 -15.6,15.9 -3.2,11.9 3.1,25.6 14,30.3 4.9,2.1 15.5,2 20.6,-0.3 z"
id="path162"
sodipodi:nodetypes="ccccccccscsccccccsccccc"
style="stroke-width:5.68167" /></g><g
fill="#fccd00"
id="g165"
style="display:inline;stroke-width:5.68167"
inkscape:label="Yellow"><path
d="M 290.57843,579.73223 C 262.53343,574.09041 238.11479,563.08508 212.75,550.7 189.86762,538.42339 184.68162,535.3415 175.4,519.55 c -7.00993,-11.92651 -30.58414,-55.74044 -23.8,-86.25 4.0198,-18.07777 14.86881,-43.99552 38.1,-55.6 16.46843,-0.10091 32.45479,1.52207 48.61284,3.12963 26.00767,2.58749 51.5763,9.85418 77.70491,10.47812 23.17389,0.55338 47.87531,2.89829 69.28278,-5.99304 22.20756,-9.22363 37.89511,-23.97358 55.12824,-46.53102 -2.5563,14.26912 -7.95593,45.65799 -44.98524,71.69133 -11.14814,7.83767 -23.62107,14.42481 -36.84575,17.7139 -10.72566,2.66757 -18.69625,1.20562 -33.13151,1.30575 C 310.59858,429.5978 291.1,429.3 281.1,434.3 c -12.2,6 -20.6,17.5 -23.7,32.3 -3.2,15.3 0.11875,24.31875 9.51875,31.11875 4.9,3.6 9.48125,5.48125 25.58125,8.38125 10.2,1.8 14.5,2 29,1.6 19.3,-0.6 27.7,-2.1 45,-7.8 65,-21.6 108.32042,-74.69846 114.2483,-146.4 0.5433,-6.57154 0.51635,-11.00098 0.35824,-16.5 -0.12685,-4.41201 -0.53376,-8.81617 -1.04757,-13.2 -0.31035,-2.64783 -0.73303,-5.28343 -1.22803,-7.90303 -1.04804,-5.54641 -2.17688,-11.08849 -3.68486,-16.52789 -3.8173,-13.76923 -7.04718,-27.944 -13.54608,-40.66908 -8.57845,-16.79692 -6.03317,-32.79012 -12.7776,-53.20969 -5.4006,-16.35095 -14.13511,-31.22562 -25.45092,-47.68672 9.20262,-3.00968 42.04296,-13.97755 50.15501,-17.80255 10.28756,9.39474 26.84483,25.52589 38.78601,40.81146 30.4959,39.03695 51.65187,83.78847 56.2875,132.1875 4.21372,43.99397 -0.37701,62.58021 -7.1,82.25 -6.8,20.7 -14.2,35.95 -22.6,53.65 -14.8,30.9 -37.8,59.1 -65.1,79.7 -34.6,26.2 -53.59209,36.03122 -84.7,43.9 -28.19212,7.13123 -69.76059,13.01808 -98.52157,7.23223 z"
id="path163"
sodipodi:nodetypes="sssscaaacsasccccccsaaaassccsscccss"
style="display:inline;stroke-width:5.68167" /><path
d="M 391.3,71.5 C 387.8,70 384,64.8 384,61.4 c 0,-2.7 3.4,-7 7,-8.9 4.9,-2.5 7.8,-1.9 12.2,2.5 3.6,3.5 4,4.4 3.5,7.5 -0.7,4.5 -3.5,8.2 -7.1,9.5 -3.7,1.3 -4.4,1.2 -8.3,-0.5 z"
id="path165"
style="stroke-width:5.68167" /></g><g
fill="#fafcfc"
id="g183"
inkscape:label="White"
style="display:inline;stroke-width:5.68167"><path
d="M 292.22204,510.87608 C 280.22101,508.20541 268.81402,500.34672 263.69227,494.9842 275.64093,505.5687 304.1,508.3 321.5,507.7 c 21.55,-2.225 49.37501,-6.43114 86.62589,-28.91732 22.61919,-13.65389 51.87112,-50.42418 60.53015,-75.76929 6.66561,-19.51032 10.07957,-35.4123 12.39396,-53.90714 3.1459,18.64649 1.15198,36.57617 -1.3,46.46875 -2.9,11.1 -6.35,24.125 -11.95,34.225 -8.3,15.1 -27.2,38.1 -39.1,47.8 -25.5,20.5 -61.64365,33.01311 -92.85,36.3 -15.06775,1.58705 -35.15198,-1.1377 -43.62796,-3.02392 z"
id="path174"
sodipodi:nodetypes="sccssccccss"
style="stroke-width:5.68167" /><path
d="M 28.4,416.5 C 15.349709,374.67557 18.014551,365.86291 17.43688,340.1625 17.048048,322.86353 19.119484,305.4699 22.5,288.5 c 10.62259,-53.3245 29.9,-91.9 61.3,-131 11,-13.7 40.9,-44 52.7,-53.5 C 166.2,80.3 209,59.4 252,47.5 c 8.5,-2.3 15.6,-4.2 15.7,-4.1 0.1,0.1 -0.4,3.8 -1.2,8.1 -0.8,4.4 -1.4,8.1 -1.5,8.3 0,0.1 -0.8,0.2 -1.9,0.2 -1,0 -6.3,1.4 -11.7,3 -41.6,12.8 -72.7,28.3 -103.6,51.7 -24.8,18.7 -39.9,34 -59.6,60 C 63.3,207.6 42.3,251 34.6,285.5 29.2,309.4 26.825886,327.09972 25.755456,348.16934 24.598916,370.93392 24.8,389.7 28.4,416.5 Z"
id="path178"
sodipodi:nodetypes="cascccsccsccccac"
style="stroke-width:5.68167" /><path
d="m 208.22773,289.96967 c 9.51882,-5.66851 21.67237,-10.67386 30.98163,-12.63033 5.43202,-1.14162 18.645,-2.6057 32.04905,-3.10711 14.85841,-0.5558 26.43935,0.0727 34.62618,-2.66291 17.29397,-5.77872 28.56982,-17.26767 32.18039,-30.34042 1.49085,-5.3979 2.16985,-10.98219 1.55113,-16.06452 -0.70068,-5.7556 -3.89365,-15.38399 -6.46854,-18.70034 7.65573,3.55244 13.50421,17.23897 13.20338,31.10442 -0.37371,17.22406 -13.0606,32.1577 -24.74645,38.26377 -9.47406,4.95038 -29.08518,7.77124 -44.57677,8.07938 -10.95355,0.21788 -20.76029,0.67236 -31.82773,2.18839 -11.53232,1.57971 -30.58589,8.52074 -45.60676,17.46672 -7.81866,4.65656 -18.21827,12.44919 -21.26902,14.46609 4.45077,-6.22439 16.85283,-20.2914 29.90351,-28.06314 z"
id="path181"
sodipodi:nodetypes="sssssscssssscs"
style="stroke-width:5.68167" /><path
d="m 282.3,76.8 c -1.6,-2.7 -0.6,-19.1 1.6,-25.2 4.3,-12 13.6,-22.7 23.4,-27.1 12.6,-5.5 18.3,-6.7 36.2,-7.2 29.7,-0.9 49.3,2 77,11.3 7.2,2.4 19.8,6.1 28.2,8.3 19.3,5.1 26.3,8.5 30.5,14.9 1.6,2.4 2.5,8.2 1.3,8.2 -0.3,0 -2.6,-1.3 -5.2,-2.9 C 470.5,54.2 463.8,51.9 442,46 435.7,44.2 426,41.1 420.5,39 415,36.9 408.9,34.7 407,34.1 c -12,-3.7 -49.7,-5.9 -71.3,-4.2 -11,0.8 -13.8,1.4 -19.4,4.1 -12.9,6 -24.1,20.5 -27.6,35.9 -1.7,7.2 -4.3,10.1 -6.4,6.9 z"
id="path183"
style="stroke-width:5.68167" /></g></g><text
xml:space="preserve"
style="font-size:11.2889px;font-family:Comfortaa;-inkscape-font-specification:Comfortaa;text-align:center;writing-mode:lr-tb;direction:ltr;text-anchor:middle;opacity:0.66761;fill:#083f91;stroke-width:3.307;stroke-linejoin:round;stroke-miterlimit:2.6"
x="91.349724"
y="151.56494"
id="text2"><tspan
sodipodi:role="line"
id="tspan2"
style="font-size:11.2889px;fill:#000000;fill-opacity:1;stroke-width:3.307"
x="91.349724"
y="151.56494">pydase</tspan></text></g></svg>

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -1 +1,17 @@
{% include-markdown "../README.md" %}
{%
include-markdown "../README.md"
start="<!--introduction-start-->"
end="<!--introduction-end-->"
%}
[pydase Banner]: ./images/logo-with-text.png
[License]: ./about/license.md
[Observer Pattern]: ./dev-guide/Observer_Pattern_Implementation.md
[Service Persistence]: ./user-guide/Service_Persistence.md
[Defining DataService]: ./getting-started.md#defining-a-dataservice
[Web Interface Access]: ./getting-started.md#accessing-the-web-interface
[Short RPC Client]: ./getting-started.md#connecting-to-the-service-via-python-rpc-client
[Customizing Web Interface]: ./user-guide/interaction/Auto-generated-Frontend.md#customization-options
[Task Management]: ./user-guide/Tasks.md
[Units]: ./user-guide/Understanding-Units.md
[Property Validation]: ./user-guide/Validating-Property-Setters.md

View File

@ -1,20 +1,38 @@
babel==2.15.0 ; python_version >= "3.10" and python_version < "4.0"
beautifulsoup4==4.12.3 ; python_version >= "3.10" and python_version < "4.0"
certifi==2024.7.4 ; python_version >= "3.10" and python_version < "4.0"
charset-normalizer==3.3.2 ; python_version >= "3.10" and python_version < "4.0"
click==8.1.7 ; python_version >= "3.10" and python_version < "4.0"
colorama==0.4.6 ; python_version >= "3.10" and python_version < "4.0" and platform_system == "Windows"
colorama==0.4.6 ; python_version >= "3.10" and python_version < "4.0"
ghp-import==2.1.0 ; python_version >= "3.10" and python_version < "4.0"
jinja2==3.1.2 ; python_version >= "3.10" and python_version < "4.0"
markdown==3.4.4 ; python_version >= "3.10" and python_version < "4.0"
markupsafe==2.1.3 ; python_version >= "3.10" and python_version < "4.0"
griffe==1.1.0 ; python_version >= "3.10" and python_version < "4.0"
idna==3.7 ; python_version >= "3.10" and python_version < "4.0"
jinja2==3.1.4 ; python_version >= "3.10" and python_version < "4.0"
markdown==3.6 ; python_version >= "3.10" and python_version < "4.0"
markupsafe==2.1.5 ; python_version >= "3.10" and python_version < "4.0"
mergedeep==1.3.4 ; python_version >= "3.10" and python_version < "4.0"
mkdocs-autorefs==0.5.0 ; python_version >= "3.10" and python_version < "4.0"
mkdocs-autorefs==1.0.1 ; python_version >= "3.10" and python_version < "4.0"
mkdocs-get-deps==0.2.0 ; python_version >= "3.10" and python_version < "4.0"
mkdocs-include-markdown-plugin==3.9.1 ; python_version >= "3.10" and python_version < "4.0"
mkdocs==1.5.3 ; python_version >= "3.10" and python_version < "4.0"
mkdocstrings==0.22.0 ; python_version >= "3.10" and python_version < "4.0"
packaging==23.1 ; python_version >= "3.10" and python_version < "4.0"
pathspec==0.11.2 ; python_version >= "3.10" and python_version < "4.0"
platformdirs==3.10.0 ; python_version >= "3.10" and python_version < "4.0"
pymdown-extensions==10.3 ; python_version >= "3.10" and python_version < "4.0"
python-dateutil==2.8.2 ; python_version >= "3.10" and python_version < "4.0"
mkdocs-material-extensions==1.3.1 ; python_version >= "3.10" and python_version < "4.0"
mkdocs-material==9.5.31 ; python_version >= "3.10" and python_version < "4.0"
mkdocs-swagger-ui-tag==0.6.10 ; python_version >= "3.10" and python_version < "4.0"
mkdocs==1.6.0 ; python_version >= "3.10" and python_version < "4.0"
mkdocstrings-python==1.10.8 ; python_version >= "3.10" and python_version < "4.0"
mkdocstrings==0.25.2 ; python_version >= "3.10" and python_version < "4.0"
mkdocstrings[python]==0.25.2 ; python_version >= "3.10" and python_version < "4.0"
packaging==24.1 ; python_version >= "3.10" and python_version < "4.0"
paginate==0.5.6 ; python_version >= "3.10" and python_version < "4.0"
pathspec==0.12.1 ; python_version >= "3.10" and python_version < "4.0"
platformdirs==4.2.2 ; python_version >= "3.10" and python_version < "4.0"
pygments==2.18.0 ; python_version >= "3.10" and python_version < "4.0"
pymdown-extensions==10.9 ; python_version >= "3.10" and python_version < "4.0"
python-dateutil==2.9.0.post0 ; python_version >= "3.10" and python_version < "4.0"
pyyaml-env-tag==0.1 ; python_version >= "3.10" and python_version < "4.0"
pyyaml==6.0.1 ; python_version >= "3.10" and python_version < "4.0"
regex==2024.7.24 ; python_version >= "3.10" and python_version < "4.0"
requests==2.32.3 ; python_version >= "3.10" and python_version < "4.0"
six==1.16.0 ; python_version >= "3.10" and python_version < "4.0"
watchdog==3.0.0 ; python_version >= "3.10" and python_version < "4.0"
soupsieve==2.5 ; python_version >= "3.10" and python_version < "4.0"
urllib3==2.2.2 ; python_version >= "3.10" and python_version < "4.0"
watchdog==4.0.1 ; python_version >= "3.10" and python_version < "4.0"

View File

@ -1,6 +1,435 @@
# Components Guide
{%
include-markdown "../../README.md"
start="<!-- Component User Guide Start -->"
end="<!-- Component User Guide End -->"
%}
In `pydase`, components are fundamental building blocks that bridge the Python backend logic with frontend visual representation and interactions. This system can be understood based on the following categories:
## Built-in Type and Enum Components
`pydase` automatically maps standard Python data types to their corresponding frontend components:
- `str`: Translated into a `StringComponent` on the frontend.
- `int` and `float`: Manifested as the `NumberComponent`.
- `bool`: Rendered as a `ButtonComponent`.
- `list`: Each item displayed individually, named after the list attribute and its index.
- `dict`: Each key-value pair displayed individually, named after the dictionary attribute and its key. **Note** that the dictionary keys must be strings.
- `enum.Enum`: Presented as an `EnumComponent`, facilitating dropdown selection.
## Method Components
Within the `DataService` class of `pydase`, only methods devoid of arguments can be represented in the frontend, classified into two distinct categories
1. [**Tasks**](./Tasks.md): Argument-free asynchronous functions, identified within `pydase` as tasks, are inherently designed for frontend interaction. These tasks are automatically rendered with a start/stop button, allowing users to initiate or halt the task execution directly from the web interface.
2. **Synchronous Methods with `@frontend` Decorator**: Synchronous methods without arguments can also be presented in the frontend. For this, they have to be decorated with the `@frontend` decorator.
```python
import pydase
import pydase.components
import pydase.units as u
from pydase.utils.decorators import frontend
class MyService(pydase.DataService):
@frontend
def exposed_method(self) -> None:
...
async def my_task(self) -> None:
while True:
# ...
```
![Method Components](../images/method_components.png)
You can still define synchronous tasks with arguments and call them using a python client. However, decorating them with the `@frontend` decorator will raise a `FunctionDefinitionError`. Defining a task with arguments will raise a `TaskDefinitionError`.
I decided against supporting function arguments for functions rendered in the frontend due to the following reasons:
1. Feature Request Pitfall: supporting function arguments create a bottomless pit of feature requests. As users encounter the limitations of supported types, demands for extending support to more complex types would grow.
2. Complexity in Supported Argument Types: while simple types like `int`, `float`, `bool` and `str` could be easily supported, more complicated types are not (representation, (de-)serialization).
## DataService Instances (Nested Classes)
Nested `DataService` instances offer an organized hierarchy for components, enabling richer applications. Each nested class might have its own attributes and methods, each mapped to a frontend component.
Here is an example:
```python
from pydase import DataService, Server
class Channel(DataService):
def __init__(self, channel_id: int) -> None:
super().__init__()
self._channel_id = channel_id
self._current = 0.0
@property
def current(self) -> float:
# run code to get current
result = self._current
return result
@current.setter
def current(self, value: float) -> None:
# run code to set current
self._current = value
class Device(DataService):
def __init__(self) -> None:
super().__init__()
self.channels = [Channel(i) for i in range(2)]
if __name__ == "__main__":
service = Device()
Server(service).run()
```
![Nested Classes App](../images/Nested_Class_App.png)
**Note** that defining classes within `DataService` classes is not supported (see [this issue](https://github.com/tiqi-group/pydase/issues/16)).
## Custom Components (`pydase.components`)
The custom components in `pydase` have two main parts:
- A **Python Component Class** in the backend, implementing the logic needed to set, update, and manage the component's state and data.
- A **Frontend React Component** that renders and manages user interaction in the browser.
Below are the components available in the `pydase.components` module, accompanied by their Python usage:
### `DeviceConnection`
The `DeviceConnection` component acts as a base class within the `pydase` framework for managing device connections. It provides a structured approach to handle connections by offering a customizable `connect` method and a `connected` property. This setup facilitates the implementation of automatic reconnection logic, which periodically attempts reconnection whenever the connection is lost.
In the frontend, this class abstracts away the direct interaction with the `connect` method and the `connected` property. Instead, it showcases user-defined attributes, methods, and properties. When the `connected` status is `False`, the frontend displays an overlay that prompts manual reconnection through the `connect()` method. Successful reconnection removes the overlay.
```python
import pydase.components
import pydase.units as u
class Device(pydase.components.DeviceConnection):
def __init__(self) -> None:
super().__init__()
self._voltage = 10 * u.units.V
def connect(self) -> None:
if not self._connected:
self._connected = True
@property
def voltage(self) -> float:
return self._voltage
class MyService(pydase.DataService):
def __init__(self) -> None:
super().__init__()
self.device = Device()
if __name__ == "__main__":
service_instance = MyService()
pydase.Server(service_instance).run()
```
![DeviceConnection Component](../images/DeviceConnection_component.png)
#### Customizing Connection Logic
Users are encouraged to primarily override the `connect` method to tailor the connection process to their specific device. This method should adjust the `self._connected` attribute based on the outcome of the connection attempt:
```python
import pydase.components
class MyDeviceConnection(pydase.components.DeviceConnection):
def __init__(self) -> None:
super().__init__()
# Add any necessary initialization code here
def connect(self) -> None:
# Implement device-specific connection logic here
# Update self._connected to `True` if the connection is successful,
# or `False` if unsuccessful
...
```
Moreover, if the connection status requires additional logic, users can override the `connected` property:
```python
import pydase.components
class MyDeviceConnection(pydase.components.DeviceConnection):
def __init__(self) -> None:
super().__init__()
# Add any necessary initialization code here
def connect(self) -> None:
# Implement device-specific connection logic here
# Ensure self._connected reflects the connection status accurately
...
@property
def connected(self) -> bool:
# Implement custom logic to accurately report connection status
return self._connected
```
#### Reconnection Interval
The `DeviceConnection` component automatically executes a task that checks for device availability at a default interval of 10 seconds. This interval is adjustable by modifying the `_reconnection_wait_time` attribute on the class instance.
### `Image`
This component provides a versatile interface for displaying images within the application. Users can update and manage images from various sources, including local paths, URLs, and even matplotlib figures.
The component offers methods to load images seamlessly, ensuring that visual content is easily integrated and displayed within the data service.
```python
import matplotlib.pyplot as plt
import numpy as np
import pydase
from pydase.components.image import Image
class MyDataService(pydase.DataService):
my_image = Image()
if __name__ == "__main__":
service = MyDataService()
# loading from local path
service.my_image.load_from_path("/your/image/path/")
# loading from a URL
service.my_image.load_from_url("https://cataas.com/cat")
# loading a matplotlib figure
fig = plt.figure()
x = np.linspace(0, 2 * np.pi)
plt.plot(x, np.sin(x))
plt.grid()
service.my_image.load_from_matplotlib_figure(fig)
pydase.Server(service).run()
```
![Image Component](../images/Image_component.png)
### `NumberSlider`
The `NumberSlider` component in the `pydase` package provides an interactive slider interface for adjusting numerical values on the frontend. It is designed to support both numbers and quantities and ensures that values adjusted on the frontend are synchronized with the backend.
To utilize the `NumberSlider`, users should implement a class that derives from `NumberSlider`. This class can then define the initial values, minimum and maximum limits, step sizes, and additional logic as needed.
Here's an example of how to implement and use a custom slider:
```python
import pydase
import pydase.components
class MySlider(pydase.components.NumberSlider):
def __init__(
self,
value: float = 0.0,
min_: float = 0.0,
max_: float = 100.0,
step_size: float = 1.0,
) -> None:
super().__init__(value, min_, max_, step_size)
@property
def min(self) -> float:
return self._min
@min.setter
def min(self, value: float) -> None:
self._min = value
@property
def max(self) -> float:
return self._max
@max.setter
def max(self, value: float) -> None:
self._max = value
@property
def step_size(self) -> float:
return self._step_size
@step_size.setter
def step_size(self, value: float) -> None:
self._step_size = value
@property
def value(self) -> float:
"""Slider value."""
return self._value
@value.setter
def value(self, value: float) -> None:
if value < self._min or value > self._max:
raise ValueError("Value is either below allowed min or above max value.")
self._value = value
class MyService(pydase.DataService):
def __init__(self) -> None:
super().__init__()
self.voltage = MySlider()
if __name__ == "__main__":
service_instance = MyService()
service_instance.voltage.value = 5
print(service_instance.voltage.value) # Output: 5
pydase.Server(service_instance).run()
```
In this example, `MySlider` overrides the `min`, `max`, `step_size`, and `value` properties. Users can make any of these properties read-only by omitting the corresponding setter method.
![Slider Component](../images/Slider_component.png)
- Accessing parent class resources in `NumberSlider`
In scenarios where you need the slider component to interact with or access resources from its parent class, you can achieve this by passing a callback function to it. This method avoids directly passing the entire parent class instance (`self`) and offers a more encapsulated approach. The callback function can be designed to utilize specific attributes or methods of the parent class, allowing the slider to perform actions or retrieve data in response to slider events.
Here's an illustrative example:
```python
from collections.abc import Callable
import pydase
import pydase.components
class MySlider(pydase.components.NumberSlider):
def __init__(
self,
value: float,
on_change: Callable[[float], None],
) -> None:
super().__init__(value=value)
self._on_change = on_change
# ... other properties ...
@property
def value(self) -> float:
return self._value
@value.setter
def value(self, new_value: float) -> None:
if new_value < self._min or new_value > self._max:
raise ValueError("Value is either below allowed min or above max value.")
self._value = new_value
self._on_change(new_value)
class MyService(pydase.DataService):
def __init__(self) -> None:
self.voltage = MySlider(
5,
on_change=self.handle_voltage_change,
)
def handle_voltage_change(self, new_voltage: float) -> None:
print(f"Voltage changed to: {new_voltage}")
# Additional logic here
if __name__ == "__main__":
service_instance = MyService()
my_service.voltage.value = 7 # Output: "Voltage changed to: 7"
pydase.Server(service_instance).run()
```
- Incorporating units in `NumberSlider`
The `NumberSlider` is capable of [displaying units](./Understanding-Units.md) alongside values, enhancing its usability in contexts where unit representation is crucial. When utilizing `pydase.units`, you can specify units for the slider's value, allowing the component to reflect these units in the frontend.
Here's how to implement a `NumberSlider` with unit display:
```python
import pydase
import pydase.components
import pydase.units as u
class MySlider(pydase.components.NumberSlider):
def __init__(
self,
value: u.Quantity = 0.0 * u.units.V,
) -> None:
super().__init__(value)
@property
def value(self) -> u.Quantity:
return self._value
@value.setter
def value(self, value: u.Quantity) -> None:
if value.m < self._min or value.m > self._max:
raise ValueError("Value is either below allowed min or above max value.")
self._value = value
class MyService(pydase.DataService):
def __init__(self) -> None:
super().__init__()
self.voltage = MySlider()
if __name__ == "__main__":
service_instance = MyService()
service_instance.voltage.value = 5 * u.units.V
print(service_instance.voltage.value) # Output: 5 V
pydase.Server(service_instance).run()
```
### `ColouredEnum`
This component provides a way to visually represent different states or categories in a data service using colour-coded options. It behaves similarly to a standard `Enum`, but the values encode colours in a format understood by CSS. The colours can be defined using various methods like Hexadecimal, RGB, HSL, and more.
If the property associated with the `ColouredEnum` has a setter function, the keys of the enum will be rendered as a dropdown menu, allowing users to interact and select different options. Without a setter function, the selected key will simply be displayed as a coloured box with text inside, serving as a visual indicator.
```python
import pydase
import pydase.components as pyc
class MyStatus(pyc.ColouredEnum):
PENDING = "#FFA500" # Hexadecimal colour (Orange)
RUNNING = "#0000FF80" # Hexadecimal colour with transparency (Blue)
PAUSED = "rgb(169, 169, 169)" # RGB colour (Dark Gray)
RETRYING = "rgba(255, 255, 0, 0.3)" # RGB colour with transparency (Yellow)
COMPLETED = "hsl(120, 100%, 50%)" # HSL colour (Green)
FAILED = "hsla(0, 100%, 50%, 0.7)" # HSL colour with transparency (Red)
CANCELLED = "SlateGray" # Cross-browser colour name (Slate Gray)
class StatusTest(pydase.DataService):
_status = MyStatus.RUNNING
@property
def status(self) -> MyStatus:
return self._status
@status.setter
def status(self, value: MyStatus) -> None:
# do something ...
self._status = value
# Modifying or accessing the status value:
my_service = StatusExample()
my_service.status = MyStatus.FAILED
```
![ColouredEnum Component](../images/ColouredEnum_component.png)
**Note** that each enumeration name and value must be unique.
This means that you should use different colour formats when you want to use a colour multiple times.
### Extending with New Components
Users can also extend the library by creating custom components. This involves defining the behavior on the Python backend and the visual representation on the frontend. For those looking to introduce new components, the [guide on adding components](https://pydase.readthedocs.io/en/latest/dev-guide/Adding_Components/) provides detailed steps on achieving this.

View File

@ -0,0 +1,211 @@
# Configuring `pydase`
## Do I Need to Configure My `pydase` Service?
`pydase` services work out of the box without requiring any configuration. However, you
might want to change some options, such as the web server port or logging level. To
accommodate such customizations, `pydase` allows configuration through environment
variables - avoiding hard-coded settings in your service code.
Why should you avoid hard-coding configurations? Here are two reasons:
1. **Security**:
Protect sensitive information, such as usernames and passwords. By using environment
variables, your service code can remain public while keeping private information
secure.
2. **Reusability**:
Services often need to be reused in different environments. For example, you might
deploy multiple instances of a service (e.g., for different sensors in a lab). By
separating configuration from code, you can adapt the service to new requirements
without modifying its codebase.
Next, well walk you through the environment variables `pydase` supports and provide an
example of how to separate service code from configuration.
## Configuring `pydase` Using Environment Variables
`pydase` provides the following environment variables for customization:
- **`ENVIRONMENT`**:
Defines the operation mode (`"development"` or `"production"`), which influences
behaviour such as logging (see [Logging in pydase](./Logging.md)).
- **`SERVICE_CONFIG_DIR`**:
Specifies the directory for configuration files (e.g., `web_settings.json`). Defaults
to the `config` folder in the service root. Access this programmatically using:
```python
import pydase.config
pydase.config.ServiceConfig().config_dir
```
- **`SERVICE_WEB_PORT`**:
Defines the web servers port. Ensure each service on the same host uses a unique
port. Default: `8001`.
- **`GENERATE_WEB_SETTINGS`**:
When `true`, generates or updates the `web_settings.json` file (see [Tailoring Frontend Component Layout](./interaction/Auto-generated-Frontend.md#tailoring-frontend-component-layout)).
Existing entries are preserved, and new entries are appended.
### Configuring `pydase` via Keyword Arguments
Some settings can also be overridden directly in your service code using keyword
arguments when initializing the server. This allows for flexibility in code-based
configuration:
```python
import pathlib
from pydase import Server
from your_service_module import YourService
server = Server(
YourService(),
web_port=8080, # Overrides SERVICE_WEB_PORT
config_dir=pathlib.Path("custom_config"), # Overrides SERVICE_CONFIG_DIR
generate_web_settings=True # Overrides GENERATE_WEB_SETTINGS
).run()
```
## Separating Service Code from Configuration
To decouple configuration from code, `pydase` utilizes `confz` for configuration
management. Below is an example that demonstrates how to configure a `pydase` service
for a sensor readout application.
### Scenario: Configuring a Sensor Service
Imagine you have multiple sensors distributed across your lab. You need to configure
each service instance with:
1. **Hostname**: The hostname or IP address of the sensor.
2. **Authentication Token**: A token or credentials to authenticate with the sensor.
3. **Readout Interval**: A periodic interval to read sensor data and log it to a
database.
Given the repository structure:
```bash title="Service Repository Structure"
my_sensor
├── pyproject.toml
├── README.md
└── src
└── my_sensor
├── my_sensor.py
├── config.py
├── __init__.py
└── __main__.py
```
Your service might look like this:
### Configuration
Define the configuration using `confz`:
```python title="src/my_sensor/config.py"
import confz
from pydase.config import ServiceConfig
class MySensorConfig(confz.BaseConfig):
instance_name: str
hostname: str
auth_token: str
readout_interval_s: float
CONFIG_SOURCES = confz.FileSource(file=ServiceConfig().config_dir / "config.yaml")
```
This class defines configurable parameters and loads values from a `config.yaml` file
located in the services configuration directory (which is configurable through an
environment variable, see [above](#configuring-pydase-using-environment-variables)).
A sample YAML file might look like this:
```yaml title="config.yaml"
instance_name: my-sensor-service-01
hostname: my-sensor-01.example.com
auth_token: my-secret-authentication-token
readout_interval_s: 5
```
### Service Implementation
Your service implementation might look like this:
```python title="src/my_sensor/my_sensor.py"
import asyncio
import http.client
import json
import logging
from typing import Any
import pydase.components
import pydase.units as u
from pydase.task.decorator import task
from my_sensor.config import MySensorConfig
logger = logging.getLogger(__name__)
class MySensor(pydase.DataService):
def __init__(self) -> None:
super().__init__()
self.readout_interval_s: u.Quantity = (
MySensorConfig().readout_interval_s * u.units.s
)
@property
def hostname(self) -> str:
"""Hostname of the sensor. Read-only."""
return MySensorConfig().hostname
def _get_data(self) -> dict[str, Any]:
"""Fetches sensor data via an HTTP GET request. It passes the authentication
token as "Authorization" header."""
connection = http.client.HTTPConnection(self.hostname, timeout=10)
connection.request(
"GET", "/", headers={"Authorization": MySensorConfig().auth_token}
)
response = connection.getresponse()
connection.close()
return json.loads(response.read())
@task(autostart=True)
async def get_and_log_sensor_values(self) -> None:
"""Periodically fetches and logs sensor data."""
while True:
try:
data = self._get_data()
# Write data to database using MySensorConfig().instance_name ...
except Exception as e:
logger.error(
"Error occurred, retrying in %s seconds. Error: %s",
self.readout_interval_s.m,
e,
)
await asyncio.sleep(self.readout_interval_s.m)
```
### Starting the Service
The service is launched via the `__main__.py` entry point:
```python title="src/my_sensor/__main__.py"
import pydase
from my_sensor.my_sensor import MySensor
pydase.Server(MySensor()).run()
```
You can now start the service with:
```bash
python -m my_sensor
```
This approach ensures the service is fully configured via the `config.yaml` file,
separating service logic from configuration.

View File

@ -0,0 +1,91 @@
# Logging in pydase
The `pydase` library organizes its loggers per module, mirroring the Python package hierarchy. This structured approach allows for granular control over logging levels and behaviour across different parts of the library. Logs can also include details about client identification based on headers sent by the client or proxy, providing additional context for debugging or auditing.
## Changing the pydase Log Level
You have two primary ways to adjust the log levels in `pydase`:
1. **Directly targeting `pydase` loggers**
You can set the log level for any `pydase` logger directly in your code. This method is useful for fine-tuning logging levels for specific modules within `pydase`. For instance, if you want to change the log level of the main `pydase` logger or target a submodule like `pydase.data_service`, you can do so as follows:
```python
# <your_script.py>
import logging
# Set the log level for the main pydase logger
logging.getLogger("pydase").setLevel(logging.INFO)
# Optionally, target a specific submodule logger
# logging.getLogger("pydase.data_service").setLevel(logging.DEBUG)
# Your logger for the current script
from pydase.utils.logging import configure_logging_with_pydase_formatter
configure_logging_with_pydase_formatter(level=logging.DEBUG)
logger = logging.getLogger(__name__)
logger.debug("My debug message.")
```
This approach allows for specific control over different parts of the `pydase` library, depending on your logging needs.
2. **Using the `ENVIRONMENT` environment variable**
For a more global setting that affects the entire `pydase` library, you can utilize the `ENVIRONMENT` environment variable. Setting this variable to `"production"` will configure all `pydase` loggers to only log messages of level `"INFO"` and above, filtering out more verbose logging. This is particularly useful for production environments where excessive logging can be overwhelming or unnecessary.
```bash
ENVIRONMENT="production" python -m <module_using_pydase>
```
In the absence of this setting, the default behavior is to log everything of level `"DEBUG"` and above, suitable for development environments where more detailed logs are beneficial.
## Client Identification in pydase Logs
The logging system in `pydase` includes information about clients based on headers sent by the client or a proxy. The priority for identifying the client is fixed and as follows:
1. **`Remote-User` Header**: This header is typically set by authentication servers like [Authelia](https://www.authelia.com/). While it can be set manually by users, its primary purpose is to provide client information authenticated through such servers.
2. **`X-Client-ID` Header**: This header is intended for use by Python clients to pass custom client identification information. It acts as a fallback when the `Remote-User` header is not available.
3. **Default Socket.IO Session ID**: If neither of the above headers is present, the system falls back to the default Socket.IO session ID to identify the client.
For example, a log entries might include the following details based on the available headers:
```plaintext
2025-01-20 06:47:50.940 | INFO | pydase.server.web_server.api.v1.application:_get_value:36 - Client [id=This is me!] is getting the value of 'property_attr'
2025-01-20 06:48:13.710 | INFO | pydase.server.web_server.api.v1.application:_get_value:36 - Client [user=Max Muster] is getting the value of 'property_attr'
```
## Configuring Logging in Services
To configure logging in services built with `pydase`, use the helper function [`configure_logging_with_pydase_formatter`][pydase.utils.logging.configure_logging_with_pydase_formatter]. This function sets up a logger with the same formatting used internally by `pydase`, so your service logs match the style and structure of `pydase` logs.
### Example
If your service follows a typical layout like:
```text
└── src
└── my_service
├── __init__.py
└── ...
```
you should call `configure_logging_with_pydase_formatter` inside `src/my_service/__init__.py`. This ensures the logger is configured as soon as your service is imported, and before any log messages are emitted.
```python title="src/my_service/__init__.py"
import sys
from pydase.utils.logging import configure_logging_with_pydase_formatter
configure_logging_with_pydase_formatter(
name="my_service", # Use the package/module name or None for the root logger
level=logging.DEBUG, # Set the desired logging level (defaults to INFO)
stream=sys.stderr # Optional: set the output stream (stderr by default)
)
```
### Notes
- If you pass `name=None`, the root logger will be configured. This affects **all logs** that propagate to the root logger.
- Passing a specific `name` like `"my_service"` allows you to scope the configuration to your service only, which is safer in multi-library environments.
- You can use `sys.stdout` instead of `sys.stderr` if your logs are being captured or processed differently (e.g., in containers or logging systems).

View File

@ -0,0 +1,66 @@
# Understanding Service Persistence
`pydase` allows you to easily persist the state of your service by saving it to a file. This is especially useful when you want to maintain the service's state across different runs.
To enable persistence, pass a `filename` keyword argument to the constructor of the [`pydase.Server`][pydase.Server] class. The `filename` specifies the file where the state will be saved:
- If the file **does not exist**, it will be created and populated with the current state when the service shuts down or saves.
- If the file **already exists**, the state manager will **load** the saved values into the service at startup.
Heres an example:
```python
import pydase
class Device(pydase.DataService):
# ... define your service class ...
if __name__ == "__main__":
service = Device()
pydase.Server(service=service, filename="device_state.json").run()
```
In this example, the service state will be automatically loaded from `device_state.json` at startup (if it exists), and saved to the same file periodically and upon shutdown.
## Automatic Periodic State Saving
When a `filename` is provided, `pydase` automatically enables **periodic autosaving** of the service state to that file. This ensures that the current state is regularly persisted, reducing the risk of data loss during unexpected shutdowns.
The autosave happens every 30 seconds by default. You can customize the interval using the `autosave_interval` argument (in seconds):
```python
pydase.Server(
service=service,
filename="device_state.json",
autosave_interval=10.0, # save every 10 seconds
).run()
```
To disable automatic saving, set `autosave_interval` to `None`.
## Controlling Property State Loading with `@load_state`
By default, the state manager only restores values for public attributes of your service (i.e. *it does not restore property values*). If you have properties that you want to control the loading for, you can use the [`@load_state`][pydase.data_service.state_manager.load_state] decorator on your property setters. This indicates to the state manager that the value of the property should be loaded from the state file.
Example:
```python
import pydase
from pydase.data_service.state_manager import load_state
class Device(pydase.DataService):
_name = "Default Device Name"
@property
def name(self) -> str:
return self._name
@name.setter
@load_state
def name(self, value: str) -> None:
self._name = value
```
With the `@load_state` decorator applied to the `name` property setter, the state manager will load and apply the `name` property's value from the file upon server startup.
**Note**: If the structure of your service class changes between saves, only properties decorated with `@load_state` and unchanged public attributes will be restored safely.

82
docs/user-guide/Tasks.md Normal file
View File

@ -0,0 +1,82 @@
# Understanding Tasks
In `pydase`, a task is defined as an asynchronous function without arguments that is decorated with the [`@task`][pydase.task.decorator.task] decorator and contained in a class that inherits from [`pydase.DataService`][pydase.DataService]. These tasks usually contain a while loop and are designed to carry out periodic functions. For example, a task might be used to periodically read sensor data, update a database, or perform any other recurring job.
`pydase` allows you to control task execution via both the frontend and Python clients and can automatically start tasks upon initialization of the service. By using the [`@task`][pydase.task.decorator.task] decorator with the `autostart=True` argument in your service class, `pydase` will automatically start these tasks when the server is started. Here's an example:
```python
import pydase
from pydase.task.decorator import task
class SensorService(pydase.DataService):
def __init__(self):
super().__init__()
self.readout_frequency = 1.0
def _process_data(self, data: ...) -> None:
...
def _read_from_sensor(self) -> Any:
...
@task(autostart=True)
async def read_sensor_data(self):
while True:
data = self._read_from_sensor()
self._process_data(data) # Process the data as needed
await asyncio.sleep(self.readout_frequency)
if __name__ == "__main__":
service = SensorService()
pydase.Server(service=service).run()
```
In this example, `read_sensor_data` is a task that continuously reads data from a sensor. By decorating it with `@task(autostart=True)`, it will automatically start running when `pydase.Server(service).run()` is executed.
## Task Lifecycle Control
The [`@task`][pydase.task.decorator.task] decorator replaces the function with a task object that has `start()` and `stop()` methods. This means you can control the task execution directly using these methods. For instance, you can manually start or stop the task by calling `service.read_sensor_data.start()` and `service.read_sensor_data.stop()`, respectively.
## Advanced Task Options
The [`@task`][pydase.task.decorator.task] decorator supports several options inspired by systemd unit services, allowing fine-grained control over task behavior:
- **`autostart`**: Automatically starts the task when the service initializes. Defaults to `False`.
- **`restart_on_exception`**: Configures whether the task should restart if it exits due to an exception (other than `asyncio.CancelledError`). Defaults to `True`.
- **`restart_sec`**: Specifies the delay (in seconds) before restarting a failed task. Defaults to `1.0`.
- **`start_limit_interval_sec`**: Configures a time window (in seconds) for rate limiting task restarts. If the task restarts more than `start_limit_burst` times within this interval, it will no longer restart. Defaults to `None` (disabled).
- **`start_limit_burst`**: Defines the maximum number of restarts allowed within the interval specified by `start_limit_interval_sec`. Defaults to `3`.
- **`exit_on_failure`**: If set to `True`, the service will exit if the task fails and either `restart_on_exception` is `False` or the start rate limiting is exceeded. Defaults to `False`.
### Example with Advanced Options
Here is an example showcasing advanced task options:
```python
import pydase
from pydase.task.decorator import task
class AdvancedTaskService(pydase.DataService):
def __init__(self):
super().__init__()
@task(
autostart=True,
restart_on_exception=True,
restart_sec=2.0,
start_limit_interval_sec=10.0,
start_limit_burst=5,
exit_on_failure=True,
)
async def critical_task(self):
while True:
raise Exception("Critical failure")
if __name__ == "__main__":
service = AdvancedTaskService()
pydase.Server(service=service).run()
```

View File

@ -0,0 +1,64 @@
# Understanding Units
`pydase` integrates with the [`pint`](https://pint.readthedocs.io/en/stable/) package to allow you to work with physical quantities within your service. This enables you to define attributes with units, making your service more expressive and ensuring consistency in the handling of physical quantities.
You can define quantities in your `pydase.DataService` subclass using the `pydase.units` module.
Here's an example:
```python
from typing import Any
import pydase
import pydase.units as u
class ServiceClass(pydase.DataService):
voltage = 1.0 * u.units.V
_current: u.Quantity = 1.0 * u.units.mA
@property
def current(self) -> u.Quantity:
return self._current
@current.setter
def current(self, value: u.Quantity) -> None:
self._current = value
if __name__ == "__main__":
service = ServiceClass()
service.voltage = 10.0 * u.units.V
service.current = 1.5 * u.units.mA
pydase.Server(service=service).run()
```
In the frontend, quantities are rendered as floats, with the unit displayed as additional text. This allows you to maintain a clear and consistent representation of physical quantities across both the backend and frontend of your service.
![Web interface with rendered units](../images/Units_App.png)
Should you need to access the magnitude or the unit of a quantity, you can use the `.m` attribute or the `.u` attribute of the variable, respectively. For example, this could be necessary to set the periodicity of a task:
```python
import asyncio
import pydase
import pydase.units as u
class ServiceClass(pydase.DataService):
readout_wait_time = 1.0 * u.units.ms
async def read_sensor_data(self):
while True:
print("Reading out sensor ...")
await asyncio.sleep(self.readout_wait_time.to("s").m)
if __name__ == "__main__":
service = ServiceClass()
pydase.Server(service=service).run()
```
For more information about what you can do with the units, please consult the documentation of [`pint`](https://pint.readthedocs.io/en/stable/).

View File

@ -0,0 +1,38 @@
# Using `validate_set` to Validate Property Setters
The `validate_set` decorator ensures that a property setter reads back the set value using the property getter and checks it against the desired value.
This decorator can be used to validate that a parameter has been correctly set on a device within a specified precision and timeout.
The decorator takes two keyword arguments: `timeout` and `precision`. The `timeout` argument specifies the maximum time (in seconds) to wait for the value to be within the precision boundary.
If the value is not within the precision boundary after this time, an exception is raised.
The `precision` argument defines the acceptable deviation from the desired value.
If `precision` is `None`, the value must be exact.
For example, if `precision` is set to `1e-5`, the value read from the device must be within ±0.00001 of the desired value.
Heres how to use the `validate_set` decorator in a `DataService` class:
```python
import pydase
from pydase.observer_pattern.observable.decorators import validate_set
class Service(pydase.DataService):
def __init__(self) -> None:
super().__init__()
self._device = RemoteDevice() # dummy class
@property
def value(self) -> float:
# Implement how to get the value from the remote device...
return self._device.value
@value.setter
@validate_set(timeout=1.0, precision=1e-5)
def value(self, value: float) -> None:
# Implement how to set the value on the remote device...
self._device.value = value
if __name__ == "__main__":
pydase.Server(service=Service()).run()
```

View File

@ -0,0 +1,59 @@
# Deploying Services Behind a Reverse Proxy
In some environments, you may need to deploy your services behind a reverse proxy. Typically, this involves adding a CNAME record for your service that points to the reverse proxy in your DNS server. The proxy then routes requests to the `pydase` backend on the appropriate web server port.
However, in scenarios where you dont control the DNS server, or where adding new CNAME records is time-consuming, `pydase` supports **service multiplexing** using a path prefix. This means multiple services can be hosted on a single CNAME (e.g., `services.example.com`), with each service accessible through a unique path such as `services.example.com/my-service`.
To ensure seamless operation, the reverse proxy must strip the path prefix (e.g., `/my-service`) from the request URL and forward it as the `X-Forwarded-Prefix` header. `pydase` then uses this header to dynamically adjust the frontend paths, ensuring all resources are correctly located.
## Example Deployment with Traefik
Below is an example setup using [Traefik](https://doc.traefik.io/traefik/), a widely-used reverse proxy. This configuration demonstrates how to forward requests for a `pydase` service using a path prefix.
### 1. Reverse Proxy Configuration
Save the following configuration to a file (e.g., `/etc/traefik/dynamic_conf/my-service-config.yml`):
```yaml
http:
routers:
my-service-route:
rule: PathPrefix(`/my-service`)
entryPoints:
- web
service: my-service
middlewares:
- strip-prefix
services:
my-service:
loadBalancer:
servers:
- url: http://127.0.0.1:8001
middlewares:
strip-prefix:
stripprefix:
prefixes: /my-service
```
This configuration:
- Routes requests with the path prefix `/my-service` to the `pydase` backend.
- Strips the prefix (`/my-service`) from the request URL using the `stripprefix` middleware.
- Forwards the stripped prefix as the `X-Forwarded-Prefix` header.
### 2. Static Configuration for Traefik
Ensure Traefik is set up to use the dynamic configuration. Add this to your Traefik static configuration (e.g., `/etc/traefik/traefik.yml`):
```yaml
providers:
file:
filename: /etc/traefik/dynamic_conf/my-service-config.yml
entrypoints:
web:
address: ":80"
```
### 3. Accessing the Service
Once configured, your `pydase` service will be accessible at `http://services.example.com/my-service`. The path prefix will be handled transparently by `pydase`, so you dont need to make any changes to your application code or frontend resources.

View File

@ -0,0 +1,48 @@
# Connecting Through a SOCKS5 Proxy
If your target service is only reachable via an SSH gateway or resides behind a
firewall, you can route your [`pydase.Client`][pydase.Client] connection through a local
SOCKS5 proxy. This is particularly useful in network environments where direct access to
the service is not possible.
## Setting Up a SOCKS5 Proxy
You can create a local [SOCKS5 proxy](https://en.wikipedia.org/wiki/SOCKS) using SSH's
`-D` option:
```bash
ssh -D 2222 user@gateway.example.com
```
This command sets up a SOCKS5 proxy on `localhost:2222`, securely forwarding traffic
over the SSH connection.
## Using the Proxy in Your Python Client
Once the proxy is running, configure the [`pydase.Client`][pydase.Client] to route
traffic through it using the `proxy_url` parameter:
```python
import pydase
client = pydase.Client(
url="ws://target-service:8001",
proxy_url="socks5://localhost:2222"
).proxy
```
* You can also use this setup with `wss://` URLs for encrypted WebSocket connections.
## Installing Required Dependencies
To use this feature, you must install the optional `socks` dependency group, which
includes [`aiohttp_socks`](https://pypi.org/project/aiohttp-socks/):
- `poetry`
```bash
poetry add "pydase[socks]"
```
- `pip`
```bash
pip install "pydase[socks]"
```

View File

@ -0,0 +1,196 @@
# Auto-generated Frontend
`pydase` automatically generates a frontend interface based on your service definition, representing the current state and controls of the service.
It simplifies the process of visualization and control of the data and devices managed by your `pydase` service, making it accessible to both developers and end-users.
Through the integration of Socket.IO, the frontend provides real-time updates, reflecting changes as they occur and allowing for immediate interaction with the backend.
## Accessing the Frontend
You can access the auto-generated frontend by navigating to the hostname of the device the service is hosted on, followed by the exposed port:
```
http://<hostname>:<port>/
```
The frontend uses a component-based approach, representing various data types and control mechanisms as distinct UI components. For more information about this, please refer to [Components Guide](../Components.md).
## Customization Options
`pydase` allows you to enhance the user experience by customizing the web interface's appearance through
1. a custom CSS file, and
2. a custom favicon image, and
3. tailoring the frontend component layout and display style.
For more advanced customization, you can provide a completely custom frontend source.
### Custom CSS Styling
You can apply your own styles globally across the web interface by passing a custom CSS file to the server during initialization.
Here's how you can use this feature:
1. Prepare your custom CSS file with the desired styles.
2. When initializing your server, use the `css` parameter of the `Server` class to specify the path to your custom CSS file.
```python
from pydase import Server, DataService
class MyService(DataService):
# ... your service definition ...
if __name__ == "__main__":
service = MyService()
server = Server(service, css="path/to/your/custom.css").run()
```
This will apply the styles defined in `custom.css` to the web interface, allowing you to maintain branding consistency or improve visual accessibility.
Please ensure that the CSS file path is accessible from the server's running location. Relative or absolute paths can be used depending on your setup.
### Custom favicon image
You can customize the favicon displayed in the browser tab by providing your own favicon image file during the server initialization.
Here's how you can use this feature:
1. Prepare your custom favicon image (e.g. a `.png` file).
2. Pass the path to your favicon file as the `favicon_path` argument when initializing the `Server` class.
Heres an example:
```python
import pydase
class MyService(pydase.DataService):
# ... your service definition ...
if __name__ == "__main__":
service = MyService()
pydase.Server(service, favicon_path="./my/local/my-favicon.png").run()
```
This will serve the specified image instead of the default `pydase` logo.
### Tailoring Frontend Component Layout
You can customize the display names, visibility, and order of components via the `web_settings.json` file.
Each key in the file corresponds to the full access path of public attributes, properties, and methods of the exposed service, using dot-notation.
- **Custom Display Names**: Modify the `"displayName"` value in the file to change how each component appears in the frontend.
- **Control Component Visibility**: Utilize the `"display"` key-value pair to control whether a component is rendered in the frontend. Set the value to `true` to make the component visible or `false` to hide it.
- **Adjustable Component Order**: The `"displayOrder"` values determine the order of components. Alter these values to rearrange the components as desired. The value defaults to [`Number.MAX_SAFE_INTEGER`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER).
The `web_settings.json` file will be stored in the directory specified by the `SERVICE_CONFIG_DIR` environment variable. You can generate a `web_settings.json` file by setting the `GENERATE_WEB_SETTINGS` to `True`. For more information, see the [configuration section](../Configuration.md).
For example, styling the following service
```python
import pydase
class Device(pydase.DataService):
name = "My Device"
temperature = 1.0
power = 1
class Service(pydase.DataService):
device = Device()
state = "RUNNING"
if __name__ == "__main__":
pydase.Server(Service()).run()
```
with the following `web_settings.json`
```json
{
"device": {
"displayName": "My Device",
"displayOrder": 1
},
"device.name": {
"display": false
},
"device.power": {
"displayName": "Power",
"displayOrder": 1
},
"device.temperature": {
"displayName": "Temperature",
"displayOrder": 0
},
"state": {
"displayOrder": 0
}
}
```
looks like this:
![Tailoring frontend component layout](../../images/Tailoring_frontend_component_layout.png)
### Specifying a Custom Frontend Source
To further customize your web interface, you can provide a custom frontend source.
By specifying the `frontend_src` parameter when initializing the server, you can host a tailored frontend application:
```python
from pathlib import Path
import pydase
class MyService(pydase.DataService):
# Service definition
if __name__ == "__main__":
service = MyService()
pydase.Server(
service,
frontend_src=Path("path/to/your/frontend/directory"),
).run()
```
`pydase` expects a directory structured as follows:
```bash title="Frontend directory structure"
<your_frontend_directory>
├── assets
│   └── ...
└── index.html
```
Any CSS, js, image or other files need to be put into the assets folder for the web server to be able to provide access to it.
#### Example: Custom React Frontend
You can use vite to generate a react app template:
```bash
npm create vite@latest my-react-app -- --template react
```
*TODO: Add some useful information here...*
To deploy the custom react frontend, build it with
```bash
npm run build
```
and pass the relative path of the output directory to the `frontend_src` parameter of the `pydase.Server`.
**Note** that you have to make sure that all the generated files (except the `index.html`) are in the `assets` folder. In the react app, you can achieve this by not using the `public` folder, but instead using e.g. `src/assets`.

View File

@ -0,0 +1,97 @@
# Python RPC Client
The [`pydase.Client`][pydase.Client] allows you to connect to a remote `pydase` service using Socket.IO, facilitating interaction with the service as though it were running locally.
## Basic Usage
```python
import pydase
# Replace <ip_addr> and <service_port> with the appropriate values for your service
client_proxy = pydase.Client(url="ws://<ip_addr>:<service_port>").proxy
# For SSL-encrypted services, use the wss protocol
# client_proxy = pydase.Client(url="wss://your-domain.ch").proxy
# Interact with the service attributes as if they were local
client_proxy.voltage = 5.0
print(client_proxy.voltage) # Expected output: 5.0
```
This example shows how to set and retrieve the `voltage` attribute through the client proxy.
The proxy acts as a local representation of the remote service, enabling intuitive interaction.
The proxy class automatically synchronizes with the server's attributes and methods, keeping itself up-to-date with any changes. This dynamic synchronization essentially mirrors the server's API, making it feel like you're working with a local object.
### Accessing Services Behind Firewalls or SSH Gateways
If your service is only reachable through a private network or SSH gateway, you can route your connection through a local SOCKS5 proxy using the `proxy_url` parameter.
See [Connecting Through a SOCKS5 Proxy](../advanced/SOCKS-Proxy.md) for details.
## Context Manager Support
You can also use the client within a context manager, which automatically handles connection management (i.e., opening and closing the connection):
```python
import pydase
with pydase.Client(url="ws://localhost:8001") as client:
client.proxy.my_method()
```
Using the context manager ensures that connections are cleanly closed once the block of code finishes executing.
## Tab Completion Support
In interactive environments like Python interpreters or Jupyter notebooks, the proxy supports tab completion. This allows users to explore available methods and attributes.
## Integrating the Client into Another Service
You can integrate a `pydase` client proxy within another service. Here's an example of how to set this up:
```python
import pydase
class MyService(pydase.DataService):
proxy = pydase.Client(
url="ws://<ip_addr>:<service_port>",
block_until_connected=False,
client_id="my_pydase_client_id", # optional, defaults to system hostname
).proxy
# For SSL-encrypted services, use the wss protocol
# proxy = pydase.Client(
# url="wss://your-domain.ch",
# block_until_connected=False,
# client_id="my_pydase_client_id",
# ).proxy
if __name__ == "__main__":
service = MyService()
# Create a server that exposes this service
server = pydase.Server(service, web_port=8002).run()
```
In this example:
- The `MyService` class has a `proxy` attribute that connects to a `pydase` service at `<ip_addr>:<service_port>`.
- By setting `block_until_connected=False`, the service can start without waiting for the connection to succeed.
- The `client_id` is optional. If not specified, it defaults to the system hostname, which will be sent in the `X-Client-Id` HTTP header for logging or authentication on the server side.
## Custom `socketio.AsyncClient` Connection Parameters
You can configure advanced connection options by passing arguments to the underlying [`AsyncClient`][socketio.AsyncClient] via `sio_client_kwargs`. For example:
```python
client = pydase.Client(
url="ws://localhost:8001",
sio_client_kwargs={
"reconnection_attempts": 3,
"reconnection_delay": 2,
"reconnection_delay_max": 10,
}
).proxy
```
In this setup, the client will attempt to reconnect three times, with an initial delay of 2 seconds (each successive attempt doubles this delay) and a maximum delay of 10 seconds between attempts.

View File

@ -0,0 +1,7 @@
# Interacting with `pydase` Services
`pydase` offers multiple ways for users to interact with the services they create.
- [Auto-generated Frontend](./Auto-generated-Frontend.md)
- [RESTful API](./RESTful-API.md)
- [Python Client](./Python-Client.md)

View File

@ -0,0 +1,22 @@
# RESTful API
The `pydase` RESTful API allows for standard HTTP-based interactions and provides access to various functionalities through specific routes. This is particularly useful for integrating `pydase` services with other applications or for scripting and automation.
For example, you can get a value like this:
```python
import json
import requests
response = requests.get(
"http://<hostname>:<port>/api/v1/get_value?access_path=<full_access_path>"
)
serialized_value = json.loads(response.text)
```
To help developers understand and utilize the API, we provide an OpenAPI specification. This specification describes the available endpoints and corresponding request/response formats.
## OpenAPI Specification
<swagger-ui src="./openapi.yaml"/>

View File

@ -0,0 +1,326 @@
openapi: 3.1.0
info:
version: 1.0.0
title: pydase API
tags:
- name: /api/v1
description: Version 1
paths:
/api/v1/get_value:
get:
tags:
- /api/v1
summary: Get the value of an existing attribute.
description: Get the value of an existing attribute by full access path.
operationId: getValue
parameters:
- in: query
name: access_path
schema:
type: string
example: device.channel[0].voltage
required: true
description: Full access path of the service attribute.
responses:
'200':
description: Successful Operation
content:
application/json:
schema:
$ref: '#/components/schemas/SerializedAttribute'
examples:
Exists:
summary: Attribute exists
value:
docs: My documentation string.
full_access_path: device.channel[0].voltage
readonly: false
type: float
value: 12.1
'400':
description: Could not get attribute
content:
application/json:
schema:
$ref: '#/components/schemas/SerializedException'
examples:
Attribute:
summary: Attribute does not exist
value:
docs: null
full_access_path: ""
name: AttributeError
readonly: true
type: Exception
value: "'MyService' object has no attribute 'invalid_attribute'"
List:
summary: List index out of range
value:
docs: null
full_access_path: ""
name: IndexError
readonly: true
type: Exception
value: "list index out of range"
/api/v1/update_value:
put:
tags:
- /api/v1
summary: Update an existing attribute.
description: Update an existing attribute by full access path.
operationId: updateValue
requestBody:
description: Update an existent attribute in the service
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateValue'
required: true
responses:
'200':
description: Successful Operation
'400':
description: Could not Update Attribute
content:
application/json:
schema:
$ref: '#/components/schemas/SerializedException'
examples:
Attribute:
summary: Attribute does not exist
value:
docs: null
full_access_path: ""
name: AttributeError
readonly: true
type: Exception
value: "'MyService' object has no attribute 'invalid_attribute'"
ReadOnly:
summary: Attribute is read-only
value:
docs: null
full_access_path: ""
name: AttributeError
readonly: true
type: Exception
value: "property 'readonly_property' of 'MyService' object has no setter"
List:
summary: List index out of range
value:
docs: null
full_access_path: ""
name: IndexError
readonly: true
type: Exception
value: "list index out of range"
/api/v1/trigger_method:
put:
tags:
- /api/v1
summary: Trigger method.
description: Trigger method with by full access path with provided args and kwargs.
operationId: triggerMethod
requestBody:
description: Update an existent attribute in the service
content:
application/json:
schema:
$ref: '#/components/schemas/TriggerMethod'
required: true
responses:
'200':
description: Successful Operation
content:
application/json:
schema:
$ref: '#/components/schemas/SerializedAttribute'
examples:
NoneReturn:
summary: Function returns None
value:
docs: null
full_access_path: ""
readonly: false
type: "NoneType"
value: null
FloatReturn:
summary: Function returns float
value:
docs: null
full_access_path: ""
readonly: false
type: "float"
value: 23.2
'400':
description: Method does not exist
content:
application/json:
schema:
$ref: '#/components/schemas/SerializedException'
examples:
Args:
summary: Wrong number of arguments
value:
docs: null
full_access_path: ""
name: TypeError
readonly: true
type: Exception
value: "MyService.some_function() takes 1 positional argument but 2 were given"
Attribute:
summary: Attribute does not exist
value:
docs: null
full_access_path: ""
name: AttributeError
readonly: true
type: Exception
value: "'MyService' object has no attribute 'invalid_method'"
List:
summary: List index out of range
value:
docs: null
full_access_path: ""
name: IndexError
readonly: true
type: Exception
value: "list index out of range"
Dict:
summary: Dictionary key does not exist
value:
docs: null
full_access_path: ""
name: KeyError
readonly: true
type: Exception
value: "invalid_key"
components:
schemas:
UpdateValue:
required:
- access_path
- value
type: object
properties:
access_path:
type: string
example: device.channel[0].voltage
value:
$ref: '#/components/schemas/SerializedValue'
TriggerMethod:
required:
- access_path
type: object
properties:
access_path:
type: string
example: device.channel[0].voltage
args:
type: object
required:
- type
- value
- full_access_path
properties:
full_access_path:
type: string
example: ""
type:
type: string
enum:
- list
value:
type: array
items:
$ref: '#/components/schemas/SerializedValue'
kwargs:
type: object
required:
- type
- value
- full_access_path
properties:
full_access_path:
type: string
example: ""
type:
type: string
enum:
- dict
value:
type: object
additionalProperties:
$ref: '#/components/schemas/SerializedValue'
SerializedValue:
required:
- full_access_path
- type
- value
type: object
properties:
docs:
type: string | null
example: null
full_access_path:
type: string
example: ""
readonly:
type: boolean
example: false
type:
type: string
example: float
value:
type: any
example: 22.0
SerializedAttribute:
required:
- full_access_path
- type
- value
type: object
properties:
docs:
type: string | null
example: My documentation string.
full_access_path:
type: string
example: device.channel[0].voltage
readonly:
type: boolean
example: false
type:
type: string
example: float
value:
type: any
example: 22.0
SerializedException:
required:
- full_access_path
- type
- value
type: object
properties:
docs:
type: string | null
example: Raised when the access path does not correspond to a valid attribute.
full_access_path:
type: string
example: ""
name:
type: string
example: SerializationPathError
readonly:
type: boolean
example: true
type:
type: string
example: Exception
value:
type: string
examples:
value:
"Index '2': list index out of range"
some:
"Index '2': list index out of range"

View File

@ -1,16 +0,0 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint",
"prettier"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"rules": {
"prettier/prettier": "error"
}
}

38
frontend/.gitignore vendored
View File

@ -1,20 +1,24 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -1,11 +1,9 @@
{
"arrowParens": "always",
"bracketSameLine": true,
"endOfLine": "auto",
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"vueIndentScriptAndStyle": true,
"printWidth": 88,
"trailingComma": "none"
"arrowParens": "always",
"bracketSameLine": true,
"endOfLine": "auto",
"semi": true,
"singleQuote": false,
"tabWidth": 2,
"printWidth": 88
}

View File

@ -1,70 +1,30 @@
# Getting Started with Create React App
# React + TypeScript + Vite
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
## Available Scripts
Currently, two official plugins are available:
In the project directory, you can run:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
### `npm start`
## Expanding the ESLint configuration
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
The page will reload when you make changes.\
You may also see any lint errors in the console.
- Configure the top-level `parserOptions` property like this:
### `npm test`
```js
export default {
// other rules...
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: ['./tsconfig.json', './tsconfig.node.json'],
tsconfigRootDir: __dirname,
},
}
```
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
### Analyzing the Bundle Size
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
### Making a Progressive Web App
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
### Advanced Configuration
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
### Deployment
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
### `npm run build` fails to minify
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list

24
frontend/eslint.config.js Normal file
View File

@ -0,0 +1,24 @@
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
import reactRecommended from "eslint-plugin-react/configs/recommended.js";
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
...tseslint.configs.stylistic,
{
files: ["**/*.{js,jsx,ts,tsx}"],
...reactRecommended,
languageOptions: {
parser: tseslint.parser,
},
rules: {
"prettier/prettier": "error",
"react/react-in-jsx-scope": "off",
"react/prop-types": "off",
"@typescript-eslint/no-empty-function": "off",
},
},
eslintPluginPrettierRecommended,
);

24
frontend/index.html Normal file
View File

@ -0,0 +1,24 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Web site displaying a pydase UI." />
</head>
<script>
// this will be set by the python backend if the service is behind a proxy which strips a prefix. The frontend can use this to build the paths to the resources.
window.__FORWARDED_PREFIX__ = "";
window.__FORWARDED_PROTO__ = "";
</script>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>

25158
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,58 +1,40 @@
{
"name": "pydase",
"version": "0.1.0",
"private": true,
"dependencies": {
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@fsouza/prettierd": "^0.25.1",
"@mui/material": "^5.14.1",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"bootstrap": "^5.3.0",
"react": "^18.2.0",
"react-bootstrap": "^2.8.0",
"react-bootstrap-icons": "^1.10.3",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"socket.io-client": "^4.7.1",
"web-vitals": "^3.4.0"
},
"scripts": {
"start": "NODE_ENV=development react-scripts start",
"build": "BUILD_PATH='../src/pydase/frontend' react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@types/node": "^20.0.0",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.9.0",
"eslint": "^8.52.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.1",
"prettier": "^3.0.3",
"typescript": "^4.9.0"
}
"name": "pydase",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build --emptyOutDir",
"lint": "eslint . --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@emotion/styled": "^11.14.0",
"@mui/material": "^5.16.14",
"bootstrap": "^5.3.3",
"deep-equal": "^2.2.3",
"react": "^19.0.0",
"react-bootstrap": "^2.10.7",
"react-bootstrap-icons": "^1.11.5",
"socket.io-client": "^4.8.1"
},
"devDependencies": {
"@eslint/js": "^9.18.0",
"@types/deep-equal": "^1.0.4",
"@types/eslint__js": "^8.42.3",
"@types/node": "^20.17.14",
"@types/react": "^19.0.7",
"@types/react-dom": "^19.0.3",
"@typescript-eslint/eslint-plugin": "^7.15.0",
"@vitejs/plugin-react-swc": "^3.7.2",
"eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.3",
"eslint-plugin-react": "^7.37.4",
"prettier": "3.3.2",
"typescript": "^5.7.3",
"typescript-eslint": "^7.18.0",
"vite": "^5.4.12"
}
}

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

View File

@ -1,43 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site displaying a pydase UI."
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>pydase App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View File

@ -1,25 +0,0 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -5,11 +5,6 @@ body {
input.instantUpdate {
background-color: rgba(255, 0, 0, 0.1);
}
.numberComponentButton {
padding: 0.15em 6px !important;
font-size: 0.70rem !important;
}
.navbarOffset {
padding-top: 60px !important;
}
@ -17,26 +12,41 @@ input.instantUpdate {
position: fixed !important;
padding: 5px;
}
.debugToast, .infoToast {
.debugToast,
.infoToast {
background-color: rgba(114, 214, 253, 0.5) !important;
}
.warningToast {
background-color: rgba(255, 181, 44, 0.603) !important;
}
.errorToast, .criticalToast {
.errorToast,
.criticalToast {
background-color: rgba(216, 41, 18, 0.678) !important;
}
.buttonComponent {
.component {
position: relative;
float: left !important;
margin-right: 10px !important;
padding: 5px !important;
z-index: 1;
}
.stringComponent {
.dataServiceComponent {
width: 100%;
}
.deviceConnectionComponent {
position: relative;
float: left !important;
margin-right: 10px !important;
width: 100%;
z-index: 1;
}
.numberComponent {
float: left !important;
margin-right: 10px !important;
width: 270px !important;
.overlayContent {
position: absolute;
inset: 5px; /* (see https://developer.mozilla.org/en-US/docs/Web/CSS/inset) */
background: rgba(155, 155, 155, 0.75);
z-index: 2;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column; /* Stack children vertically */
border-radius: var(--bs-border-radius);
border: var(--bs-border-width) solid var(--bs-border-color-translucent)
}

View File

@ -1,40 +1,50 @@
import { useCallback, useEffect, useReducer, useRef, useState } from 'react';
import { Navbar, Form, Offcanvas, Container } from 'react-bootstrap';
import { hostname, port, socket } from './socket';
import {
DataServiceComponent,
DataServiceJSON
} from './components/DataServiceComponent';
import './App.css';
import { useCallback, useEffect, useReducer, useState } from "react";
import { Navbar, Form, Offcanvas, Container } from "react-bootstrap";
import { authority, socket, forwardedProto } from "./socket";
import "./App.css";
import {
Notifications,
Notification,
LevelName
} from './components/NotificationsComponent';
import { ConnectionToast } from './components/ConnectionToast';
import { SerializedValue, setNestedValueByPath, State } from './utils/stateUtils';
LevelName,
} from "./components/NotificationsComponent";
import { ConnectionToast } from "./components/ConnectionToast";
import { setNestedValueByPath, State } from "./utils/stateUtils";
import { WebSettingsContext, WebSetting } from "./WebSettings";
import { GenericComponent } from "./components/GenericComponent";
import { SerializedObject } from "./types/SerializedObject";
import useLocalStorage from "./hooks/useLocalStorage";
type Action =
| { type: 'SET_DATA'; data: State }
| { type: "SET_DATA"; data: State }
| {
type: 'UPDATE_ATTRIBUTE';
type: "UPDATE_ATTRIBUTE";
fullAccessPath: string;
newValue: SerializedValue;
newValue: SerializedObject;
};
type UpdateMessage = {
data: { full_access_path: string; value: SerializedValue };
};
type LogMessage = {
interface UpdateMessage {
data: { full_access_path: string; value: SerializedObject };
}
interface LogMessage {
levelname: LevelName;
message: string;
};
}
const reducer = (state: State, action: Action): State => {
const reducer = (state: State | null, action: Action): State | null => {
switch (action.type) {
case 'SET_DATA':
case "SET_DATA":
return action.data;
case 'UPDATE_ATTRIBUTE': {
return setNestedValueByPath(state, action.fullAccessPath, action.newValue);
case "UPDATE_ATTRIBUTE": {
if (state === null) {
return null;
}
return {
...state,
value: setNestedValueByPath(
state.value as Record<string, SerializedObject>,
action.fullAccessPath,
action.newValue,
),
};
}
default:
throw new Error();
@ -42,64 +52,76 @@ const reducer = (state: State, action: Action): State => {
};
const App = () => {
const [state, dispatch] = useReducer(reducer, null);
const stateRef = useRef(state); // Declare a reference to hold the current state
const [isInstantUpdate, setIsInstantUpdate] = useState(false);
const [serviceName, setServiceName] = useState<string | null>(null);
const [webSettings, setWebSettings] = useState<Record<string, WebSetting>>({});
const [isInstantUpdate, setIsInstantUpdate] = useLocalStorage(
"isInstantUpdate",
false,
);
const [showSettings, setShowSettings] = useState(false);
const [showNotification, setShowNotification] = useState(false);
const [showNotification, setShowNotification] = useLocalStorage(
"showNotification",
false,
);
const [notifications, setNotifications] = useState<Notification[]>([]);
const [connectionStatus, setConnectionStatus] = useState('connecting');
// Keep the state reference up to date
useEffect(() => {
stateRef.current = state;
}, [state]);
const [connectionStatus, setConnectionStatus] = useState("connecting");
useEffect(() => {
// Allow the user to add a custom css file
fetch(`http://${hostname}:${port}/custom.css`)
fetch(`${forwardedProto}://${authority}/custom.css`, { credentials: "include" })
.then((response) => {
if (response.ok) {
// If the file exists, create a link element for the custom CSS
const link = document.createElement('link');
link.href = `http://${hostname}:${port}/custom.css`;
link.type = 'text/css';
link.rel = 'stylesheet';
const link = document.createElement("link");
link.href = `${forwardedProto}://${authority}/custom.css`;
link.type = "text/css";
link.rel = "stylesheet";
document.head.appendChild(link);
}
})
.catch(console.error); // Handle the error appropriately
socket.on('connect', () => {
socket.on("connect", () => {
// Fetch data from the API when the client connects
fetch(`http://${hostname}:${port}/service-properties`)
fetch(`${forwardedProto}://${authority}/service-properties`, {
credentials: "include",
})
.then((response) => response.json())
.then((data: State) => dispatch({ type: 'SET_DATA', data }));
setConnectionStatus('connected');
.then((data: State) => {
dispatch({ type: "SET_DATA", data });
setServiceName(data.name);
document.title = data.name; // Setting browser tab title
});
fetch(`${forwardedProto}://${authority}/web-settings`, { credentials: "include" })
.then((response) => response.json())
.then((data: Record<string, WebSetting>) => setWebSettings(data));
setConnectionStatus("connected");
});
socket.on('disconnect', () => {
setConnectionStatus('disconnected');
socket.on("disconnect", () => {
setConnectionStatus("disconnected");
setTimeout(() => {
// Only set "reconnecting" is the state is still "disconnected"
// E.g. when the client has already reconnected
setConnectionStatus((currentState) =>
currentState === 'disconnected' ? 'reconnecting' : currentState
currentState === "disconnected" ? "reconnecting" : currentState,
);
}, 2000);
});
socket.on('notify', onNotify);
socket.on('log', onLogMessage);
socket.on("notify", onNotify);
socket.on("log", onLogMessage);
return () => {
socket.off('notify', onNotify);
socket.off('log', onLogMessage);
socket.off("notify", onNotify);
socket.off("log", onLogMessage);
};
}, []);
// Adding useCallback to prevent notify to change causing a re-render of all
// components
const addNotification = useCallback(
(message: string, levelname: LevelName = 'DEBUG') => {
(message: string, levelname: LevelName = "DEBUG") => {
// Getting the current time in the required format
const timeStamp = new Date().toISOString().substring(11, 19);
// Adding an id to the notification to provide a way of removing it
@ -108,15 +130,15 @@ const App = () => {
// Custom logic for notifications
setNotifications((prevNotifications) => [
{ levelname, id, message, timeStamp },
...prevNotifications
...prevNotifications,
]);
},
[]
[],
);
const removeNotificationById = (id: number) => {
setNotifications((prevNotifications) =>
prevNotifications.filter((n) => n.id !== id)
prevNotifications.filter((n) => n.id !== id),
);
};
@ -129,9 +151,9 @@ const App = () => {
// Dispatching the update to the reducer
dispatch({
type: 'UPDATE_ATTRIBUTE',
type: "UPDATE_ATTRIBUTE",
fullAccessPath,
newValue
newValue,
});
}
@ -147,7 +169,7 @@ const App = () => {
<>
<Navbar expand={false} bg="primary" variant="dark" fixed="top">
<Container fluid>
<Navbar.Brand>Data Service App</Navbar.Brand>
<Navbar.Brand>{serviceName}</Navbar.Brand>
<Navbar.Toggle aria-controls="offcanvasNavbar" onClick={handleShowSettings} />
</Container>
</Navbar>
@ -184,12 +206,13 @@ const App = () => {
</Offcanvas>
<div className="App navbarOffset">
<DataServiceComponent
name={''}
props={state as DataServiceJSON}
isInstantUpdate={isInstantUpdate}
addNotification={addNotification}
/>
<WebSettingsContext.Provider value={webSettings}>
<GenericComponent
attribute={state as SerializedObject}
isInstantUpdate={isInstantUpdate}
addNotification={addNotification}
/>
</WebSettingsContext.Provider>
</div>
<ConnectionToast connectionStatus={connectionStatus} />
</>

View File

@ -0,0 +1,9 @@
import { createContext } from "react";
export const WebSettingsContext = createContext<Record<string, WebSetting>>({});
export interface WebSetting {
displayName: string;
display: boolean;
displayOrder: number;
}

View File

@ -1,109 +0,0 @@
import React, { useEffect, useRef } from 'react';
import { runMethod } from '../socket';
import { InputGroup, Form, Button } from 'react-bootstrap';
import { DocStringComponent } from './DocStringComponent';
import { getIdFromFullAccessPath } from '../utils/stringUtils';
import { LevelName } from './NotificationsComponent';
interface AsyncMethodProps {
name: string;
parentPath: string;
parameters: Record<string, string>;
value: Record<string, string>;
docString?: string;
hideOutput?: boolean;
addNotification: (message: string, levelname?: LevelName) => void;
}
export const AsyncMethodComponent = React.memo((props: AsyncMethodProps) => {
const { name, parentPath, docString, value: runningTask, addNotification } = props;
const renderCount = useRef(0);
const formRef = useRef(null);
const id = getIdFromFullAccessPath(parentPath.concat('.' + name));
useEffect(() => {
renderCount.current++;
// updates the value of each form control that has a matching name in the
// runningTask object
if (runningTask) {
const formElement = formRef.current;
if (formElement) {
Object.entries(runningTask).forEach(([name, value]) => {
const inputElement = formElement.elements.namedItem(name);
if (inputElement) {
inputElement.value = value;
}
});
}
}
}, [runningTask]);
useEffect(() => {
let message: string;
if (runningTask === null) {
message = `${parentPath}.${name} task was stopped.`;
} else {
const runningTaskEntries = Object.entries(runningTask)
.map(([key, value]) => `${key}: "${value}"`)
.join(', ');
message = `${parentPath}.${name} was started with parameters { ${runningTaskEntries} }.`;
}
addNotification(message);
}, [props.value]);
const execute = async (event: React.FormEvent) => {
event.preventDefault();
let method_name: string;
const kwargs: Record<string, unknown> = {};
if (runningTask !== undefined && runningTask !== null) {
method_name = `stop_${name}`;
} else {
Object.keys(props.parameters).forEach(
(name) => (kwargs[name] = event.target[name].value)
);
method_name = `start_${name}`;
}
runMethod(method_name, parentPath, kwargs);
};
const args = Object.entries(props.parameters).map(([name, type], index) => {
const form_name = `${name} (${type})`;
const value = runningTask && runningTask[name];
const isRunning = value !== undefined && value !== null;
return (
<InputGroup key={index}>
<InputGroup.Text className="component-label">{form_name}</InputGroup.Text>
<Form.Control
type="text"
name={name}
defaultValue={isRunning ? value : ''}
disabled={isRunning}
/>
</InputGroup>
);
});
return (
<div className="align-items-center asyncMethodComponent" id={id}>
{process.env.NODE_ENV === 'development' && (
<div>Render count: {renderCount.current}</div>
)}
<h5>
Function: {name}
<DocStringComponent docString={docString} />
</h5>
<Form onSubmit={execute} ref={formRef}>
{args}
<Button id={`button-${id}`} name={name} value={parentPath} type="submit">
{runningTask ? 'Stop' : 'Start'}
</Button>
</Form>
</div>
);
});

View File

@ -1,57 +1,68 @@
import React, { useEffect, useRef } from 'react';
import { ToggleButton } from 'react-bootstrap';
import { setAttribute } from '../socket';
import { DocStringComponent } from './DocStringComponent';
import { getIdFromFullAccessPath } from '../utils/stringUtils';
import { LevelName } from './NotificationsComponent';
import React, { useEffect } from "react";
import { ToggleButton } from "react-bootstrap";
import { DocStringComponent } from "./DocStringComponent";
import { LevelName } from "./NotificationsComponent";
import { SerializedObject } from "../types/SerializedObject";
import useRenderCount from "../hooks/useRenderCount";
interface ButtonComponentProps {
name: string;
parentPath?: string;
fullAccessPath: string;
value: boolean;
readOnly: boolean;
docString: string;
docString: string | null;
mapping?: [string, string]; // Enforce a tuple of two strings
addNotification: (message: string, levelname?: LevelName) => void;
changeCallback?: (value: SerializedObject, callback?: (ack: unknown) => void) => void;
displayName: string;
id: string;
}
export const ButtonComponent = React.memo((props: ButtonComponentProps) => {
const { name, parentPath, value, readOnly, docString, mapping, addNotification } =
props;
const buttonName = mapping ? (value ? mapping[0] : mapping[1]) : name;
const id = getIdFromFullAccessPath(parentPath.concat('.' + name));
const {
value,
fullAccessPath,
readOnly,
docString,
addNotification,
changeCallback = () => {},
displayName,
id,
} = props;
// const buttonName = props.mapping ? (value ? props.mapping[0] : props.mapping[1]) : name;
const renderCount = useRef(0);
const renderCount = useRenderCount();
useEffect(() => {
renderCount.current++;
});
useEffect(() => {
addNotification(`${parentPath}.${name} changed to ${value}.`);
addNotification(`${fullAccessPath} changed to ${value}.`);
}, [props.value]);
const setChecked = (checked: boolean) => {
setAttribute(name, parentPath, checked);
changeCallback({
type: "bool",
value: checked,
full_access_path: fullAccessPath,
readonly: readOnly,
doc: docString,
});
};
return (
<div className={'buttonComponent'} id={id}>
{process.env.NODE_ENV === 'development' && (
<div>Render count: {renderCount.current}</div>
)}
<div className={"component buttonComponent"} id={id}>
{process.env.NODE_ENV === "development" && <div>Render count: {renderCount}</div>}
<DocStringComponent docString={docString} />
<ToggleButton
id={`toggle-check-${id}`}
type="checkbox"
variant={value ? 'success' : 'secondary'}
variant={value ? "success" : "secondary"}
checked={value}
value={parentPath}
value={displayName}
disabled={readOnly}
onChange={(e) => setChecked(e.currentTarget.checked)}>
{buttonName}
{displayName}
<DocStringComponent docString={docString} />
</ToggleButton>
</div>
);
});
ButtonComponent.displayName = "ButtonComponent";

View File

@ -1,77 +0,0 @@
import React, { useEffect, useRef } from 'react';
import { InputGroup, Form, Row, Col } from 'react-bootstrap';
import { setAttribute } from '../socket';
import { DocStringComponent } from './DocStringComponent';
import { getIdFromFullAccessPath } from '../utils/stringUtils';
import { LevelName } from './NotificationsComponent';
interface ColouredEnumComponentProps {
name: string;
parentPath: string;
value: string;
docString?: string;
readOnly: boolean;
enumDict: Record<string, string>;
addNotification: (message: string, levelname?: LevelName) => void;
}
export const ColouredEnumComponent = React.memo((props: ColouredEnumComponentProps) => {
const {
name,
parentPath: parentPath,
value,
docString,
enumDict,
readOnly,
addNotification
} = props;
const renderCount = useRef(0);
const id = getIdFromFullAccessPath(parentPath.concat('.' + name));
useEffect(() => {
renderCount.current++;
});
useEffect(() => {
addNotification(`${parentPath}.${name} changed to ${value}.`);
}, [props.value]);
const handleValueChange = (newValue: string) => {
setAttribute(name, parentPath, newValue);
};
return (
<div className={'enumComponent'} id={id}>
{process.env.NODE_ENV === 'development' && (
<div>Render count: {renderCount.current}</div>
)}
<DocStringComponent docString={docString} />
<Row>
<Col className="d-flex align-items-center">
<InputGroup.Text>{name}</InputGroup.Text>
{readOnly ? (
// Display the Form.Control when readOnly is true
<Form.Control
value={value}
disabled={true}
style={{ backgroundColor: enumDict[value] }}
/>
) : (
// Display the Form.Select when readOnly is false
<Form.Select
aria-label="coloured-enum-select"
value={value}
style={{ backgroundColor: enumDict[value] }}
onChange={(event) => handleValueChange(event.target.value)}>
{Object.entries(enumDict).map(([key]) => (
<option key={key} value={key}>
{key}
</option>
))}
</Form.Select>
)}
</Col>
</Row>
</div>
);
});

View File

@ -1,9 +1,9 @@
import React, { useEffect, useState } from 'react';
import { Toast, Button, ToastContainer } from 'react-bootstrap';
import React, { useEffect, useState } from "react";
import { Toast, Button, ToastContainer } from "react-bootstrap";
type ConnectionToastProps = {
interface ConnectionToastProps {
connectionStatus: string;
};
}
/**
* ConnectionToast Component
@ -36,31 +36,31 @@ export const ConnectionToast = React.memo(
delay: number | undefined;
} => {
switch (connectionStatus) {
case 'connecting':
case "connecting":
return {
message: 'Connecting...',
bg: 'info',
delay: undefined
message: "Connecting...",
bg: "info",
delay: undefined,
};
case 'connected':
return { message: 'Connected', bg: 'success', delay: 1000 };
case 'disconnected':
case "connected":
return { message: "Connected", bg: "success", delay: 1000 };
case "disconnected":
return {
message: 'Disconnected',
bg: 'danger',
delay: undefined
message: "Disconnected",
bg: "danger",
delay: undefined,
};
case 'reconnecting':
case "reconnecting":
return {
message: 'Reconnecting...',
bg: 'info',
delay: undefined
message: "Reconnecting...",
bg: "info",
delay: undefined,
};
default:
return {
message: '',
bg: 'info',
delay: undefined
message: "",
bg: "info",
delay: undefined,
};
}
};
@ -82,5 +82,7 @@ export const ConnectionToast = React.memo(
</Toast>
</ToastContainer>
);
}
},
);
ConnectionToast.displayName = "ConnectionToast";

View File

@ -1,63 +1,66 @@
import { useState } from 'react';
import React from 'react';
import { Card, Collapse } from 'react-bootstrap';
import { ChevronDown, ChevronRight } from 'react-bootstrap-icons';
import { Attribute, GenericComponent } from './GenericComponent';
import { getIdFromFullAccessPath } from '../utils/stringUtils';
import { LevelName } from './NotificationsComponent';
import React from "react";
import { Card, Collapse } from "react-bootstrap";
import { ChevronDown, ChevronRight } from "react-bootstrap-icons";
import { GenericComponent } from "./GenericComponent";
import { LevelName } from "./NotificationsComponent";
import { SerializedObject } from "../types/SerializedObject";
import useLocalStorage from "../hooks/useLocalStorage";
import useSortedEntries from "../hooks/useSortedEntries";
type DataServiceProps = {
name: string;
interface DataServiceProps {
props: DataServiceJSON;
parentPath?: string;
isInstantUpdate: boolean;
addNotification: (message: string, levelname?: LevelName) => void;
};
displayName: string;
id: string;
}
export type DataServiceJSON = Record<string, Attribute>;
export type DataServiceJSON = Record<string, SerializedObject>;
export const DataServiceComponent = React.memo(
({
name,
props,
parentPath = 'DataService',
isInstantUpdate,
addNotification
}: DataServiceProps) => {
const [open, setOpen] = useState(true);
let fullAccessPath = parentPath;
if (name) {
fullAccessPath = parentPath.concat('.' + name);
}
const id = getIdFromFullAccessPath(fullAccessPath);
({ props, isInstantUpdate, addNotification, displayName, id }: DataServiceProps) => {
// Retrieve the initial state from localStorage, default to true if not found
const [open, setOpen] = useLocalStorage(`dataServiceComponent-${id}-open`, true);
return (
<div className="dataServiceComponent" id={id}>
<Card className="mb-3">
<Card.Header
onClick={() => setOpen(!open)}
style={{ cursor: 'pointer' }} // Change cursor style on hover
>
{fullAccessPath} {open ? <ChevronDown /> : <ChevronRight />}
</Card.Header>
<Collapse in={open}>
<Card.Body>
{Object.entries(props).map(([key, value]) => {
return (
const sortedEntries = useSortedEntries(props);
if (displayName !== "") {
return (
<div className="component dataServiceComponent" id={id}>
<Card>
<Card.Header onClick={() => setOpen(!open)} style={{ cursor: "pointer" }}>
{displayName} {open ? <ChevronDown /> : <ChevronRight />}
</Card.Header>
<Collapse in={open}>
<Card.Body>
{sortedEntries.map((value) => (
<GenericComponent
key={key}
key={value.full_access_path}
attribute={value}
name={key}
parentPath={fullAccessPath}
isInstantUpdate={isInstantUpdate}
addNotification={addNotification}
/>
);
})}
</Card.Body>
</Collapse>
</Card>
</div>
);
}
))}
</Card.Body>
</Collapse>
</Card>
</div>
);
} else {
return (
<div className="component dataServiceComponent" id={id}>
{sortedEntries.map((value) => (
<GenericComponent
key={value.full_access_path}
attribute={value}
isInstantUpdate={isInstantUpdate}
addNotification={addNotification}
/>
))}
</div>
);
}
},
);
DataServiceComponent.displayName = "DataServiceComponent";

View File

@ -0,0 +1,56 @@
import React from "react";
import { LevelName } from "./NotificationsComponent";
import { DataServiceComponent, DataServiceJSON } from "./DataServiceComponent";
import { MethodComponent } from "./MethodComponent";
interface DeviceConnectionProps {
fullAccessPath: string;
props: DataServiceJSON;
isInstantUpdate: boolean;
addNotification: (message: string, levelname?: LevelName) => void;
displayName: string;
id: string;
}
export const DeviceConnectionComponent = React.memo(
({
fullAccessPath,
props,
isInstantUpdate,
addNotification,
displayName,
id,
}: DeviceConnectionProps) => {
const { connected, connect, ...updatedProps } = props;
const connectedVal = connected.value;
return (
<div className="deviceConnectionComponent" id={id}>
{!connectedVal && (
<div className="overlayContent">
<div>
{displayName != "" ? displayName : "Device"} is currently not available!
</div>
<MethodComponent
fullAccessPath={`${fullAccessPath}.connect`}
docString={connect.doc}
addNotification={addNotification}
displayName={"reconnect"}
id={id + "-connect"}
render={true}
/>
</div>
)}
<DataServiceComponent
props={updatedProps}
isInstantUpdate={isInstantUpdate}
addNotification={addNotification}
displayName={displayName}
id={id}
/>
</div>
);
},
);
DeviceConnectionComponent.displayName = "DeviceConnectionComponent";

View File

@ -0,0 +1,41 @@
import React from "react";
import { DocStringComponent } from "./DocStringComponent";
import { GenericComponent } from "./GenericComponent";
import { LevelName } from "./NotificationsComponent";
import { SerializedObject } from "../types/SerializedObject";
import useRenderCount from "../hooks/useRenderCount";
import useSortedEntries from "../hooks/useSortedEntries";
interface DictComponentProps {
value: Record<string, SerializedObject>;
docString: string | null;
isInstantUpdate: boolean;
addNotification: (message: string, levelname?: LevelName) => void;
id: string;
}
export const DictComponent = React.memo((props: DictComponentProps) => {
const { docString, isInstantUpdate, addNotification, id } = props;
const sortedEntries = useSortedEntries(props.value);
const renderCount = useRenderCount();
return (
<div className={"listComponent"} id={id}>
{process.env.NODE_ENV === "development" && <div>Render count: {renderCount}</div>}
<DocStringComponent docString={docString} />
{sortedEntries.map((item) => {
return (
<GenericComponent
key={item.full_access_path}
attribute={item}
isInstantUpdate={isInstantUpdate}
addNotification={addNotification}
/>
);
})}
</div>
);
});
DictComponent.displayName = "DictComponent";

View File

@ -1,8 +1,8 @@
import { Badge, Tooltip, OverlayTrigger } from 'react-bootstrap';
import React from 'react';
import { Badge, Tooltip, OverlayTrigger } from "react-bootstrap";
import React from "react";
interface DocStringProps {
docString?: string;
docString?: string | null;
}
export const DocStringComponent = React.memo((props: DocStringProps) => {
@ -21,3 +21,5 @@ export const DocStringComponent = React.memo((props: DocStringProps) => {
</OverlayTrigger>
);
});
DocStringComponent.displayName = "DocStringComponent";

View File

@ -1,63 +1,88 @@
import React, { useEffect, useRef } from 'react';
import { InputGroup, Form, Row, Col } from 'react-bootstrap';
import { setAttribute } from '../socket';
import { DocStringComponent } from './DocStringComponent';
import { LevelName } from './NotificationsComponent';
import React, { useEffect } from "react";
import { InputGroup, Form, Row, Col } from "react-bootstrap";
import { DocStringComponent } from "./DocStringComponent";
import { LevelName } from "./NotificationsComponent";
import { SerializedObject, SerializedEnum } from "../types/SerializedObject";
import { propsAreEqual } from "../utils/propsAreEqual";
import useRenderCount from "../hooks/useRenderCount";
interface EnumComponentProps {
name: string;
parentPath: string;
value: string;
docString?: string;
enumDict: Record<string, string>;
interface EnumComponentProps extends SerializedEnum {
addNotification: (message: string, levelname?: LevelName) => void;
displayName: string;
id: string;
changeCallback: (value: SerializedObject, callback?: (ack: unknown) => void) => void;
}
export const EnumComponent = React.memo((props: EnumComponentProps) => {
const {
name,
parentPath: parentPath,
addNotification,
displayName,
id,
value,
docString,
enumDict,
addNotification
full_access_path: fullAccessPath,
enum: enumDict,
doc: docString,
readonly: readOnly,
changeCallback,
} = props;
const renderCount = useRef(0);
const renderCount = useRenderCount();
useEffect(() => {
renderCount.current++;
});
useEffect(() => {
addNotification(`${parentPath}.${name} changed to ${value}.`);
}, [props.value]);
const handleValueChange = (newValue: string) => {
setAttribute(name, parentPath, newValue);
};
addNotification(`${fullAccessPath} changed to ${value}.`);
}, [value]);
return (
<div className={'enumComponent'} id={parentPath.concat('.' + name)}>
{process.env.NODE_ENV === 'development' && (
<div>Render count: {renderCount.current}</div>
)}
<DocStringComponent docString={docString} />
<div className={"component enumComponent"} id={id}>
{process.env.NODE_ENV === "development" && <div>Render count: {renderCount}</div>}
<Row>
<Col className="d-flex align-items-center">
<InputGroup.Text>{name}</InputGroup.Text>
<Form.Select
aria-label="Default select example"
value={value}
onChange={(event) => handleValueChange(event.target.value)}>
{Object.entries(enumDict).map(([key, val]) => (
<option key={key} value={key}>
{key} - {val}
</option>
))}
</Form.Select>
<InputGroup.Text>
{displayName}
<DocStringComponent docString={docString} />
</InputGroup.Text>
{readOnly ? (
// Display the Form.Control when readOnly is true
<Form.Control
style={
props.type == "ColouredEnum" ? { backgroundColor: enumDict[value] } : {}
}
value={props.type == "ColouredEnum" ? value : enumDict[value]}
name={fullAccessPath}
disabled={true}
/>
) : (
// Display the Form.Select when readOnly is false
<Form.Select
aria-label="example-select"
value={value}
name={fullAccessPath}
style={
props.type == "ColouredEnum" ? { backgroundColor: enumDict[value] } : {}
}
onChange={(event) =>
changeCallback({
type: props.type,
name: props.name,
enum: enumDict,
value: event.target.value,
full_access_path: fullAccessPath,
readonly: props.readonly,
doc: props.doc,
})
}>
{Object.entries(enumDict).map(([key, val]) => (
<option key={key} value={key}>
{props.type == "ColouredEnum" ? key : val}
</option>
))}
</Form.Select>
)}
</Col>
</Row>
</div>
);
});
}, propsAreEqual);
EnumComponent.displayName = "EnumComponent";

View File

@ -1,205 +1,241 @@
import React from 'react';
import { ButtonComponent } from './ButtonComponent';
import { NumberComponent } from './NumberComponent';
import { SliderComponent } from './SliderComponent';
import { EnumComponent } from './EnumComponent';
import { MethodComponent } from './MethodComponent';
import { AsyncMethodComponent } from './AsyncMethodComponent';
import { StringComponent } from './StringComponent';
import { ListComponent } from './ListComponent';
import { DataServiceComponent, DataServiceJSON } from './DataServiceComponent';
import { ImageComponent } from './ImageComponent';
import { ColouredEnumComponent } from './ColouredEnumComponent';
import { LevelName } from './NotificationsComponent';
import React, { useContext } from "react";
import { ButtonComponent } from "./ButtonComponent";
import { NumberComponent, NumberObject } from "./NumberComponent";
import { SliderComponent } from "./SliderComponent";
import { EnumComponent } from "./EnumComponent";
import { MethodComponent } from "./MethodComponent";
import { StringComponent } from "./StringComponent";
import { ListComponent } from "./ListComponent";
import { DataServiceComponent, DataServiceJSON } from "./DataServiceComponent";
import { DeviceConnectionComponent } from "./DeviceConnection";
import { ImageComponent } from "./ImageComponent";
import { LevelName } from "./NotificationsComponent";
import { getIdFromFullAccessPath } from "../utils/stringUtils";
import { WebSettingsContext } from "../WebSettings";
import { updateValue } from "../socket";
import { DictComponent } from "./DictComponent";
import { parseFullAccessPath } from "../utils/stateUtils";
import { SerializedEnum, SerializedObject } from "../types/SerializedObject";
import { TaskComponent, TaskStatus } from "./TaskComponent";
type AttributeType =
| 'str'
| 'bool'
| 'float'
| 'int'
| 'Quantity'
| 'list'
| 'method'
| 'DataService'
| 'Enum'
| 'NumberSlider'
| 'Image'
| 'ColouredEnum';
type ValueType = boolean | string | number | object;
export interface Attribute {
type: AttributeType;
value?: ValueType | ValueType[];
readonly: boolean;
doc?: string | null;
parameters?: Record<string, string>;
async?: boolean;
enum?: Record<string, string>;
}
type GenericComponentProps = {
attribute: Attribute;
name: string;
parentPath: string;
interface GenericComponentProps {
attribute: SerializedObject;
isInstantUpdate: boolean;
addNotification: (message: string, levelname?: LevelName) => void;
}
const getPathFromPathParts = (pathParts: string[]): string => {
let path = "";
for (const pathPart of pathParts) {
if (!pathPart.startsWith("[") && path !== "") {
path += ".";
}
path += pathPart;
}
return path;
};
const createDisplayNameFromAccessPath = (fullAccessPath: string): string => {
const displayNameParts = [];
const parsedFullAccessPath = parseFullAccessPath(fullAccessPath);
for (let i = parsedFullAccessPath.length - 1; i >= 0; i--) {
const item = parsedFullAccessPath[i];
displayNameParts.unshift(item);
if (!item.startsWith("[")) {
break;
}
}
return getPathFromPathParts(displayNameParts);
};
function changeCallback(
value: SerializedObject,
callback: (ack: unknown) => void = () => {},
) {
updateValue(value, callback);
}
export const GenericComponent = React.memo(
({
attribute,
name,
parentPath,
isInstantUpdate,
addNotification
}: GenericComponentProps) => {
if (attribute.type === 'bool') {
({ attribute, isInstantUpdate, addNotification }: GenericComponentProps) => {
const { full_access_path: fullAccessPath } = attribute;
const id = getIdFromFullAccessPath(fullAccessPath);
const webSettings = useContext(WebSettingsContext);
let displayName = createDisplayNameFromAccessPath(fullAccessPath);
if (webSettings[fullAccessPath]) {
if (webSettings[fullAccessPath].display === false) {
return null;
}
if (webSettings[fullAccessPath].displayName) {
displayName = webSettings[fullAccessPath].displayName;
}
}
if (attribute.type === "bool") {
return (
<ButtonComponent
name={name}
parentPath={parentPath}
fullAccessPath={fullAccessPath}
docString={attribute.doc}
readOnly={attribute.readonly}
value={Boolean(attribute.value)}
addNotification={addNotification}
changeCallback={changeCallback}
displayName={displayName}
id={id}
/>
);
} else if (attribute.type === 'float' || attribute.type === 'int') {
} else if (attribute.type === "float" || attribute.type === "int") {
return (
<NumberComponent
name={name}
type={attribute.type}
parentPath={parentPath}
fullAccessPath={fullAccessPath}
docString={attribute.doc}
readOnly={attribute.readonly}
value={Number(attribute.value)}
isInstantUpdate={isInstantUpdate}
addNotification={addNotification}
changeCallback={changeCallback}
displayName={displayName}
id={id}
/>
);
} else if (attribute.type === 'Quantity') {
} else if (attribute.type === "Quantity") {
return (
<NumberComponent
name={name}
type="float"
parentPath={parentPath}
type="Quantity"
fullAccessPath={fullAccessPath}
docString={attribute.doc}
readOnly={attribute.readonly}
value={Number(attribute.value['magnitude'])}
unit={attribute.value['unit']}
value={Number(attribute.value["magnitude"])}
unit={attribute.value["unit"]}
isInstantUpdate={isInstantUpdate}
addNotification={addNotification}
changeCallback={changeCallback}
displayName={displayName}
id={id}
/>
);
} else if (attribute.type === 'NumberSlider') {
} else if (attribute.type === "NumberSlider") {
return (
<SliderComponent
name={name}
parentPath={parentPath}
docString={attribute.doc}
fullAccessPath={fullAccessPath}
docString={attribute.value["value"].doc}
readOnly={attribute.readonly}
value={attribute.value['value']['value']}
min={attribute.value['min']['value']}
max={attribute.value['max']['value']}
stepSize={attribute.value['step_size']['value']}
value={attribute.value["value"] as NumberObject}
min={attribute.value["min"] as NumberObject}
max={attribute.value["max"] as NumberObject}
stepSize={attribute.value["step_size"] as NumberObject}
isInstantUpdate={isInstantUpdate}
addNotification={addNotification}
changeCallback={changeCallback}
displayName={displayName}
id={id}
/>
);
} else if (attribute.type === 'Enum') {
} else if (attribute.type === "Enum" || attribute.type === "ColouredEnum") {
return (
<EnumComponent
name={name}
parentPath={parentPath}
docString={attribute.doc}
value={String(attribute.value)}
enumDict={attribute.enum}
{...(attribute as SerializedEnum)}
addNotification={addNotification}
changeCallback={changeCallback}
displayName={displayName}
id={id}
/>
);
} else if (attribute.type === 'method') {
if (!attribute.async) {
return (
<MethodComponent
name={name}
parentPath={parentPath}
docString={attribute.doc}
parameters={attribute.parameters}
addNotification={addNotification}
/>
);
} else {
return (
<AsyncMethodComponent
name={name}
parentPath={parentPath}
docString={attribute.doc}
parameters={attribute.parameters}
value={attribute.value as Record<string, string>}
addNotification={addNotification}
/>
);
}
} else if (attribute.type === 'str') {
} else if (attribute.type === "method") {
return (
<MethodComponent
fullAccessPath={fullAccessPath}
docString={attribute.doc}
addNotification={addNotification}
displayName={displayName}
id={id}
render={attribute.frontend_render}
/>
);
} else if (attribute.type === "str") {
return (
<StringComponent
name={name}
fullAccessPath={fullAccessPath}
value={attribute.value as string}
readOnly={attribute.readonly}
docString={attribute.doc}
parentPath={parentPath}
isInstantUpdate={isInstantUpdate}
addNotification={addNotification}
changeCallback={changeCallback}
displayName={displayName}
id={id}
/>
);
} else if (attribute.type === 'DataService') {
} else if (attribute.type == "Task") {
return (
<TaskComponent
fullAccessPath={fullAccessPath}
docString={attribute.doc}
status={attribute.value["status"].value as TaskStatus}
addNotification={addNotification}
displayName={displayName}
id={id}
/>
);
} else if (attribute.type === "DataService") {
return (
<DataServiceComponent
name={name}
props={attribute.value as DataServiceJSON}
parentPath={parentPath}
isInstantUpdate={isInstantUpdate}
addNotification={addNotification}
displayName={displayName}
id={id}
/>
);
} else if (attribute.type === 'list') {
} else if (attribute.type === "DeviceConnection") {
return (
<DeviceConnectionComponent
fullAccessPath={fullAccessPath}
props={attribute.value as DataServiceJSON}
isInstantUpdate={isInstantUpdate}
addNotification={addNotification}
displayName={displayName}
id={id}
/>
);
} else if (attribute.type === "list") {
return (
<ListComponent
name={name}
value={attribute.value as Attribute[]}
value={attribute.value}
docString={attribute.doc}
parentPath={parentPath}
isInstantUpdate={isInstantUpdate}
addNotification={addNotification}
id={id}
/>
);
} else if (attribute.type === 'Image') {
} else if (attribute.type === "dict") {
return (
<DictComponent
value={attribute.value}
docString={attribute.doc}
isInstantUpdate={isInstantUpdate}
addNotification={addNotification}
id={id}
/>
);
} else if (attribute.type === "Image") {
return (
<ImageComponent
name={name}
parentPath={parentPath}
value={attribute.value['value']['value'] as string}
readOnly={attribute.readonly}
docString={attribute.doc}
// Add any other specific props for the ImageComponent here
format={attribute.value['format']['value'] as string}
addNotification={addNotification}
/>
);
} else if (attribute.type === 'ColouredEnum') {
console.log(attribute);
return (
<ColouredEnumComponent
name={name}
parentPath={parentPath}
docString={attribute.doc}
value={String(attribute.value)}
readOnly={attribute.readonly}
enumDict={attribute.enum}
fullAccessPath={fullAccessPath}
docString={attribute.value["value"].doc}
displayName={displayName}
id={id}
addNotification={addNotification}
value={attribute.value["value"]["value"] as string}
format={attribute.value["format"]["value"] as string}
/>
);
} else {
return <div key={name}>{name}</div>;
return <div key={fullAccessPath}>{fullAccessPath}</div>;
}
}
},
);
GenericComponent.displayName = "GenericComponent";

View File

@ -1,55 +1,48 @@
import React, { useEffect, useRef, useState } from 'react';
import { Card, Collapse, Image } from 'react-bootstrap';
import { DocStringComponent } from './DocStringComponent';
import { ChevronDown, ChevronRight } from 'react-bootstrap-icons';
import { getIdFromFullAccessPath } from '../utils/stringUtils';
import { LevelName } from './NotificationsComponent';
import React, { useEffect, useState } from "react";
import { Card, Collapse, Image } from "react-bootstrap";
import { DocStringComponent } from "./DocStringComponent";
import { ChevronDown, ChevronRight } from "react-bootstrap-icons";
import { LevelName } from "./NotificationsComponent";
import useRenderCount from "../hooks/useRenderCount";
interface ImageComponentProps {
name: string;
parentPath: string;
fullAccessPath: string;
value: string;
readOnly: boolean;
docString: string;
docString: string | null;
format: string;
addNotification: (message: string, levelname?: LevelName) => void;
displayName: string;
id: string;
}
export const ImageComponent = React.memo((props: ImageComponentProps) => {
const { name, parentPath, value, docString, format, addNotification } = props;
const { fullAccessPath, value, docString, format, addNotification, displayName, id } =
props;
const renderCount = useRef(0);
const renderCount = useRenderCount();
const [open, setOpen] = useState(true);
const id = getIdFromFullAccessPath(parentPath.concat('.' + name));
useEffect(() => {
renderCount.current++;
});
useEffect(() => {
addNotification(`${parentPath}.${name} changed.`);
addNotification(`${fullAccessPath} changed.`);
}, [props.value]);
return (
<div className={'imageComponent'} id={id}>
{process.env.NODE_ENV === 'development' && (
<div>Render count: {renderCount.current}</div>
)}
<div className="component imageComponent" id={id}>
<Card>
<Card.Header
onClick={() => setOpen(!open)}
style={{ cursor: 'pointer' }} // Change cursor style on hover
style={{ cursor: "pointer" }} // Change cursor style on hover
>
{name} {open ? <ChevronDown /> : <ChevronRight />}
{displayName}
<DocStringComponent docString={docString} />
{open ? <ChevronDown /> : <ChevronRight />}
</Card.Header>
<Collapse in={open}>
<Card.Body>
{process.env.NODE_ENV === 'development' && (
<p>Render count: {renderCount.current}</p>
{process.env.NODE_ENV === "development" && (
<p>Render count: {renderCount}</p>
)}
<DocStringComponent docString={docString} />
{/* Your component JSX here */}
{format === '' && value === '' ? (
{format === "" && value === "" ? (
<p>No image set in the backend.</p>
) : (
<Image src={`data:image/${format.toLowerCase()};base64,${value}`}></Image>
@ -60,3 +53,5 @@ export const ImageComponent = React.memo((props: ImageComponentProps) => {
</div>
);
});
ImageComponent.displayName = "ImageComponent";

View File

@ -1,42 +1,35 @@
import React, { useEffect, useRef } from 'react';
import { DocStringComponent } from './DocStringComponent';
import { Attribute, GenericComponent } from './GenericComponent';
import { getIdFromFullAccessPath } from '../utils/stringUtils';
import { LevelName } from './NotificationsComponent';
import React from "react";
import { DocStringComponent } from "./DocStringComponent";
import { GenericComponent } from "./GenericComponent";
import { LevelName } from "./NotificationsComponent";
import { SerializedObject } from "../types/SerializedObject";
import useRenderCount from "../hooks/useRenderCount";
import useSortedEntries from "../hooks/useSortedEntries";
interface ListComponentProps {
name: string;
parentPath?: string;
value: Attribute[];
docString: string;
value: SerializedObject[];
docString: string | null;
isInstantUpdate: boolean;
addNotification: (message: string, levelname?: LevelName) => void;
id: string;
}
export const ListComponent = React.memo((props: ListComponentProps) => {
const { name, parentPath, value, docString, isInstantUpdate, addNotification } =
props;
const { docString, isInstantUpdate, addNotification, id } = props;
const renderCount = useRef(0);
const id = getIdFromFullAccessPath(parentPath.concat('.' + name));
const sortedEntries = useSortedEntries(props.value);
useEffect(() => {
renderCount.current++;
}, [props]);
const renderCount = useRenderCount();
return (
<div className={'listComponent'} id={id}>
{process.env.NODE_ENV === 'development' && (
<div>Render count: {renderCount.current}</div>
)}
<div className={"listComponent"} id={id}>
{process.env.NODE_ENV === "development" && <div>Render count: {renderCount}</div>}
<DocStringComponent docString={docString} />
{value.map((item, index) => {
{sortedEntries.map((item) => {
return (
<GenericComponent
key={`${name}[${index}]`}
key={item.full_access_path}
attribute={item}
name={`${name}[${index}]`}
parentPath={parentPath}
isInstantUpdate={isInstantUpdate}
addNotification={addNotification}
/>
@ -45,3 +38,5 @@ export const ListComponent = React.memo((props: ListComponentProps) => {
</div>
);
});
ListComponent.displayName = "ListComponent";

View File

@ -1,110 +1,55 @@
import React, { useState, useEffect, useRef } from 'react';
import { runMethod } from '../socket';
import { Button, InputGroup, Form, Collapse } from 'react-bootstrap';
import { DocStringComponent } from './DocStringComponent';
import { getIdFromFullAccessPath } from '../utils/stringUtils';
import { LevelName } from './NotificationsComponent';
import React, { useRef } from "react";
import { runMethod } from "../socket";
import { Button, Form } from "react-bootstrap";
import { DocStringComponent } from "./DocStringComponent";
import { LevelName } from "./NotificationsComponent";
import useRenderCount from "../hooks/useRenderCount";
import { propsAreEqual } from "../utils/propsAreEqual";
interface MethodProps {
name: string;
parentPath: string;
parameters: Record<string, string>;
docString?: string;
hideOutput?: boolean;
fullAccessPath: string;
docString: string | null;
addNotification: (message: string, levelname?: LevelName) => void;
displayName: string;
id: string;
render: boolean;
}
export const MethodComponent = React.memo((props: MethodProps) => {
const { name, parentPath, docString, addNotification } = props;
const { fullAccessPath, docString, addNotification, displayName, id } = props;
const renderCount = useRef(0);
const [hideOutput, setHideOutput] = useState(false);
// Add a new state variable to hold the list of function calls
const [functionCalls, setFunctionCalls] = useState([]);
const id = getIdFromFullAccessPath(parentPath.concat('.' + name));
// Conditional rendering based on the 'render' prop.
if (!props.render) {
return null;
}
useEffect(() => {
renderCount.current++;
if (props.hideOutput !== undefined) {
setHideOutput(props.hideOutput);
}
});
const renderCount = useRenderCount();
const formRef = useRef(null);
const triggerNotification = (args: Record<string, string>) => {
const argsString = Object.entries(args)
.map(([key, value]) => `${key}: "${value}"`)
.join(', ');
let message = `Method ${parentPath}.${name} was triggered`;
const triggerNotification = () => {
const message = `Method ${fullAccessPath} was triggered.`;
if (argsString === '') {
message += '.';
} else {
message += ` with arguments {${argsString}}.`;
}
addNotification(message);
};
const execute = async (event: React.FormEvent) => {
event.preventDefault();
runMethod(fullAccessPath);
const kwargs = {};
Object.keys(props.parameters).forEach(
(name) => (kwargs[name] = event.target[name].value)
);
runMethod(name, parentPath, kwargs, (ack) => {
// Update the functionCalls state with the new call if we get an acknowledge msg
if (ack !== undefined) {
setFunctionCalls((prevCalls) => [
...prevCalls,
{ name, args: kwargs, result: ack }
]);
}
});
triggerNotification(kwargs);
triggerNotification();
};
const args = Object.entries(props.parameters).map(([name, type], index) => {
const form_name = `${name} (${type})`;
return (
<InputGroup key={index}>
<InputGroup.Text className="component-label">{form_name}</InputGroup.Text>
<Form.Control type="text" name={name} />
</InputGroup>
);
});
return (
<div className="align-items-center methodComponent" id={id}>
{process.env.NODE_ENV === 'development' && (
<div>Render count: {renderCount.current}</div>
)}
<h5 onClick={() => setHideOutput(!hideOutput)} style={{ cursor: 'pointer' }}>
Function: {name}
<DocStringComponent docString={docString} />
</h5>
<Form onSubmit={execute}>
{args}
<Button variant="primary" type="submit">
Execute
<div className="component methodComponent" id={id}>
{process.env.NODE_ENV === "development" && <div>Render count: {renderCount}</div>}
<Form onSubmit={execute} ref={formRef}>
<Button className="component" variant="primary" type="submit">
{`${displayName} `}
<DocStringComponent docString={docString} />
</Button>
</Form>
<Collapse in={!hideOutput}>
<div id="function-output">
{functionCalls.map((call, index) => (
<div key={index}>
<div style={{ color: 'grey', fontSize: 'small' }}>
{Object.entries(call.args)
.map(([key, val]) => `${key}=${JSON.stringify(val)}`)
.join(', ') +
' => ' +
JSON.stringify(call.result)}
</div>
</div>
))}
</div>
</Collapse>
</div>
);
});
}, propsAreEqual);
MethodComponent.displayName = "MethodComponent";

View File

@ -1,19 +1,19 @@
import React from 'react';
import { ToastContainer, Toast } from 'react-bootstrap';
import React from "react";
import { ToastContainer, Toast } from "react-bootstrap";
export type LevelName = 'CRITICAL' | 'ERROR' | 'WARNING' | 'INFO' | 'DEBUG';
export type Notification = {
export type LevelName = "CRITICAL" | "ERROR" | "WARNING" | "INFO" | "DEBUG";
export interface Notification {
id: number;
timeStamp: string;
message: string;
levelname: LevelName;
};
}
type NotificationProps = {
interface NotificationProps {
showNotification: boolean;
notifications: Notification[];
removeNotificationById: (id: number) => void;
};
}
export const Notifications = React.memo((props: NotificationProps) => {
const { showNotification, notifications, removeNotificationById } = props;
@ -23,10 +23,10 @@ export const Notifications = React.memo((props: NotificationProps) => {
{notifications.map((notification) => {
// Determine if the toast should be shown
const shouldShow =
notification.levelname === 'ERROR' ||
notification.levelname === 'CRITICAL' ||
notification.levelname === "ERROR" ||
notification.levelname === "CRITICAL" ||
(showNotification &&
['WARNING', 'INFO', 'DEBUG'].includes(notification.levelname));
["WARNING", "INFO", "DEBUG"].includes(notification.levelname));
if (!shouldShow) {
return null;
@ -34,31 +34,31 @@ export const Notifications = React.memo((props: NotificationProps) => {
return (
<Toast
className={notification.levelname.toLowerCase() + 'Toast'}
className={notification.levelname.toLowerCase() + "Toast"}
key={notification.id}
onClose={() => removeNotificationById(notification.id)}
onClick={() => removeNotificationById(notification.id)}
onMouseLeave={() => {
if (notification.levelname !== 'ERROR') {
if (notification.levelname !== "ERROR") {
removeNotificationById(notification.id);
}
}}
show={true}
autohide={
notification.levelname === 'WARNING' ||
notification.levelname === 'INFO' ||
notification.levelname === 'DEBUG'
notification.levelname === "WARNING" ||
notification.levelname === "INFO" ||
notification.levelname === "DEBUG"
}
delay={
notification.levelname === 'WARNING' ||
notification.levelname === 'INFO' ||
notification.levelname === 'DEBUG'
notification.levelname === "WARNING" ||
notification.levelname === "INFO" ||
notification.levelname === "DEBUG"
? 2000
: undefined
}>
<Toast.Header
closeButton={false}
className={notification.levelname.toLowerCase() + 'Toast text-right'}>
className={notification.levelname.toLowerCase() + "Toast text-right"}>
<strong className="me-auto">{notification.levelname}</strong>
<small>{notification.timeStamp}</small>
</Toast.Header>
@ -69,3 +69,5 @@ export const Notifications = React.memo((props: NotificationProps) => {
</ToastContainer>
);
});
Notifications.displayName = "Notifications";

View File

@ -1,30 +1,46 @@
import React, { useEffect, useRef, useState } from 'react';
import { Form, InputGroup } from 'react-bootstrap';
import { setAttribute } from '../socket';
import { DocStringComponent } from './DocStringComponent';
import '../App.css';
import { getIdFromFullAccessPath } from '../utils/stringUtils';
import { LevelName } from './NotificationsComponent';
import React, { useEffect, useRef, useState } from "react";
import { Form, InputGroup } from "react-bootstrap";
import { DocStringComponent } from "./DocStringComponent";
import "../App.css";
import { LevelName } from "./NotificationsComponent";
import { SerializedObject } from "../types/SerializedObject";
import { QuantityMap } from "../types/QuantityMap";
import useRenderCount from "../hooks/useRenderCount";
// TODO: add button functionality
export interface QuantityObject {
type: "Quantity";
readonly: boolean;
value: QuantityMap;
doc: string | null;
}
export interface IntObject {
type: "int";
readonly: boolean;
value: number;
doc: string | null;
}
export interface FloatObject {
type: "float";
readonly: boolean;
value: number;
doc: string | null;
}
export type NumberObject = IntObject | FloatObject | QuantityObject;
interface NumberComponentProps {
name: string;
type: 'float' | 'int';
parentPath?: string;
type: "float" | "int" | "Quantity";
fullAccessPath: string;
value: number;
readOnly: boolean;
docString: string;
docString: string | null;
isInstantUpdate: boolean;
unit?: string;
showName?: boolean;
customEmitUpdate?: (
name: string,
parent_path: string,
value: number,
callback?: (ack: unknown) => void
) => void;
addNotification: (message: string, levelname?: LevelName) => void;
changeCallback?: (value: SerializedObject, callback?: (ack: unknown) => void) => void;
displayName?: string;
id: string;
}
// TODO: highlight the digit that is being changed by setting both selectionStart and
@ -32,11 +48,11 @@ interface NumberComponentProps {
const handleArrowKey = (
key: string,
value: string,
selectionStart: number
selectionStart: number,
// selectionEnd: number
) => {
// Split the input value into the integer part and decimal part
const parts = value.split('.');
const parts = value.split(".");
const beforeDecimalCount = parts[0].length; // Count digits before the decimal
const afterDecimalCount = parts[1] ? parts[1].length : 0; // Count digits after the decimal
@ -52,14 +68,14 @@ const handleArrowKey = (
// Convert the input value to a number, increment or decrement it based on the
// arrow key
const numValue = parseFloat(value) + (key === 'ArrowUp' ? increment : -increment);
const numValue = parseFloat(value) + (key === "ArrowUp" ? increment : -increment);
// Convert the resulting number to a string, maintaining the same number of digits
// after the decimal
const newValue = numValue.toFixed(afterDecimalCount);
// Check if the length of the integer part of the number string has in-/decreased
const newBeforeDecimalCount = newValue.split('.')[0].length;
const newBeforeDecimalCount = newValue.split(".")[0].length;
if (newBeforeDecimalCount > beforeDecimalCount) {
// Move the cursor one position to the right
selectionStart += 1;
@ -73,18 +89,18 @@ const handleArrowKey = (
const handleBackspaceKey = (
value: string,
selectionStart: number,
selectionEnd: number
selectionEnd: number,
) => {
if (selectionEnd > selectionStart) {
// If there is a selection, delete all characters in the selection
return {
value: value.slice(0, selectionStart) + value.slice(selectionEnd),
selectionStart
selectionStart,
};
} else if (selectionStart > 0) {
return {
value: value.slice(0, selectionStart - 1) + value.slice(selectionStart),
selectionStart: selectionStart - 1
selectionStart: selectionStart - 1,
};
}
return { value, selectionStart };
@ -93,177 +109,217 @@ const handleBackspaceKey = (
const handleDeleteKey = (
value: string,
selectionStart: number,
selectionEnd: number
selectionEnd: number,
) => {
if (selectionEnd > selectionStart) {
// If there is a selection, delete all characters in the selection
return {
value: value.slice(0, selectionStart) + value.slice(selectionEnd),
selectionStart
selectionStart,
};
} else if (selectionStart < value.length) {
return {
value: value.slice(0, selectionStart) + value.slice(selectionStart + 1),
selectionStart
selectionStart,
};
}
return { value, selectionStart };
};
const handleNumericKey = (
key: string,
value: string,
selectionStart: number,
selectionEnd: number,
) => {
let newValue = value;
// Check if a number key or a decimal point key is pressed
if (key === "." && value.includes(".")) {
// Check if value already contains a decimal. If so, ignore input.
console.warn("Invalid input! Ignoring...");
return { value, selectionStart };
}
// Handle minus sign input
if (key === "-") {
if (selectionStart === 0 && selectionEnd > selectionStart) {
// Replace selection with minus if selection starts at 0
newValue = "-" + value.slice(selectionEnd);
selectionStart = 1;
} else if (selectionStart === 0 && !value.startsWith("-")) {
// Add minus at the beginning if it doesn't exist
newValue = "-" + value;
selectionStart = 1;
} else if (
(selectionStart === 0 || selectionStart === 1) &&
value.startsWith("-")
) {
// Remove minus if it exists
newValue = value.slice(1);
selectionStart = 0;
}
return { value: newValue, selectionStart };
}
// Add the new key at the cursor's position
if (selectionEnd > selectionStart) {
// If there is a selection, replace it with the key
newValue = value.slice(0, selectionStart) + key + value.slice(selectionEnd);
} else {
// Otherwise, insert the key at the cursor position
newValue = value.slice(0, selectionStart) + key + value.slice(selectionStart);
}
return { value: newValue, selectionStart: selectionStart + 1 };
};
/**
* Calculates the new cursor position after moving left by a specified step size.
*
* @param cursorPosition - The current position of the cursor.
* @param step - The number of positions to move left.
* @returns The new cursor position, clamped to a minimum of 0.
*/
const getCursorLeftPosition = (cursorPosition: number, step: number): number => {
return Math.max(0, cursorPosition - step);
};
/**
* Calculates the new cursor position after moving right by a specified step size.
*
* @param cursorPosition - The current position of the cursor.
* @param step - The number of positions to move right.
* @param maxPosition - The maximum allowed cursor position (e.g., value.length).
* @returns The new cursor position, clamped to a maximum of maxPosition.
*/
const getCursorRightPosition = (
cursorPosition: number,
step: number,
maxPosition: number,
): number => {
return Math.min(maxPosition, cursorPosition + step);
};
export const NumberComponent = React.memo((props: NumberComponentProps) => {
const {
name,
parentPath,
fullAccessPath,
value,
readOnly,
type,
docString,
isInstantUpdate,
unit,
addNotification
addNotification,
changeCallback = () => {},
displayName,
id,
} = props;
// Whether to show the name infront of the component (false if used with a slider)
const showName = props.showName !== undefined ? props.showName : true;
// If emitUpdate is passed, use this instead of the setAttribute from the socket
// Also used when used with a slider
const emitUpdate =
props.customEmitUpdate !== undefined ? props.customEmitUpdate : setAttribute;
const renderCount = useRef(0);
// Create a state for the cursor position
const [cursorPosition, setCursorPosition] = useState(null);
const cursorPositionRef = useRef<number | null>(null);
// Create a state for the input string
const [inputString, setInputString] = useState(props.value.toString());
const fullAccessPath = parentPath.concat('.' + name);
const id = getIdFromFullAccessPath(fullAccessPath);
const [inputString, setInputString] = useState(value.toString());
const renderCount = useRenderCount();
useEffect(() => {
renderCount.current++;
// Set the cursor position after the component re-renders
const inputElement = document.getElementsByName(
fullAccessPath
)[0] as HTMLInputElement;
if (inputElement && cursorPosition !== null) {
inputElement.setSelectionRange(cursorPosition, cursorPosition);
}
});
useEffect(() => {
// Parse the input string to a number for comparison
const numericInputString =
props.type === 'int' ? parseInt(inputString) : parseFloat(inputString);
// Only update the inputString if it's different from the prop value
if (props.value !== numericInputString) {
setInputString(props.value.toString());
}
// emitting notification
let notificationMsg = `${parentPath}.${name} changed to ${props.value}`;
if (unit === undefined) {
notificationMsg += '.';
} else {
notificationMsg += ` ${unit}.`;
}
addNotification(notificationMsg);
}, [props.value]);
const handleNumericKey = (
key: string,
value: string,
selectionStart: number,
selectionEnd: number
) => {
// Check if a number key or a decimal point key is pressed
if (key === '.' && (value.includes('.') || props.type === 'int')) {
// Check if value already contains a decimal. If so, ignore input.
// eslint-disable-next-line no-console
console.warn('Invalid input! Ignoring...');
return { value, selectionStart };
}
let newValue = value;
// Add the new key at the cursor's position
if (selectionEnd > selectionStart) {
// If there is a selection, replace it with the key
newValue = value.slice(0, selectionStart) + key + value.slice(selectionEnd);
} else {
// otherwise, append the key after the selection start
newValue = value.slice(0, selectionStart) + key + value.slice(selectionStart);
}
return { value: newValue, selectionStart: selectionStart + 1 };
};
const handleKeyDown = (event) => {
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
const { key, target } = event;
if (
key === 'F1' ||
key === 'F5' ||
key === 'F12' ||
key === 'Tab' ||
key === 'ArrowRight' ||
key === 'ArrowLeft'
) {
const inputTarget = target as HTMLInputElement;
// Get the current input value and cursor position
const { value } = inputTarget;
const valueLength = value.length;
const selectionEnd = inputTarget.selectionEnd ?? 0;
let selectionStart = inputTarget.selectionStart ?? 0;
if (key === "F1" || key === "F5" || key === "F12" || key === "Tab") {
return;
} else if (key === "ArrowLeft" || key === "ArrowRight") {
const hasSelection = selectionEnd > selectionStart;
if (hasSelection && !event.shiftKey) {
// Collapse selection: ArrowLeft -> start, ArrowRight -> end
const collapseTo = key === "ArrowLeft" ? selectionStart : selectionEnd;
cursorPositionRef.current = collapseTo;
} else {
// No selection or shift key is pressed, just move cursor by one
const newSelectionStart =
key === "ArrowLeft"
? getCursorLeftPosition(selectionStart, 1)
: getCursorRightPosition(selectionEnd, 1, valueLength);
cursorPositionRef.current = newSelectionStart;
}
return;
}
event.preventDefault();
// Get the current input value and cursor position
const { value } = target;
let { selectionStart } = target;
const { selectionEnd } = target;
let newValue: string = value;
if (event.ctrlKey && key === 'a') {
if (event.ctrlKey && key === "a") {
// Select everything when pressing Ctrl + a
target.setSelectionRange(0, target.value.length);
inputTarget.setSelectionRange(0, value.length);
return;
} else if (key === '-') {
if (selectionStart === 0 && !value.startsWith('-')) {
newValue = '-' + value;
selectionStart++;
} else if (value.startsWith('-') && selectionStart === 1) {
newValue = value.substring(1); // remove minus sign
selectionStart--;
} else {
return; // Ignore "-" pressed in other positions
}
} else if (!isNaN(key) && key !== ' ') {
} else if ((key >= "0" && key <= "9") || key === "-") {
// Check if a number key or a decimal point key is pressed
({ value: newValue, selectionStart } = handleNumericKey(
key,
value,
selectionStart,
selectionEnd
selectionEnd,
));
} else if (key === '.') {
} else if (key === "." && (type === "float" || type === "Quantity")) {
({ value: newValue, selectionStart } = handleNumericKey(
key,
value,
selectionStart,
selectionEnd
selectionEnd,
));
} else if (key === 'ArrowUp' || key === 'ArrowDown') {
} else if (key === "ArrowUp" || key === "ArrowDown") {
({ value: newValue, selectionStart } = handleArrowKey(
key,
value,
selectionStart
selectionStart,
// selectionEnd
));
} else if (key === 'Backspace') {
} else if (key === "Backspace") {
({ value: newValue, selectionStart } = handleBackspaceKey(
value,
selectionStart,
selectionEnd
selectionEnd,
));
} else if (key === 'Delete') {
} else if (key === "Delete") {
({ value: newValue, selectionStart } = handleDeleteKey(
value,
selectionStart,
selectionEnd
selectionEnd,
));
} else if (key === 'Enter' && !isInstantUpdate) {
emitUpdate(name, parentPath, Number(newValue));
} else if (key === "Enter" && !isInstantUpdate) {
let serializedObject: SerializedObject;
if (type === "Quantity") {
serializedObject = {
type: "Quantity",
value: {
magnitude: Number(newValue),
unit: unit,
} as QuantityMap,
full_access_path: fullAccessPath,
readonly: readOnly,
doc: docString,
};
} else {
serializedObject = {
type: type,
value: Number(newValue),
full_access_path: fullAccessPath,
readonly: readOnly,
doc: docString,
};
}
changeCallback(serializedObject);
return;
} else {
console.debug(key);
@ -272,43 +328,119 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
// Update the input value and maintain the cursor position
if (isInstantUpdate) {
emitUpdate(name, parentPath, Number(newValue));
let serializedObject: SerializedObject;
if (type === "Quantity") {
serializedObject = {
type: "Quantity",
value: {
magnitude: Number(newValue),
unit: unit,
} as QuantityMap,
full_access_path: fullAccessPath,
readonly: readOnly,
doc: docString,
};
} else {
serializedObject = {
type: type,
value: Number(newValue),
full_access_path: fullAccessPath,
readonly: readOnly,
doc: docString,
};
}
changeCallback(serializedObject);
}
setInputString(newValue);
// Save the current cursor position before the component re-renders
setCursorPosition(selectionStart);
cursorPositionRef.current = selectionStart;
};
const handleBlur = () => {
if (!isInstantUpdate) {
// If not in "instant update" mode, emit an update when the input field loses focus
emitUpdate(name, parentPath, Number(inputString));
let serializedObject: SerializedObject;
if (type === "Quantity") {
serializedObject = {
type: "Quantity",
value: {
magnitude: Number(inputString),
unit: unit,
} as QuantityMap,
full_access_path: fullAccessPath,
readonly: readOnly,
doc: docString,
};
} else {
serializedObject = {
type: type,
value: Number(inputString),
full_access_path: fullAccessPath,
readonly: readOnly,
doc: docString,
};
}
changeCallback(serializedObject);
}
};
useEffect(() => {
// Parse the input string to a number for comparison
const numericInputString =
type === "int" ? parseInt(inputString) : parseFloat(inputString);
// Only update the inputString if it's different from the prop value
if (value !== numericInputString) {
setInputString(value.toString());
}
// emitting notification
let notificationMsg = `${fullAccessPath} changed to ${props.value}`;
if (unit === undefined) {
notificationMsg += ".";
} else {
notificationMsg += ` ${unit}.`;
}
addNotification(notificationMsg);
}, [value]);
useEffect(() => {
// Set the cursor position after the component re-renders
const inputElement = document.getElementsByName(id)[0] as HTMLInputElement;
if (inputElement && cursorPositionRef.current !== null) {
inputElement.setSelectionRange(
cursorPositionRef.current,
cursorPositionRef.current,
);
}
});
return (
<div className="numberComponent" id={id}>
{process.env.NODE_ENV === 'development' && (
<div>Render count: {renderCount.current}</div>
)}
<DocStringComponent docString={docString} />
<div className="d-flex">
<InputGroup>
{showName && <InputGroup.Text>{name}</InputGroup.Text>}
<Form.Control
type="text"
value={inputString}
disabled={readOnly}
name={fullAccessPath}
onKeyDown={handleKeyDown}
onBlur={handleBlur}
className={isInstantUpdate && !readOnly ? 'instantUpdate' : ''}
/>
{unit && <InputGroup.Text>{unit}</InputGroup.Text>}
</InputGroup>
</div>
<div className="component numberComponent" id={id}>
{process.env.NODE_ENV === "development" && <div>Render count: {renderCount}</div>}
<InputGroup>
{displayName && (
<InputGroup.Text>
{displayName}
<DocStringComponent docString={docString} />
</InputGroup.Text>
)}
<Form.Control
type="text"
value={inputString}
disabled={readOnly}
onChange={() => {}}
name={id}
onKeyDown={handleKeyDown}
onBlur={handleBlur}
className={isInstantUpdate && !readOnly ? "instantUpdate" : ""}
/>
{unit && <InputGroup.Text>{unit}</InputGroup.Text>}
</InputGroup>
</div>
);
});
NumberComponent.displayName = "NumberComponent";

View File

@ -1,149 +1,184 @@
import React, { useEffect, useRef, useState } from 'react';
import { InputGroup, Form, Row, Col, Collapse, ToggleButton } from 'react-bootstrap';
import { setAttribute } from '../socket';
import { DocStringComponent } from './DocStringComponent';
import { Slider } from '@mui/material';
import { NumberComponent } from './NumberComponent';
import { getIdFromFullAccessPath } from '../utils/stringUtils';
import { LevelName } from './NotificationsComponent';
import React, { useEffect, useState } from "react";
import { InputGroup, Form, Row, Col, Collapse, ToggleButton } from "react-bootstrap";
import { DocStringComponent } from "./DocStringComponent";
import { Slider } from "@mui/material";
import { NumberComponent, NumberObject } from "./NumberComponent";
import { LevelName } from "./NotificationsComponent";
import { SerializedObject } from "../types/SerializedObject";
import { QuantityMap } from "../types/QuantityMap";
import { propsAreEqual } from "../utils/propsAreEqual";
import useRenderCount from "../hooks/useRenderCount";
interface SliderComponentProps {
name: string;
min: number;
max: number;
parentPath?: string;
value: number;
fullAccessPath: string;
min: NumberObject;
max: NumberObject;
value: NumberObject;
readOnly: boolean;
docString: string;
stepSize: number;
docString: string | null;
stepSize: NumberObject;
isInstantUpdate: boolean;
addNotification: (message: string, levelname?: LevelName) => void;
changeCallback?: (value: SerializedObject, callback?: (ack: unknown) => void) => void;
displayName: string;
id: string;
}
const deconstructNumberDict = (
numberDict: NumberObject,
): [number, boolean, string | undefined] => {
let numberMagnitude = 0;
let numberUnit: string | undefined = undefined;
const numberReadOnly = numberDict.readonly;
if (numberDict.type === "int" || numberDict.type === "float") {
numberMagnitude = numberDict.value;
} else if (numberDict.type === "Quantity") {
numberMagnitude = numberDict.value.magnitude;
numberUnit = numberDict.value.unit;
}
return [numberMagnitude, numberReadOnly, numberUnit];
};
export const SliderComponent = React.memo((props: SliderComponentProps) => {
const renderCount = useRef(0);
const renderCount = useRenderCount();
const [open, setOpen] = useState(false);
const {
name,
parentPath,
fullAccessPath,
value,
min,
max,
stepSize,
readOnly,
docString,
isInstantUpdate,
addNotification
addNotification,
changeCallback = () => {},
displayName,
id,
} = props;
const fullAccessPath = parentPath.concat('.' + name);
const id = getIdFromFullAccessPath(fullAccessPath);
useEffect(() => {
renderCount.current++;
});
addNotification(`${fullAccessPath} changed to ${value.value}.`);
}, [props.value.value]);
useEffect(() => {
addNotification(`${parentPath}.${name} changed to ${value}.`);
}, [props.value]);
addNotification(`${fullAccessPath}.min changed to ${min.value}.`);
}, [props.min.value, props.min.type]);
useEffect(() => {
addNotification(`${parentPath}.${name}.min changed to ${min}.`);
}, [props.min]);
addNotification(`${fullAccessPath}.max changed to ${max.value}.`);
}, [props.max.value, props.max.type]);
useEffect(() => {
addNotification(`${parentPath}.${name}.max changed to ${max}.`);
}, [props.max]);
addNotification(`${fullAccessPath}.stepSize changed to ${stepSize.value}.`);
}, [props.stepSize.value, props.stepSize.type]);
useEffect(() => {
addNotification(`${parentPath}.${name}.stepSize changed to ${stepSize}.`);
}, [props.stepSize]);
const emitSliderUpdate = (
name: string,
parentPath: string,
value: number,
callback?: (ack: unknown) => void,
min: number = props.min,
max: number = props.max,
stepSize: number = props.stepSize
) => {
setAttribute(
name,
parentPath,
{
value: value,
min: min,
max: max,
step_size: stepSize
},
callback
);
};
const handleOnChange = (event, newNumber: number | number[]) => {
const handleOnChange = (_: Event, newNumber: number | number[]) => {
// This will never be the case as we do not have a range slider. However, we should
// make sure this is properly handled.
if (Array.isArray(newNumber)) {
newNumber = newNumber[0];
}
emitSliderUpdate(name, parentPath, newNumber);
let serializedObject: SerializedObject;
if (value.type === "Quantity") {
serializedObject = {
type: "Quantity",
value: {
magnitude: newNumber,
unit: value.value.unit,
} as QuantityMap,
full_access_path: `${fullAccessPath}.value`,
readonly: value.readonly,
doc: docString,
};
} else {
serializedObject = {
type: value.type,
value: newNumber,
full_access_path: `${fullAccessPath}.value`,
readonly: value.readonly,
doc: docString,
};
}
changeCallback(serializedObject);
};
const handleValueChange = (newValue: number, valueType: string) => {
switch (valueType) {
case 'min':
emitSliderUpdate(name, parentPath, value, undefined, newValue);
break;
case 'max':
emitSliderUpdate(name, parentPath, value, undefined, min, newValue);
break;
case 'stepSize':
emitSliderUpdate(name, parentPath, value, undefined, min, max, newValue);
break;
default:
break;
const handleValueChange = (
newValue: number,
name: string,
valueObject: NumberObject,
) => {
let serializedObject: SerializedObject;
if (valueObject.type === "Quantity") {
serializedObject = {
type: valueObject.type,
value: {
magnitude: newValue,
unit: valueObject.value.unit,
} as QuantityMap,
full_access_path: `${fullAccessPath}.${name}`,
readonly: valueObject.readonly,
doc: null,
};
} else {
serializedObject = {
type: valueObject.type,
value: newValue,
full_access_path: `${fullAccessPath}.${name}`,
readonly: valueObject.readonly,
doc: null,
};
}
changeCallback(serializedObject);
};
const [valueMagnitude, valueReadOnly, valueUnit] = deconstructNumberDict(value);
const [minMagnitude, minReadOnly] = deconstructNumberDict(min);
const [maxMagnitude, maxReadOnly] = deconstructNumberDict(max);
const [stepSizeMagnitude, stepSizeReadOnly] = deconstructNumberDict(stepSize);
return (
<div className="sliderComponent" id={id}>
{process.env.NODE_ENV === 'development' && (
<div>Render count: {renderCount.current}</div>
)}
<div className="component sliderComponent" id={id}>
{process.env.NODE_ENV === "development" && <div>Render count: {renderCount}</div>}
<DocStringComponent docString={docString} />
<Row>
<Col xs="auto" xl="auto">
<InputGroup.Text>{name}</InputGroup.Text>
<InputGroup.Text>
{displayName}
<DocStringComponent docString={docString} />
</InputGroup.Text>
</Col>
<Col xs="5" xl>
<Slider
style={{ margin: '0px 0px 10px 0px' }}
style={{ margin: "0px 0px 10px 0px" }}
aria-label="Always visible"
// valueLabelDisplay="on"
disabled={readOnly}
value={value}
disabled={valueReadOnly}
value={valueMagnitude}
onChange={(event, newNumber) => handleOnChange(event, newNumber)}
min={min}
max={max}
step={stepSize}
min={minMagnitude}
max={maxMagnitude}
step={stepSizeMagnitude}
marks={[
{ value: min, label: `${min}` },
{ value: max, label: `${max}` }
{ value: minMagnitude, label: `${minMagnitude}` },
{ value: maxMagnitude, label: `${maxMagnitude}` },
]}
/>
</Col>
<Col xs="3" xl>
<NumberComponent
isInstantUpdate={isInstantUpdate}
parentPath={parentPath}
name={name}
docString=""
readOnly={readOnly}
type="float"
value={value}
showName={false}
customEmitUpdate={emitSliderUpdate}
addNotification={() => null}
fullAccessPath={`${fullAccessPath}.value`}
docString={docString}
readOnly={valueReadOnly}
type={value.type}
value={valueMagnitude}
unit={valueUnit}
addNotification={() => {}}
changeCallback={changeCallback}
id={id + "-value"}
/>
</Col>
<Col xs="auto">
@ -173,13 +208,14 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
<Form.Group>
<Row
className="justify-content-center"
style={{ paddingTop: '20px', margin: '10px' }}>
style={{ paddingTop: "20px", margin: "10px" }}>
<Col xs="auto">
<Form.Label>Min Value</Form.Label>
<Form.Control
type="number"
value={min}
onChange={(e) => handleValueChange(Number(e.target.value), 'min')}
value={minMagnitude}
disabled={minReadOnly}
onChange={(e) => handleValueChange(Number(e.target.value), "min", min)}
/>
</Col>
@ -187,8 +223,9 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
<Form.Label>Max Value</Form.Label>
<Form.Control
type="number"
value={max}
onChange={(e) => handleValueChange(Number(e.target.value), 'max')}
value={maxMagnitude}
disabled={maxReadOnly}
onChange={(e) => handleValueChange(Number(e.target.value), "max", max)}
/>
</Col>
@ -196,8 +233,11 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
<Form.Label>Step Size</Form.Label>
<Form.Control
type="number"
value={stepSize}
onChange={(e) => handleValueChange(Number(e.target.value), 'stepSize')}
value={stepSizeMagnitude}
disabled={stepSizeReadOnly}
onChange={(e) =>
handleValueChange(Number(e.target.value), "step_size", stepSize)
}
/>
</Col>
</Row>
@ -205,4 +245,6 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
</Collapse>
</div>
);
});
}, propsAreEqual);
SliderComponent.displayName = "SliderComponent";

View File

@ -1,82 +1,107 @@
import React, { useEffect, useRef, useState } from 'react';
import { Form, InputGroup } from 'react-bootstrap';
import { setAttribute } from '../socket';
import { DocStringComponent } from './DocStringComponent';
import '../App.css';
import { getIdFromFullAccessPath } from '../utils/stringUtils';
import { LevelName } from './NotificationsComponent';
import React, { useEffect, useState } from "react";
import { Form, InputGroup } from "react-bootstrap";
import { DocStringComponent } from "./DocStringComponent";
import "../App.css";
import { LevelName } from "./NotificationsComponent";
import { SerializedObject } from "../types/SerializedObject";
import useRenderCount from "../hooks/useRenderCount";
// TODO: add button functionality
interface StringComponentProps {
name: string;
parentPath?: string;
fullAccessPath: string;
value: string;
readOnly: boolean;
docString: string;
docString: string | null;
isInstantUpdate: boolean;
addNotification: (message: string, levelname?: LevelName) => void;
changeCallback?: (value: SerializedObject, callback?: (ack: unknown) => void) => void;
displayName: string;
id: string;
}
export const StringComponent = React.memo((props: StringComponentProps) => {
const { name, parentPath, readOnly, docString, isInstantUpdate, addNotification } =
props;
const {
fullAccessPath,
readOnly,
docString,
isInstantUpdate,
addNotification,
changeCallback = () => {},
displayName,
id,
} = props;
const renderCount = useRef(0);
const renderCount = useRenderCount();
const [inputString, setInputString] = useState(props.value);
const fullAccessPath = parentPath.concat('.' + name);
const id = getIdFromFullAccessPath(fullAccessPath);
useEffect(() => {
renderCount.current++;
}, [isInstantUpdate, inputString, renderCount]);
useEffect(() => {
// Only update the inputString if it's different from the prop value
if (props.value !== inputString) {
setInputString(props.value);
}
addNotification(`${parentPath}.${name} changed to ${props.value}.`);
addNotification(`${fullAccessPath} changed to ${props.value}.`);
}, [props.value]);
const handleChange = (event) => {
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setInputString(event.target.value);
if (isInstantUpdate) {
setAttribute(name, parentPath, event.target.value);
changeCallback({
type: "str",
value: event.target.value,
full_access_path: fullAccessPath,
readonly: readOnly,
doc: docString,
});
}
};
const handleKeyDown = (event) => {
if (event.key === 'Enter' && !isInstantUpdate) {
setAttribute(name, parentPath, inputString);
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Enter" && !isInstantUpdate) {
changeCallback({
type: "str",
value: inputString,
full_access_path: fullAccessPath,
readonly: readOnly,
doc: docString,
});
event.preventDefault();
}
};
const handleBlur = () => {
if (!isInstantUpdate) {
setAttribute(name, parentPath, inputString);
changeCallback({
type: "str",
value: inputString,
full_access_path: fullAccessPath,
readonly: readOnly,
doc: docString,
});
}
};
return (
<div className={'stringComponent'} id={id}>
{process.env.NODE_ENV === 'development' && (
<div>Render count: {renderCount.current}</div>
)}
<DocStringComponent docString={docString} />
<div className="component stringComponent" id={id}>
{process.env.NODE_ENV === "development" && <div>Render count: {renderCount}</div>}
<InputGroup>
<InputGroup.Text>{name}</InputGroup.Text>
<InputGroup.Text>
{displayName}
<DocStringComponent docString={docString} />
</InputGroup.Text>
<Form.Control
type="text"
name={id}
value={inputString}
disabled={readOnly}
name={name}
onChange={handleChange}
onKeyDown={handleKeyDown}
onBlur={handleBlur}
className={isInstantUpdate && !readOnly ? 'instantUpdate' : ''}
className={isInstantUpdate && !readOnly ? "instantUpdate" : ""}
/>
</InputGroup>
</div>
);
});
StringComponent.displayName = "StringComponent";

View File

@ -0,0 +1,75 @@
import React, { useEffect, useRef, useState } from "react";
import { runMethod } from "../socket";
import { Form, Button, InputGroup, Spinner } from "react-bootstrap";
import { DocStringComponent } from "./DocStringComponent";
import { LevelName } from "./NotificationsComponent";
import useRenderCount from "../hooks/useRenderCount";
export type TaskStatus = "RUNNING" | "NOT_RUNNING";
interface TaskProps {
fullAccessPath: string;
docString: string | null;
status: TaskStatus;
addNotification: (message: string, levelname?: LevelName) => void;
displayName: string;
id: string;
}
export const TaskComponent = React.memo((props: TaskProps) => {
const { fullAccessPath, docString, status, addNotification, displayName, id } = props;
const renderCount = useRenderCount();
const formRef = useRef(null);
const [spinning, setSpinning] = useState(false);
useEffect(() => {
let message: string;
if (status === "RUNNING") {
message = `${fullAccessPath} was started.`;
} else {
message = `${fullAccessPath} was stopped.`;
}
addNotification(message);
setSpinning(false);
}, [status]);
const execute = async (event: React.FormEvent) => {
event.preventDefault();
const method_name = status == "RUNNING" ? "stop" : "start";
const accessPath = [fullAccessPath, method_name]
.filter((element) => element)
.join(".");
setSpinning(true);
runMethod(accessPath);
};
return (
<div className="component taskComponent" id={id}>
{process.env.NODE_ENV === "development" && <div>Render count: {renderCount}</div>}
<Form onSubmit={execute} ref={formRef}>
<InputGroup>
<InputGroup.Text>
{displayName}
<DocStringComponent docString={docString} />
</InputGroup.Text>
<Button id={`button-${id}`} type="submit">
{spinning ? (
<Spinner size="sm" role="status" aria-hidden="true" />
) : status === "RUNNING" ? (
"Stop "
) : (
"Start "
)}
</Button>
</InputGroup>
</Form>
</div>
);
});
TaskComponent.displayName = "TaskComponent";

View File

@ -0,0 +1,18 @@
import { useState, useEffect } from "react";
export default function useLocalStorage(key: string, defaultValue: unknown) {
const [value, setValue] = useState(() => {
const storedValue = localStorage.getItem(key);
if (storedValue) {
return JSON.parse(storedValue);
}
return defaultValue;
});
useEffect(() => {
if (value === undefined) return;
localStorage.setItem(key, JSON.stringify(value));
}, [value, key]);
return [value, setValue];
}

View File

@ -0,0 +1,11 @@
import { useRef, useEffect } from "react";
export default function useRenderCount() {
const count = useRef(0);
useEffect(() => {
count.current += 1;
});
return count.current;
}

View File

@ -0,0 +1,28 @@
import { useContext } from "react";
import { WebSettingsContext } from "../WebSettings";
import { SerializedObject } from "../types/SerializedObject";
export default function useSortedEntries(
props: Record<string, SerializedObject> | SerializedObject[],
) {
const webSettings = useContext(WebSettingsContext);
// Get the order for sorting
const getOrder = (fullAccessPath: string) => {
return webSettings[fullAccessPath]?.displayOrder ?? Number.MAX_SAFE_INTEGER;
};
// Sort entries based on whether props is an array or an object
let sortedEntries;
if (Array.isArray(props)) {
// Need to make copy of array to leave the original array unmodified
sortedEntries = [...props].sort((objectA, objectB) => {
return getOrder(objectA.full_access_path) - getOrder(objectB.full_access_path);
});
} else {
sortedEntries = Object.values(props).sort((objectA, objectB) => {
return getOrder(objectA.full_access_path) - getOrder(objectB.full_access_path);
});
}
return sortedEntries;
}

View File

@ -1,10 +1,13 @@
import App from './App';
import { createRoot } from 'react-dom/client';
import App from "./App";
import React from "react";
import ReactDOM from "react-dom/client";
// Importing the Bootstrap CSS
import 'bootstrap/dist/css/bootstrap.min.css';
import "bootstrap/dist/css/bootstrap.min.css";
// Render the App component into the #root div
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />);
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);

View File

@ -1,36 +1,69 @@
import { io } from 'socket.io-client';
import { io } from "socket.io-client";
import { serializeDict, serializeList } from "./utils/serializationUtils";
import { SerializedObject } from "./types/SerializedObject";
export const hostname =
process.env.NODE_ENV === 'development' ? `localhost` : window.location.hostname;
export const port =
process.env.NODE_ENV === 'development' ? 8001 : window.location.port;
const URL = `ws://${hostname}:${port}/`;
console.debug('Websocket: ', URL);
const hostname =
process.env.NODE_ENV === "development" ? `localhost` : window.location.hostname;
const port = process.env.NODE_ENV === "development" ? 8001 : window.location.port;
export const socket = io(URL, { path: '/ws/socket.io', transports: ['websocket'] });
// Get the forwarded prefix from the global variable
export const forwardedPrefix: string =
(window as any) /* eslint-disable-line @typescript-eslint/no-explicit-any */
.__FORWARDED_PREFIX__ || "";
// Get the forwarded protocol type from the global variable
export const forwardedProto: string =
(window as any) /* eslint-disable-line @typescript-eslint/no-explicit-any */
.__FORWARDED_PROTO__ || "http";
export const setAttribute = (
name: string,
parentPath: string,
value: unknown,
callback?: (ack: unknown) => void
export const authority = `${hostname}:${port}${forwardedPrefix}`;
const wsProto = forwardedProto === "http" ? "ws" : "wss";
const URL = `${wsProto}://${hostname}:${port}/`;
console.debug("Websocket: ", URL);
export const socket = io(URL, {
path: `${forwardedPrefix}/ws/socket.io`,
transports: ["websocket"],
});
export const updateValue = (
serializedObject: SerializedObject,
callback?: (ack: unknown) => void,
) => {
if (callback) {
socket.emit('set_attribute', { name, parent_path: parentPath, value }, callback);
socket.emit(
"update_value",
{ access_path: serializedObject["full_access_path"], value: serializedObject },
callback,
);
} else {
socket.emit('set_attribute', { name, parent_path: parentPath, value });
socket.emit("update_value", {
access_path: serializedObject["full_access_path"],
value: serializedObject,
});
}
};
export const runMethod = (
name: string,
parentPath: string,
kwargs: Record<string, unknown>,
callback?: (ack: unknown) => void
accessPath: string,
args: unknown[] = [],
kwargs: Record<string, unknown> = {},
callback?: (ack: unknown) => void,
) => {
const serializedArgs = serializeList(args);
const serializedKwargs = serializeDict(kwargs);
if (callback) {
socket.emit('run_method', { name, parent_path: parentPath, kwargs }, callback);
socket.emit(
"trigger_method",
{ access_path: accessPath, args: serializedArgs, kwargs: serializedKwargs },
callback,
);
} else {
socket.emit('run_method', { name, parent_path: parentPath, kwargs });
socket.emit("trigger_method", {
access_path: accessPath,
args: serializedArgs,
kwargs: serializedKwargs,
});
}
};

View File

@ -0,0 +1,4 @@
export interface QuantityMap {
magnitude: number;
unit: string;
}

View File

@ -0,0 +1,106 @@
import { QuantityMap } from "./QuantityMap";
interface SignatureDict {
parameters: Record<string, Record<string, unknown>>;
return_annotation: Record<string, unknown>;
}
interface SerializedObjectBase {
full_access_path: string;
doc: string | null;
readonly: boolean;
}
type SerializedInteger = SerializedObjectBase & {
value: number;
type: "int";
};
type SerializedFloat = SerializedObjectBase & {
value: number;
type: "float";
};
type SerializedQuantity = SerializedObjectBase & {
value: QuantityMap;
type: "Quantity";
};
type SerializedBool = SerializedObjectBase & {
value: boolean;
type: "bool";
};
type SerializedString = SerializedObjectBase & {
value: string;
type: "str";
};
export type SerializedEnum = SerializedObjectBase & {
name: string;
value: string;
type: "Enum" | "ColouredEnum";
enum: Record<string, string>;
};
type SerializedList = SerializedObjectBase & {
value: SerializedObject[];
type: "list";
};
type SerializedDict = SerializedObjectBase & {
value: Record<string, SerializedObject>;
type: "dict";
};
type SerializedNoneType = SerializedObjectBase & {
value: null;
type: "NoneType";
};
type SerializedNoValue = SerializedObjectBase & {
value: null;
type: "None";
};
type SerializedMethod = SerializedObjectBase & {
value: "RUNNING" | null;
type: "method";
async: boolean;
signature: SignatureDict;
frontend_render: boolean;
};
type SerializedException = SerializedObjectBase & {
name: string;
value: string;
type: "Exception";
};
type DataServiceTypes =
| "DataService"
| "Image"
| "NumberSlider"
| "DeviceConnection"
| "Task";
type SerializedDataService = SerializedObjectBase & {
name: string;
value: Record<string, SerializedObject>;
type: DataServiceTypes;
};
export type SerializedObject =
| SerializedBool
| SerializedFloat
| SerializedInteger
| SerializedString
| SerializedList
| SerializedDict
| SerializedNoneType
| SerializedMethod
| SerializedException
| SerializedDataService
| SerializedEnum
| SerializedQuantity
| SerializedNoValue;

View File

@ -0,0 +1,17 @@
import deepEqual from "deep-equal";
export const propsAreEqual = <T extends object>(
prevProps: T,
nextProps: T,
): boolean => {
for (const key in nextProps) {
if (typeof nextProps[key] === "object") {
if (!deepEqual(prevProps[key], nextProps[key])) {
return false;
}
} else if (!Object.is(prevProps[key], nextProps[key])) {
return false;
}
}
return true;
};

View File

@ -0,0 +1,97 @@
import { SerializedObject } from "../types/SerializedObject";
const serializePrimitive = (
obj: number | boolean | string | null,
accessPath: string,
): SerializedObject => {
if (typeof obj === "number") {
return {
full_access_path: accessPath,
doc: null,
readonly: false,
type: Number.isInteger(obj) ? "int" : "float",
value: obj,
};
} else if (typeof obj === "boolean") {
return {
full_access_path: accessPath,
doc: null,
readonly: false,
type: "bool",
value: obj,
};
} else if (typeof obj === "string") {
return {
full_access_path: accessPath,
doc: null,
readonly: false,
type: "str",
value: obj,
};
} else if (obj === null) {
return {
full_access_path: accessPath,
doc: null,
readonly: false,
type: "None",
value: null,
};
} else {
throw new Error("Unsupported type for serialization");
}
};
export const serializeList = (obj: unknown[], accessPath = "") => {
const doc = null;
const value = obj.map((item, index) => {
if (
typeof item === "number" ||
typeof item === "boolean" ||
typeof item === "string" ||
item === null
) {
serializePrimitive(
item as number | boolean | string | null,
`${accessPath}[${index}]`,
);
}
});
return {
full_access_path: accessPath,
type: "list",
value,
readonly: false,
doc,
};
};
export const serializeDict = (obj: Record<string, unknown>, accessPath = "") => {
const doc = null;
const value = Object.entries(obj).reduce(
(acc, [key, val]) => {
// Construct the new access path for nested properties
const newPath = `${accessPath}["${key}"]`;
// Serialize each value in the dictionary and assign to the accumulator
if (
typeof val === "number" ||
typeof val === "boolean" ||
typeof val === "string" ||
val === null
) {
acc[key] = serializePrimitive(val as number | boolean | string | null, newPath);
}
return acc;
},
{} as Record<string, SerializedObject>,
);
return {
full_access_path: accessPath,
type: "dict",
value,
readonly: false,
doc,
};
};

View File

@ -1,108 +1,175 @@
export interface SerializedValue {
import { SerializedObject } from "../types/SerializedObject";
export interface State {
type: string;
value: Record<string, unknown> | Array<Record<string, unknown>>;
name: string;
value: Record<string, SerializedObject> | null;
readonly: boolean;
doc: string | null;
async?: boolean;
parameters?: unknown;
}
export type State = Record<string, SerializedValue> | null;
export function setNestedValueByPath(
serializationDict: Record<string, SerializedValue>,
path: string,
serializedValue: SerializedValue
): Record<string, SerializedValue> {
const parentPathParts = path.split('.').slice(0, -1);
const attrName = path.split('.').pop();
/**
* Splits a full access path into its atomic parts, separating attribute names, numeric
* indices (including floating points), and string keys within indices.
*
* @param path The full access path string to be split into components.
* @returns An array of components that make up the path, including attribute names,
* numeric indices, and string keys as separate elements.
*/
export function parseFullAccessPath(path: string): string[] {
// The pattern matches:
// \w+ - Words
// \[\d+\.\d+\] - Floating point numbers inside brackets
// \[\d+\] - Integers inside brackets
// \["[^"]*"\] - Double-quoted strings inside brackets
// \['[^']*'\] - Single-quoted strings inside brackets
const pattern = /\w+|\[\d+\.\d+\]|\[\d+\]|\["[^"]*"\]|\['[^']*'\]/g;
const matches = path.match(pattern);
if (!attrName) {
throw new Error('Invalid path');
return matches ?? []; // Return an empty array if no matches found
}
/**
* Parse a serialized key and convert it to an appropriate type (number or string).
*
* @param serializedKey The serialized key, which might be enclosed in brackets and quotes.
* @returns The processed key as a number or an unquoted string.
*
* Examples:
* console.log(parseSerializedKey("attr_name")); // Outputs: attr_name (string)
* console.log(parseSerializedKey("[123]")); // Outputs: 123 (number)
* console.log(parseSerializedKey("[12.3]")); // Outputs: 12.3 (number)
* console.log(parseSerializedKey("['hello']")); // Outputs: hello (string)
* console.log(parseSerializedKey('["12.34"]')); // Outputs: "12.34" (string)
* console.log(parseSerializedKey('["complex"]'));// Outputs: "complex" (string)
*/
function parseSerializedKey(serializedKey: string): string | number {
// Strip outer brackets if present
if (serializedKey.startsWith("[") && serializedKey.endsWith("]")) {
serializedKey = serializedKey.slice(1, -1);
}
let currentSerializedValue: SerializedValue;
const newSerializationDict: Record<string, SerializedValue> = JSON.parse(
JSON.stringify(serializationDict)
// Strip quotes if the resulting string is quoted
if (
(serializedKey.startsWith("'") && serializedKey.endsWith("'")) ||
(serializedKey.startsWith('"') && serializedKey.endsWith('"'))
) {
return serializedKey.slice(1, -1);
}
// Try converting to a number if the string is not quoted
const parsedNumber = parseFloat(serializedKey);
if (!isNaN(parsedNumber)) {
return parsedNumber;
}
// Return the original string if it's not a valid number
return serializedKey;
}
function getOrCreateItemInContainer(
container: Record<string | number, SerializedObject> | SerializedObject[],
key: string | number,
allowAddKey: boolean,
): SerializedObject {
// Check if the key exists and return the item if it does
if (key in container) {
/* @ts-expect-error Key is in the correct form but converted to type any for some reason */
return container[key];
}
// Handling the case where the key does not exist
if (Array.isArray(container)) {
// Handling arrays
if (allowAddKey && key === container.length) {
container.push(createEmptySerializedObject());
return container[key];
}
throw new Error(`Index out of bounds: ${key}`);
} else {
// Handling objects
if (allowAddKey) {
container[key] = createEmptySerializedObject();
return container[key];
}
throw new Error(`Key not found: ${key}`);
}
}
/**
* Retrieve an item from a container specified by the passed key. Add an item to the
* container if allowAppend is set to True.
*
* @param container Either a dictionary or list of serialized objects.
* @param key The key name or index (as a string) representing the attribute in the container.
* @param allowAppend Whether to allow appending a new entry if the specified index is out of range by exactly one position.
* @returns The serialized object corresponding to the specified key.
* @throws SerializationPathError If the key is invalid or leads to an access error without append permissions.
* @throws SerializationValueError If the expected structure is incorrect.
*/
function getContainerItemByKey(
container: Record<string, SerializedObject> | SerializedObject[],
key: string,
allowAppend = false,
): SerializedObject {
const processedKey = parseSerializedKey(key);
try {
return getOrCreateItemInContainer(container, processedKey, allowAppend);
} catch (error) {
if (error instanceof RangeError) {
throw new Error(`Index '${processedKey}': ${error.message}`);
} else if (error instanceof Error) {
throw new Error(`Key '${processedKey}': ${error.message}`);
}
throw error; // Re-throw if it's not a known error type
}
}
export function setNestedValueByPath(
serializationDict: Record<string, SerializedObject>,
path: string,
serializedValue: SerializedObject,
): Record<string, SerializedObject> {
const pathParts = parseFullAccessPath(path);
const newSerializationDict: Record<string, SerializedObject> = JSON.parse(
JSON.stringify(serializationDict),
);
let currentDict = newSerializationDict;
try {
for (const pathPart of parentPathParts) {
currentSerializedValue = getNextLevelDictByKey(currentDict, pathPart, false);
// @ts-expect-error The value will be of type SerializedValue as we are still
// looping through the parent parts
currentDict = currentSerializedValue['value'];
for (let i = 0; i < pathParts.length - 1; i++) {
const pathPart = pathParts[i];
const nextLevelSerializedObject = getContainerItemByKey(
currentDict,
pathPart,
false,
);
currentDict = nextLevelSerializedObject["value"] as Record<
string,
SerializedObject
>;
}
currentSerializedValue = getNextLevelDictByKey(currentDict, attrName, true);
const finalPart = pathParts[pathParts.length - 1];
const finalObject = getContainerItemByKey(currentDict, finalPart, true);
Object.assign(finalObject, serializedValue);
Object.assign(currentSerializedValue, serializedValue);
return newSerializationDict;
} catch (error) {
console.error(error);
return currentDict;
console.error(`Error occurred trying to change ${path}: ${error}`);
}
return {};
}
function getNextLevelDictByKey(
serializationDict: Record<string, SerializedValue>,
attrName: string,
allowAppend: boolean = false
): SerializedValue {
const [key, index] = parseListAttrAndIndex(attrName);
let currentDict: SerializedValue;
try {
if (index !== null) {
if (!serializationDict[key] || !Array.isArray(serializationDict[key]['value'])) {
throw new Error(`Expected an array at '${key}', but found something else.`);
}
if (index < serializationDict[key]['value'].length) {
currentDict = serializationDict[key]['value'][index];
} else if (allowAppend && index === serializationDict[key]['value'].length) {
// Appending to list
// @ts-expect-error When the index is not null, I expect an array
serializationDict[key]['value'].push({});
currentDict = serializationDict[key]['value'][index];
} else {
throw new Error(`Index out of range for '${key}[${index}]'.`);
}
} else {
if (!serializationDict[key]) {
throw new Error(`Key '${key}' not found.`);
}
currentDict = serializationDict[key];
}
} catch (error) {
throw new Error(`Error occurred trying to access '${attrName}': ${error}`);
}
if (typeof currentDict !== 'object' || currentDict === null) {
throw new Error(
`Expected a dictionary at '${attrName}', but found type '${typeof currentDict}' instead.`
);
}
return currentDict;
}
function parseListAttrAndIndex(attrString: string): [string, number | null] {
let index: number | null = null;
let attrName = attrString;
if (attrString.includes('[') && attrString.endsWith(']')) {
const parts = attrString.split('[');
attrName = parts[0];
const indexPart = parts[1].slice(0, -1); // Removes the closing ']'
if (!isNaN(parseInt(indexPart))) {
index = parseInt(indexPart);
} else {
console.error(`Invalid index format in key: ${attrString}`);
}
}
return [attrName, index];
function createEmptySerializedObject(): SerializedObject {
return {
full_access_path: "",
value: null,
type: "None",
doc: null,
readonly: false,
};
}

View File

@ -1,12 +1,16 @@
export function getIdFromFullAccessPath(fullAccessPath: string) {
// Replace '].' with a single dash
let id = fullAccessPath.replace(/\]\./g, '-');
if (fullAccessPath) {
// Replace '].' with a single dash
let id = fullAccessPath.replace(/\]\./g, "-");
// Replace any character that is not a word character or underscore with a dash
id = id.replace(/[^\w_]+/g, '-');
// Replace any character that is not a word character or underscore with a dash
id = id.replace(/[^\w_]+/g, "-");
// Remove any trailing dashes
id = id.replace(/-+$/, '');
// Remove any trailing dashes
id = id.replace(/-+$/, "");
return id;
return id;
} else {
return "main";
}
}

View File

@ -0,0 +1,31 @@
{
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": [
"ES2020",
"DOM",
"DOM.Iterable"
],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": [
"src"
]
}

View File

@ -1,8 +1,11 @@
{
"compilerOptions": {
"jsx": "react-jsx",
"allowImportingTsExtensions": true,
"noEmit": true,
"esModuleInterop": true
}
}
"files": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.node.json"
}
]
}

View File

@ -0,0 +1,15 @@
{
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true,
"noEmit": true
},
"include": [
"vite.config.ts"
]
}

13
frontend/vite.config.ts Normal file
View File

@ -0,0 +1,13 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
build: {
outDir: "../src/pydase/frontend",
},
esbuild: {
drop: ["console", "debugger"],
},
});

View File

@ -4,8 +4,22 @@ edit_uri: blob/docs/docs/
nav:
- Home: index.md
- Getting Started: getting-started.md
- User Guide:
- User Guide:
- Components Guide: user-guide/Components.md
- Interaction:
- Overview: user-guide/interaction/README.md
- Auto-generated Frontend: user-guide/interaction/Auto-generated-Frontend.md
- RESTful API: user-guide/interaction/RESTful-API.md
- Python Client: user-guide/interaction/Python-Client.md
- Achieving Service Persistence: user-guide/Service_Persistence.md
- Understanding Tasks: user-guide/Tasks.md
- Understanding Units: user-guide/Understanding-Units.md
- Validating Property Setters: user-guide/Validating-Property-Setters.md
- Configuring pydase: user-guide/Configuration.md
- Logging in pydase: user-guide/Logging.md
- Advanced:
- Deploying behind a Reverse Proxy: user-guide/advanced/Reverse-Proxy.md
- Connecting through a SOCKS Proxy: user-guide/advanced/SOCKS-Proxy.md
- Developer Guide:
- Developer Guide: dev-guide/README.md
- API Reference: dev-guide/api.md
@ -16,7 +30,11 @@ nav:
- Contributing: about/contributing.md
- License: about/license.md
theme: readthedocs
theme:
logo: images/logo-colour.png
name: material
features:
- content.code.copy
extra_css:
- css/extra.css
@ -26,17 +44,46 @@ markdown_extensions:
- toc:
permalink: true
- pymdownx.highlight:
use_pygments: true
anchor_linenums: true
line_spans: __span
pygments_lang_class: true
- pymdownx.snippets
- pymdownx.superfences
# - pymdownx.highlight:
# - pymdownx.inlinehilite
- pymdownx.inlinehilite
plugins:
- include-markdown
- search
- mkdocstrings
- include-markdown
- search
- mkdocstrings:
handlers:
python:
paths: [src] # search packages in the src folder
inventories:
- https://docs.python.org/3/objects.inv
- https://docs.pydantic.dev/latest/objects.inv
- https://confz.readthedocs.io/en/latest/objects.inv
- https://python-socketio.readthedocs.io/en/stable/objects.inv
options:
show_source: true
inherited_members: true
merge_init_into_class: true
show_signature_annotations: true
signature_crossrefs: true
separate_signature: true
docstring_options:
ignore_init_summary: true
# docstring_section_style: list
heading_level: 2
parameter_headings: true
show_root_heading: true
show_root_full_path: true
show_symbol_type_heading: true
show_symbol_type_toc: true
# summary: true
unwrap_annotated: true
- swagger-ui-tag
watch:
- src/pydase

3121
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,52 +1,65 @@
[tool.poetry]
[project]
name = "pydase"
version = "0.4.0"
version = "0.10.15"
description = "A flexible and robust Python library for creating, managing, and interacting with data services, with built-in support for web and RPC servers, and customizable features for diverse use cases."
authors = ["Mose Mueller <mosmuell@ethz.ch>"]
authors = [
{name = "Mose Müller",email = "mosemueller@gmail.com"}
]
readme = "README.md"
packages = [{ include = "pydase", from = "src" }]
requires-python = ">=3.10,<4.0"
dependencies = [
"toml (>=0.10.2,<0.11.0)",
"python-socketio (>=5.13.0,<6.0.0)",
"confz (>=2.1.0,<3.0.0)",
"pint (>=0.24.4,<0.25.0)",
"websocket-client (>=1.8.0,<2.0.0)",
"aiohttp (>=3.11.18,<4.0.0)",
"click (>=8.2.0,<9.0.0)",
"aiohttp-middlewares (>=2.4.0,<3.0.0)",
"anyio (>=4.9.0,<5.0.0)"
]
[project.optional-dependencies]
socks = ["aiohttp-socks (>=0.10.1,<0.11.0)"]
[tool.poetry.dependencies]
python = "^3.10"
rpyc = "^5.3.1"
fastapi = "^0.100.0"
uvicorn = "^0.22.0"
toml = "^0.10.2"
python-socketio = "^5.8.0"
websockets = "^11.0.3"
confz = "^2.0.0"
pint = "^0.22"
pillow = "^10.0.0"
[tool.poetry]
packages = [{include = "pydase", from = "src"}]
[tool.poetry.group.dev]
optional = true
[tool.poetry.group.dev.dependencies]
types-toml = "^0.10.8.6"
pytest = "^7.4.0"
pytest-cov = "^4.1.0"
mypy = "^1.4.1"
matplotlib = "^3.7.2"
pyright = "^1.1.323"
pytest-mock = "^3.11.1"
ruff = "^0.1.5"
types-toml = "^0.10.8.20240310"
pytest = "^8.3.5"
pytest-cov = "^6.1.1"
mypy = "^1.15.0"
matplotlib = "^3.10.3"
pyright = "^1.1.400"
pytest-mock = "^3.14.0"
ruff = "^0.11.10"
pytest-asyncio = "^0.26.0"
[tool.poetry.group.docs]
optional = true
[tool.poetry.group.docs.dependencies]
mkdocs = "^1.5.2"
mkdocs-include-markdown-plugin = "^3.9.1"
mkdocstrings = "^0.22.0"
pymdown-extensions = "^10.1"
mkdocs-material = "^9.6.14"
mkdocs-include-markdown-plugin = "^7.1.5"
mkdocstrings = {extras = ["python"], version = "^0.29.1"}
pymdown-extensions = "^10.15"
mkdocs-swagger-ui-tag = "^0.7.1"
[build-system]
requires = ["poetry-core"]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.ruff]
target-version = "py310" # Always generate Python 3.10-compatible code
extend-exclude = [
"docs", "frontend"
]
[tool.ruff.lint]
select = [
"ASYNC", # flake8-async
"C4", # flake8-comprehensions
@ -77,12 +90,9 @@ select = [
"W", # pycodestyle warnings
]
ignore = [
"E203", # whitespace-before-punctuation
"W292", # missing-newline-at-end-of-file
"RUF006", # asyncio-dangling-task
"PERF203", # try-except-in-loop
]
extend-exclude = [
"docs", "frontend"
"ASYNC110", # async-busy-wait
]
[tool.ruff.lint.mccabe]
@ -101,3 +111,10 @@ disallow_incomplete_defs = true
disallow_any_generics = true
check_untyped_defs = true
ignore_missing_imports = false
[tool.pytest.ini_options]
asyncio_default_fixture_loop_scope = "function"
filterwarnings = [
# I don't controll the usage of the timeout
"ignore:parameter 'timeout' of type 'float' is deprecated, please use 'timeout=ClientWSTimeout"
]

View File

@ -1,3 +1,4 @@
from pydase.client.client import Client
from pydase.data_service import DataService
from pydase.server import Server
from pydase.utils.logging import setup_logging
@ -5,6 +6,7 @@ from pydase.utils.logging import setup_logging
setup_logging()
__all__ = [
"Client",
"DataService",
"Server",
]

View File

@ -0,0 +1,3 @@
from pydase.client.client import Client
__all__ = ["Client"]

255
src/pydase/client/client.py Normal file
View File

@ -0,0 +1,255 @@
import asyncio
import logging
import socket
import sys
import threading
import urllib.parse
from builtins import ModuleNotFoundError
from types import TracebackType
from typing import TYPE_CHECKING, Any, TypedDict, cast
import aiohttp
import socketio # type: ignore
from pydase.client.proxy_class import ProxyClass
from pydase.client.proxy_loader import ProxyLoader
from pydase.utils.serialization.deserializer import loads
from pydase.utils.serialization.types import SerializedDataService, SerializedObject
if sys.version_info < (3, 11):
from typing_extensions import Self
else:
from typing import Self
logger = logging.getLogger(__name__)
class NotifyDataDict(TypedDict):
full_access_path: str
value: SerializedObject
class NotifyDict(TypedDict):
data: NotifyDataDict
def asyncio_loop_thread(loop: asyncio.AbstractEventLoop) -> None:
asyncio.set_event_loop(loop)
try:
loop.run_forever()
finally:
loop.close()
class Client:
"""A client for connecting to a remote pydase service using Socket.IO. This client
handles asynchronous communication with a service, manages events such as
connection, disconnection, and updates, and ensures that the proxy object is
up-to-date with the server state.
Args:
url: The URL of the pydase Socket.IO server. This should always contain the
protocol (e.g., `ws` or `wss`) and the hostname, and can optionally include
a path prefix (e.g., `ws://localhost:8001/service`).
block_until_connected: If set to True, the constructor will block until the
connection to the service has been established. This is useful for ensuring
the client is ready to use immediately after instantiation. Default is True.
sio_client_kwargs: Additional keyword arguments passed to the underlying
[`AsyncClient`][socketio.AsyncClient]. This allows fine-tuning of the
client's behaviour (e.g., reconnection attempts or reconnection delay).
client_id: An optional client identifier. This ID is sent to the server as the
`X-Client-Id` HTTP header. It can be used for logging or authentication
purposes on the server side. If not provided, it defaults to the hostname
of the machine running the client.
proxy_url: An optional proxy URL to route the connection through. This is useful
if the service is only reachable via an SSH tunnel or behind a firewall
(e.g., `socks5://localhost:2222`).
Example:
Connect to a service directly:
```python
client = pydase.Client(url="ws://localhost:8001")
```
Connect over a secure connection:
```python
client = pydase.Client(url="wss://my-service.example.com")
```
Connect using a SOCKS5 proxy (e.g., through an SSH tunnel):
```bash
ssh -D 2222 user@gateway.example.com
```
```python
client = pydase.Client(
url="ws://remote-server:8001",
proxy_url="socks5://localhost:2222"
)
```
"""
def __init__(
self,
*,
url: str,
block_until_connected: bool = True,
sio_client_kwargs: dict[str, Any] = {},
client_id: str | None = None,
proxy_url: str | None = None,
):
# Parse the URL to separate base URL and path prefix
parsed_url = urllib.parse.urlparse(url)
# Construct the base URL without the path
self._base_url = urllib.parse.urlunparse(
(parsed_url.scheme, parsed_url.netloc, "", "", "", "")
)
# Store the path prefix (e.g., "/service" in "ws://localhost:8081/service")
self._path_prefix = parsed_url.path.rstrip("/") # Remove trailing slash if any
self._url = url
self._proxy_url = proxy_url
self._client_id = client_id or socket.gethostname()
self._sio_client_kwargs = sio_client_kwargs
self._loop: asyncio.AbstractEventLoop | None = None
self._thread: threading.Thread | None = None
self.proxy: ProxyClass
"""A proxy object representing the remote service, facilitating interaction as
if it were local."""
self.connect(block_until_connected=block_until_connected)
def __enter__(self) -> Self:
return self
def __exit__(
self,
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> None:
self.disconnect()
def connect(self, block_until_connected: bool = True) -> None:
if self._thread is None or self._loop is None:
self._loop = self._initialize_loop_and_thread()
self._initialize_socketio_client()
self.proxy = ProxyClass(
sio_client=self._sio,
loop=self._loop,
reconnect=self.connect,
)
connection_future = asyncio.run_coroutine_threadsafe(
self._connect(), self._loop
)
if block_until_connected:
connection_future.result()
def _initialize_socketio_client(self) -> None:
if self._proxy_url is not None:
try:
import aiohttp_socks.connector
except ModuleNotFoundError:
raise ModuleNotFoundError(
"Missing dependency 'aiohttp_socks'. To use SOCKS5 proxy support, "
"install the optional 'socks' extra:\n\n"
' pip install "pydase[socks]"\n\n'
"This is required when specifying a `proxy_url` for "
"`pydase.Client`."
)
session = aiohttp.ClientSession(
connector=aiohttp_socks.connector.ProxyConnector.from_url(
url=self._proxy_url, loop=self._loop
),
loop=self._loop,
)
self._sio = socketio.AsyncClient(
http_session=session, **self._sio_client_kwargs
)
else:
self._sio = socketio.AsyncClient(**self._sio_client_kwargs)
def _initialize_loop_and_thread(self) -> asyncio.AbstractEventLoop:
"""Initialize a new asyncio event loop, start it in a background thread,
and create the ProxyClass instance bound to that loop.
"""
loop = asyncio.new_event_loop()
self._thread = threading.Thread(
target=asyncio_loop_thread,
args=(loop,),
daemon=True,
)
self._thread.start()
return loop
def disconnect(self) -> None:
if self._loop is not None and self._thread is not None:
connection_future = asyncio.run_coroutine_threadsafe(
self._disconnect(), self._loop
)
connection_future.result()
# Stop the event loop and thread
self._loop.call_soon_threadsafe(self._loop.stop)
self._thread.join()
self._thread = None
async def _connect(self) -> None:
logger.debug("Connecting to server '%s' ...", self._url)
await self._setup_events()
headers = {}
if self._client_id is not None:
headers["X-Client-Id"] = self._client_id
await self._sio.connect(
url=self._base_url,
headers=headers,
socketio_path=f"{self._path_prefix}/ws/socket.io",
transports=["websocket"],
retry=True,
)
async def _disconnect(self) -> None:
await self._sio.disconnect()
async def _setup_events(self) -> None:
self._sio.on("connect", self._handle_connect)
self._sio.on("disconnect", self._handle_disconnect)
self._sio.on("notify", self._handle_update)
async def _handle_connect(self) -> None:
logger.debug("Connected to '%s' ...", self._url)
serialized_object = cast(
"SerializedDataService", await self._sio.call("service_serialization")
)
ProxyLoader.update_data_service_proxy(
self.proxy, serialized_object=serialized_object
)
serialized_object["type"] = "DeviceConnection"
if self.proxy._service_representation is not None:
# need to use object.__setattr__ to not trigger an observer notification
object.__setattr__(self.proxy, "_service_representation", serialized_object)
if TYPE_CHECKING:
self.proxy._service_representation = serialized_object # type: ignore
self.proxy._notify_changed("", self.proxy)
self.proxy._connected = True
async def _handle_disconnect(self) -> None:
logger.debug("Disconnected from '%s' ...", self._url)
self.proxy._connected = False
async def _handle_update(self, data: NotifyDict) -> None:
self.proxy._notify_changed(
data["data"]["full_access_path"],
loads(data["data"]["value"]),
)

View File

@ -0,0 +1,112 @@
import asyncio
import logging
from collections.abc import Callable
from copy import deepcopy
from typing import TYPE_CHECKING, cast
import socketio # type: ignore
import pydase.components
from pydase.client.proxy_loader import ProxyClassMixin
from pydase.utils.helpers import get_attribute_doc
from pydase.utils.serialization.types import SerializedDataService, SerializedObject
logger = logging.getLogger(__name__)
class ProxyClass(ProxyClassMixin, pydase.components.DeviceConnection):
"""
A proxy class that serves as the interface for interacting with device connections
via a socket.io client in an asyncio environment.
Args:
sio_client:
The socket.io client instance used for asynchronous communication with the
pydase service server.
loop:
The event loop in which the client operations are managed and executed.
reconnect:
The method that is called periodically when the client is not connected.
This class is used to create a proxy object that behaves like a local representation
of a remote pydase service, facilitating direct interaction as if it were local
while actually communicating over network protocols.
It can also be used as an attribute of a pydase service itself, e.g.
```python
import pydase
class MyService(pydase.DataService):
proxy = pydase.Client(
hostname="...", port=8001, block_until_connected=False
).proxy
if __name__ == "__main__":
service = MyService()
server = pydase.Server(service, web_port=8002).run()
```
"""
def __init__(
self,
sio_client: socketio.AsyncClient,
loop: asyncio.AbstractEventLoop,
reconnect: Callable[..., None],
) -> None:
if TYPE_CHECKING:
self._service_representation: None | SerializedObject = None
super().__init__()
pydase.components.DeviceConnection.__init__(self)
self._initialise(sio_client=sio_client, loop=loop)
object.__setattr__(self, "_service_representation", None)
self.reconnect = reconnect
def serialize(self) -> SerializedObject:
if self._service_representation is None:
serialization_future = cast(
"asyncio.Future[SerializedDataService]",
asyncio.run_coroutine_threadsafe(
self._sio.call("service_serialization"), self._loop
),
)
# need to use object.__setattr__ to not trigger an observer notification
object.__setattr__(
self, "_service_representation", serialization_future.result()
)
if TYPE_CHECKING:
self._service_representation = serialization_future.result()
device_connection_value = cast(
"dict[str, SerializedObject]",
pydase.components.DeviceConnection().serialize()["value"],
)
readonly = False
doc = get_attribute_doc(self)
obj_name = self.__class__.__name__
value = {
**cast(
"dict[str, SerializedObject]",
# need to deepcopy to not overwrite the _service_representation dict
# when adding a prefix with add_prefix_to_full_access_path
deepcopy(self._service_representation["value"]),
),
**device_connection_value,
}
return {
"full_access_path": "",
"name": obj_name,
"type": "DeviceConnection",
"value": value,
"readonly": readonly,
"doc": doc,
}
def connect(self) -> None:
if not self._sio.reconnection or self._sio.reconnection_attempts > 0:
self.reconnect(block_until_connected=False)

View File

@ -0,0 +1,391 @@
import asyncio
import logging
from collections.abc import Iterable
from typing import TYPE_CHECKING, Any, cast
import socketio # type: ignore
from typing_extensions import SupportsIndex
from pydase.utils.serialization.deserializer import Deserializer, loads
from pydase.utils.serialization.serializer import dump
from pydase.utils.serialization.types import SerializedObject
if TYPE_CHECKING:
from collections.abc import Callable
logger = logging.getLogger(__name__)
class ProxyAttributeError(Exception): ...
def trigger_method(
sio_client: socketio.AsyncClient,
loop: asyncio.AbstractEventLoop,
access_path: str,
args: list[Any],
kwargs: dict[str, Any],
) -> Any:
async def async_trigger_method() -> Any:
return await sio_client.call(
"trigger_method",
{
"access_path": access_path,
"args": dump(args),
"kwargs": dump(kwargs),
},
)
result: SerializedObject | None = asyncio.run_coroutine_threadsafe(
async_trigger_method(),
loop=loop,
).result()
if result is not None:
return ProxyLoader.loads_proxy(
serialized_object=result, sio_client=sio_client, loop=loop
)
return None
def update_value(
sio_client: socketio.AsyncClient,
loop: asyncio.AbstractEventLoop,
access_path: str,
value: Any,
) -> Any:
async def set_result() -> Any:
return await sio_client.call(
"update_value",
{
"access_path": access_path,
"value": dump(value),
},
)
result: SerializedObject | None = asyncio.run_coroutine_threadsafe(
set_result(),
loop=loop,
).result()
if result is not None:
ProxyLoader.loads_proxy(
serialized_object=result, sio_client=sio_client, loop=loop
)
class ProxyDict(dict[str, Any]):
def __init__(
self,
original_dict: dict[str, Any],
parent_path: str,
sio_client: socketio.AsyncClient,
loop: asyncio.AbstractEventLoop,
) -> None:
super().__init__(original_dict)
self._parent_path = parent_path
self._loop = loop
self._sio = sio_client
def __setitem__(self, key: str, value: Any) -> None:
observer_key = key
if isinstance(key, str):
observer_key = f'"{key}"'
full_access_path = f"{self._parent_path}[{observer_key}]"
update_value(self._sio, self._loop, full_access_path, value)
def pop(self, key: str) -> Any: # type: ignore
"""Removes the element from the dictionary on the server. It does not return
any proxy as the corresponding object on the server does not live anymore."""
full_access_path = f"{self._parent_path}.pop"
trigger_method(self._sio, self._loop, full_access_path, [key], {})
class ProxyList(list[Any]):
def __init__(
self,
original_list: list[Any],
parent_path: str,
sio_client: socketio.AsyncClient,
loop: asyncio.AbstractEventLoop,
) -> None:
super().__init__(original_list)
self._parent_path = parent_path
self._loop = loop
self._sio = sio_client
def __setitem__(self, key: int, value: Any) -> None: # type: ignore[override]
full_access_path = f"{self._parent_path}[{key}]"
update_value(self._sio, self._loop, full_access_path, value)
def append(self, object_: Any, /) -> None:
full_access_path = f"{self._parent_path}.append"
trigger_method(self._sio, self._loop, full_access_path, [object_], {})
def clear(self) -> None:
full_access_path = f"{self._parent_path}.clear"
trigger_method(self._sio, self._loop, full_access_path, [], {})
def extend(self, iterable: Iterable[Any], /) -> None:
full_access_path = f"{self._parent_path}.extend"
trigger_method(self._sio, self._loop, full_access_path, [iterable], {})
def insert(self, index: SupportsIndex, object_: Any, /) -> None:
full_access_path = f"{self._parent_path}.insert"
trigger_method(self._sio, self._loop, full_access_path, [index, object_], {})
def pop(self, index: SupportsIndex = -1, /) -> Any:
full_access_path = f"{self._parent_path}.pop"
return trigger_method(self._sio, self._loop, full_access_path, [index], {})
def remove(self, value: Any, /) -> None:
full_access_path = f"{self._parent_path}.remove"
trigger_method(self._sio, self._loop, full_access_path, [value], {})
class ProxyClassMixin:
def __init__(self) -> None:
# declare before DataService init to avoid warning messaged
self._observers: dict[str, Any] = {}
self._proxy_getters: dict[str, Callable[..., Any]] = {}
self._proxy_setters: dict[str, Callable[..., Any]] = {}
self._proxy_methods: dict[str, Callable[..., Any]] = {}
def _initialise(
self,
sio_client: socketio.AsyncClient,
loop: asyncio.AbstractEventLoop,
) -> None:
self._loop = loop
self._sio = sio_client
def __dir__(self) -> list[str]:
"""Used to provide tab completion on CLI / notebook"""
static_dir = super().__dir__()
return sorted({*static_dir, *self._proxy_getters, *self._proxy_methods.keys()})
def __getattribute__(self, name: str) -> Any:
try:
if name in super().__getattribute__("_proxy_getters"):
return super().__getattribute__("_proxy_getters")[name]()
if name in super().__getattribute__("_proxy_methods"):
return super().__getattribute__("_proxy_methods")[name]
except AttributeError:
pass
return super().__getattribute__(name)
def __setattr__(self, name: str, value: Any) -> None:
try:
if name in super().__getattribute__("_proxy_setters"):
return super().__getattribute__("_proxy_setters")[name](value)
if name in super().__getattribute__("_proxy_getters"):
raise ProxyAttributeError(
f"Proxy attribute {name!r} of {type(self).__name__!r} is readonly!"
)
except AttributeError:
pass
return super().__setattr__(name, value)
def _handle_serialized_method(
self, attr_name: str, serialized_object: SerializedObject
) -> None:
if serialized_object["type"] == "method":
self._add_method_proxy(attr_name, serialized_object)
def _add_method_proxy(
self, attr_name: str, serialized_object: SerializedObject
) -> None:
def method_proxy(*args: Any, **kwargs: Any) -> Any:
return trigger_method(
self._sio,
self._loop,
serialized_object["full_access_path"],
list(args),
kwargs,
)
dict.__setitem__(self._proxy_methods, attr_name, method_proxy)
def _add_attr_proxy(
self, attr_name: str, serialized_object: SerializedObject
) -> None:
self._add_getattr_proxy(attr_name, serialized_object=serialized_object)
if not serialized_object["readonly"]:
self._add_setattr_proxy(attr_name, serialized_object=serialized_object)
def _add_setattr_proxy(
self, attr_name: str, serialized_object: SerializedObject
) -> None:
self._add_getattr_proxy(attr_name, serialized_object=serialized_object)
if not serialized_object["readonly"]:
def setter_proxy(value: Any) -> None:
update_value(
self._sio, self._loop, serialized_object["full_access_path"], value
)
dict.__setitem__(self._proxy_setters, attr_name, setter_proxy) # type: ignore
def _add_getattr_proxy(
self, attr_name: str, serialized_object: SerializedObject
) -> None:
def getter_proxy() -> Any:
async def get_result() -> Any:
return await self._sio.call(
"get_value", serialized_object["full_access_path"]
)
result = asyncio.run_coroutine_threadsafe(
get_result(),
loop=self._loop,
).result()
return ProxyLoader.loads_proxy(result, self._sio, self._loop)
dict.__setitem__(self._proxy_getters, attr_name, getter_proxy) # type: ignore
class ProxyLoader:
@staticmethod
def load_list_proxy(
serialized_object: SerializedObject,
sio_client: socketio.AsyncClient,
loop: asyncio.AbstractEventLoop,
) -> Any:
return ProxyList(
[
ProxyLoader.loads_proxy(item, sio_client, loop)
for item in cast("list[SerializedObject]", serialized_object["value"])
],
parent_path=serialized_object["full_access_path"],
sio_client=sio_client,
loop=loop,
)
@staticmethod
def load_dict_proxy(
serialized_object: SerializedObject,
sio_client: socketio.AsyncClient,
loop: asyncio.AbstractEventLoop,
) -> Any:
return ProxyDict(
{
key: ProxyLoader.loads_proxy(value, sio_client, loop)
for key, value in cast(
"dict[str, SerializedObject]", serialized_object["value"]
).items()
},
parent_path=serialized_object["full_access_path"],
sio_client=sio_client,
loop=loop,
)
@staticmethod
def update_data_service_proxy(
proxy_class: ProxyClassMixin,
serialized_object: SerializedObject,
) -> Any:
proxy_class._proxy_getters.clear()
proxy_class._proxy_setters.clear()
proxy_class._proxy_methods.clear()
for key, value in cast(
"dict[str, SerializedObject]", serialized_object["value"]
).items():
type_handler: dict[str | None, None | Callable[..., Any]] = {
None: None,
"int": proxy_class._add_attr_proxy,
"float": proxy_class._add_attr_proxy,
"bool": proxy_class._add_attr_proxy,
"str": proxy_class._add_attr_proxy,
"NoneType": proxy_class._add_attr_proxy,
"Quantity": proxy_class._add_attr_proxy,
"Enum": proxy_class._add_attr_proxy,
"ColouredEnum": proxy_class._add_attr_proxy,
"method": proxy_class._handle_serialized_method,
"list": proxy_class._add_getattr_proxy,
"dict": proxy_class._add_getattr_proxy,
}
# First go through handled types (as ColouredEnum is also within the
# components)
handler = type_handler.get(value["type"])
if handler:
handler(key, value)
else:
proxy_class._add_getattr_proxy(key, value)
@staticmethod
def load_data_service_proxy(
serialized_object: SerializedObject,
sio_client: socketio.AsyncClient,
loop: asyncio.AbstractEventLoop,
) -> Any:
# Custom types like Components or DataService classes
component_class = cast(
"type", Deserializer.get_service_base_class(serialized_object["type"])
)
class_bases = (
ProxyClassMixin,
component_class,
)
proxy_base_class: type[ProxyClassMixin] = type(
serialized_object["name"], # type: ignore
class_bases,
{},
)
proxy_class_instance = proxy_base_class()
proxy_class_instance._initialise(sio_client=sio_client, loop=loop)
ProxyLoader.update_data_service_proxy(
proxy_class=proxy_class_instance, serialized_object=serialized_object
)
return proxy_class_instance
@staticmethod
def load_default(
serialized_object: SerializedObject,
sio_client: socketio.AsyncClient,
loop: asyncio.AbstractEventLoop,
) -> Any:
return loads(serialized_object)
@staticmethod
def loads_proxy(
serialized_object: SerializedObject,
sio_client: socketio.AsyncClient,
loop: asyncio.AbstractEventLoop,
) -> Any:
type_handler: dict[str | None, None | Callable[..., Any]] = {
"int": ProxyLoader.load_default,
"float": ProxyLoader.load_default,
"bool": ProxyLoader.load_default,
"str": ProxyLoader.load_default,
"NoneType": ProxyLoader.load_default,
"Quantity": ProxyLoader.load_default,
"Enum": ProxyLoader.load_default,
"ColouredEnum": ProxyLoader.load_default,
"Exception": ProxyLoader.load_default,
"list": ProxyLoader.load_list_proxy,
"dict": ProxyLoader.load_dict_proxy,
}
# First go through handled types (as ColouredEnum is also within the components)
handler = type_handler.get(serialized_object["type"])
if handler:
return handler(
serialized_object=serialized_object, sio_client=sio_client, loop=loop
)
return ProxyLoader.load_data_service_proxy(
serialized_object=serialized_object, sio_client=sio_client, loop=loop
)

View File

@ -28,11 +28,13 @@ print(my_service.voltage.value) # Output: 5
"""
from pydase.components.coloured_enum import ColouredEnum
from pydase.components.device_connection import DeviceConnection
from pydase.components.image import Image
from pydase.components.number_slider import NumberSlider
__all__ = [
"NumberSlider",
"Image",
"ColouredEnum",
"DeviceConnection",
"Image",
"NumberSlider",
]

Some files were not shown because too many files have changed in this diff Show More