68 Commits

Author SHA1 Message Date
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
36 changed files with 1538 additions and 203 deletions

View File

@@ -1,5 +1,5 @@
<!--introduction-start-->
# pydase <!-- omit from toc -->
![pydase Banner](./docs/images/logo-with-text.png)
[![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/)
@@ -184,44 +184,41 @@ For more information, see [here][RESTful API].
## Configuring pydase via Environment Variables
Configuring `pydase` through environment variables enhances flexibility, security, and reusability. This approach allows for easy adaptation of services across different environments without code changes, promoting scalability and maintainability. With that, it simplifies deployment processes and facilitates centralized configuration management. Moreover, environment variables enable separation of configuration from code, aiding in secure and collaborative development.
`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:
`pydase` offers various configurable options:
- **`ENVIRONMENT`**:
Defines the operation mode (`"development"` or `"production"`), which influences
behaviour such as logging (see [Logging in pydase](#logging-in-pydase)).
- **`ENVIRONMENT`**: Sets the operation mode to either "development" or "production". Affects logging behaviour (see [logging section](#logging-in-pydase)).
- **`SERVICE_CONFIG_DIR`**: Specifies the directory for service configuration files, like `web_settings.json`. This directory can also be used to hold user-defined configuration files. Default is the `config` folder in the service root folder. The variable can be accessed through:
- **`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 port number for the web server. This has to be different for each services running on the same host. Default is 8001.
- **`GENERATE_WEB_SETTINGS`**: When set to true, generates / updates the `web_settings.json` file. If the file already exists, only new entries are appended.
- **`SERVICE_WEB_PORT`**:
Defines the web servers port. Ensure each service on the same host uses a unique
port. Default: `8001`.
Some of those settings can also be altered directly in code when initializing the server:
- **`GENERATE_WEB_SETTINGS`**:
When `true`, generates or updates the `web_settings.json` file. Existing entries are
preserved, and new entries are appended.
```python
import pathlib
from pydase import Server
from your_service_module import YourService
server = Server(
YourService(),
web_port=8080,
config_dir=pathlib.Path("other_config_dir"), # note that you need to provide an argument of type pathlib.Path
generate_web_settings=True
).run()
```
For more information, see [Configuring pydase](https://pydase.readthedocs.io/en/stable/user-guide/Configuration/).
## Customizing the Web Interface
`pydase` allows you to enhance the user experience by customizing the web interface's appearance through
1. a custom CSS file, and
2. tailoring the frontend component layout and display style.
2. a custom favicon image, and
3. tailoring the frontend component layout and display style.
You can also provide a custom frontend source if you need even more flexibility.
@@ -280,6 +277,7 @@ We welcome contributions! Please see [contributing.md](https://pydase.readthedoc
`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

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

View File

@@ -4,6 +4,7 @@
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

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](https://github.com/tiqi-group/pydase?tab=readme-ov-file#logging-in-pydase)).
- **`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. 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,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

@@ -21,7 +21,8 @@ The frontend uses a component-based approach, representing various data types an
`pydase` allows you to enhance the user experience by customizing the web interface's appearance through
1. a custom CSS file, and
2. tailoring the frontend component layout and display style.
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.
@@ -51,6 +52,34 @@ This will apply the styles defined in `custom.css` to the web interface, allowin
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.
@@ -60,7 +89,7 @@ Each key in the file corresponds to the full access path of public attributes, p
- **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 `SERVICE_CONFIG_DIR`. You can generate a `web_settings.json` file by setting the `GENERATE_WEB_SETTINGS` to `True`. For more information, see the [configuration section](#configuring-pydase-via-environment-variables).
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).
For example, styling the following service

View File

@@ -1,60 +1,86 @@
# Python RPC Client
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:
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 the hostname and port with the IP address and the port of the machine where
# the service is running, respectively
# Replace <ip_addr> and <service_port> with the appropriate values for your service
client_proxy = pydase.Client(url="ws://<ip_addr>:<service_port>").proxy
# client_proxy = pydase.Client(url="wss://your-domain.ch").proxy # if your service uses ssl-encryption
# 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 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.
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 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.
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.
## Context Manager
## Context Manager Support
You can also use the client as a context manager which automatically opens and closes the connection again:
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>()
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 such as Python interpreters and Jupyter notebooks, the proxy class supports tab completion, which allows users to explore available methods and attributes.
In interactive environments like Python interpreters or Jupyter notebooks, the proxy supports tab completion. This allows users to explore available methods and attributes.
## Integration within Other Services
## Integrating the Client into Another Service
You can also integrate a client proxy within another service. Here's how you can set it up:
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):
# Initialize the client without blocking the constructor
proxy = pydase.Client(url="ws://<ip_addr>:<service_port>", block_until_connected=False).proxy
# proxy = pydase.Client(url="wss://your-domain.ch", block_until_connected=False).proxy # communicating with ssl-encrypted service
proxy = pydase.Client(
url="ws://<ip_addr>:<service_port>",
block_until_connected=False
).proxy
# For SSL-encrypted services, use the wss protocol
# proxy = pydase.Client(
# url="wss://your-domain.ch",
# block_until_connected=False
# ).proxy
if __name__ == "__main__":
service = MyService()
# Create a server that exposes this service; adjust the web_port as needed
server = pydase.Server(service, web_port=8002). run()
# Create a server that exposes this service
server = pydase.Server(service, web_port=8002).run()
```
In this setup, the `MyService` class has a `proxy` attribute that connects to a `pydase` service located at `<ip_addr>:8001`.
The `block_until_connected=False` argument allows the service to start up even if the initial connection attempt fails.
This configuration is particularly useful in distributed systems where services may start in any order.
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, which is particularly useful in distributed systems where services may initialize in any order.
## Custom `socketio.AsyncClient` Connection Parameters
You can also configure advanced connection options by passing additional arguments to the underlying [`AsyncClient`][socketio.AsyncClient] via `sio_client_kwargs`. This allows you to fine-tune reconnection behaviour, delays, and other settings:
```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

@@ -3,11 +3,18 @@
<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>

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

View File

@@ -1,6 +1,6 @@
import { useCallback, useEffect, useReducer, useState } from "react";
import { Navbar, Form, Offcanvas, Container } from "react-bootstrap";
import { hostname, port, socket } from "./socket";
import { authority, socket, forwardedProto } from "./socket";
import "./App.css";
import {
Notifications,
@@ -68,12 +68,12 @@ const App = () => {
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.href = `${forwardedProto}://${authority}/custom.css`;
link.type = "text/css";
link.rel = "stylesheet";
document.head.appendChild(link);
@@ -83,7 +83,9 @@ const App = () => {
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 });
@@ -91,7 +93,7 @@ const App = () => {
document.title = data.name; // Setting browser tab title
});
fetch(`http://${hostname}:${port}/web-settings`)
fetch(`${forwardedProto}://${authority}/web-settings`, { credentials: "include" })
.then((response) => response.json())
.then((data: Record<string, WebSetting>) => setWebSettings(data));
setConnectionStatus("connected");

View File

@@ -2,14 +2,29 @@ import { io } from "socket.io-client";
import { serializeDict, serializeList } from "./utils/serializationUtils";
import { SerializedObject } from "./types/SerializedObject";
export const hostname =
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 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 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,

View File

@@ -11,6 +11,9 @@ nav:
- 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
- Advanced:
- Deploying behind a Reverse Proxy: user-guide/advanced/Reverse-Proxy.md
- Developer Guide:
- Developer Guide: dev-guide/README.md
- API Reference: dev-guide/api.md
@@ -22,6 +25,7 @@ nav:
- License: about/license.md
theme:
logo: images/logo-colour.png
name: material
features:
- content.code.copy
@@ -54,6 +58,7 @@ plugins:
- 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

37
poetry.lock generated
View File

@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
[[package]]
name = "aiohappyeyeballs"
@@ -149,6 +149,28 @@ files = [
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
]
[[package]]
name = "anyio"
version = "4.6.0"
description = "High level compatibility layer for multiple asynchronous event loop implementations"
optional = false
python-versions = ">=3.9"
files = [
{file = "anyio-4.6.0-py3-none-any.whl", hash = "sha256:c7d2e9d63e31599eeb636c8c5c03a7e108d73b345f064f1c19fdc87b79036a9a"},
{file = "anyio-4.6.0.tar.gz", hash = "sha256:137b4559cbb034c477165047febb6ff83f390fc3b20bf181c1fc0a728cb8beeb"},
]
[package.dependencies]
exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""}
idna = ">=2.8"
sniffio = ">=1.1"
typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""}
[package.extras]
doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.21.0b1)"]
trio = ["trio (>=0.26.1)"]
[[package]]
name = "appdirs"
version = "1.4.4"
@@ -2244,6 +2266,17 @@ files = [
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
[[package]]
name = "sniffio"
version = "1.3.1"
description = "Sniff out which async library your code is running under"
optional = false
python-versions = ">=3.7"
files = [
{file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
]
[[package]]
name = "soupsieve"
version = "2.5"
@@ -2496,4 +2529,4 @@ multidict = ">=4.0"
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
content-hash = "7131eddc2065147a18c145bb6da09492f03eb7fe050e968109cecb6044d17ed6"
content-hash = "011b118225386513fc1c953c02bc1d58e40c198313de2a1f76183dd61ab9eec6"

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "pydase"
version = "0.10.5"
version = "0.10.7"
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>"]
readme = "README.md"
@@ -17,6 +17,7 @@ websocket-client = "^1.7.0"
aiohttp = "^3.9.3"
click = "^8.1.7"
aiohttp-middlewares = "^2.3.0"
anyio = "^4.6.0"
[tool.poetry.group.dev]
optional = true

View File

@@ -2,12 +2,14 @@ import asyncio
import logging
import sys
import threading
from typing import TypedDict, cast
import urllib.parse
from types import TracebackType
from typing import TYPE_CHECKING, Any, TypedDict, cast
import socketio # type: ignore
import pydase.components
from pydase.client.proxy_loader import ProxyClassMixin, ProxyLoader
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
@@ -34,47 +36,6 @@ def asyncio_loop_thread(loop: asyncio.AbstractEventLoop) -> None:
loop.run_forever()
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.
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
) -> None:
super().__init__()
pydase.components.DeviceConnection.__init__(self)
self._initialise(sio_client=sio_client, loop=loop)
class Client:
"""
A client for connecting to a remote pydase service using socket.io. This client
@@ -86,15 +47,35 @@ class Client:
url:
The URL of the pydase Socket.IO server. This should always contain the
protocol and the hostname.
Examples:
- `wss://my-service.example.com` # for secure connections, use wss
- `ws://localhost:8001`
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).
Default is an empty dictionary.
Example:
The following example demonstrates a `Client` instance that connects to another
pydase service, while customising some of the connection settings for the
underlying [`AsyncClient`][socketio.AsyncClient].
```python
pydase.Client(url="ws://localhost:8001", sio_client_kwargs={
"reconnection_attempts": 2,
"reconnection_delay": 2,
"reconnection_delay_max": 8,
})
```
When connecting to a server over a secure connection (i.e., the server is using
SSL/TLS encryption), make sure that the `wss` protocol is used instead of `ws`:
```python
pydase.Client(url="wss://my-service.example.com")
```
"""
def __init__(
@@ -102,11 +83,24 @@ class Client:
*,
url: str,
block_until_connected: bool = True,
sio_client_kwargs: dict[str, Any] = {},
):
# 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._sio = socketio.AsyncClient()
self._sio = socketio.AsyncClient(**sio_client_kwargs)
self._loop = asyncio.new_event_loop()
self.proxy = ProxyClass(sio_client=self._sio, loop=self._loop)
self.proxy = ProxyClass(
sio_client=self._sio, loop=self._loop, reconnect=self.connect
)
"""A proxy object representing the remote service, facilitating interaction as
if it were local."""
self._thread = threading.Thread(
@@ -116,10 +110,14 @@ class Client:
self.connect(block_until_connected=block_until_connected)
def __enter__(self) -> Self:
self.connect(block_until_connected=True)
return self
def __del__(self) -> None:
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:
@@ -139,8 +137,8 @@ class Client:
logger.debug("Connecting to server '%s' ...", self._url)
await self._setup_events()
await self._sio.connect(
self._url,
socketio_path="/ws/socket.io",
self._base_url,
socketio_path=f"{self._path_prefix}/ws/socket.io",
transports=["websocket"],
retry=True,
)
@@ -162,7 +160,13 @@ class Client:
self.proxy, serialized_object=serialized_object
)
serialized_object["type"] = "DeviceConnection"
self.proxy._notify_changed("", loads(serialized_object))
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:

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

@@ -1,7 +1,6 @@
import asyncio
import logging
from collections.abc import Iterable
from copy import copy
from typing import TYPE_CHECKING, Any, cast
import socketio # type: ignore
@@ -202,25 +201,8 @@ class ProxyClassMixin:
def _handle_serialized_method(
self, attr_name: str, serialized_object: SerializedObject
) -> None:
def add_prefix_to_last_path_element(s: str, prefix: str) -> str:
parts = s.split(".")
parts[-1] = f"{prefix}_{parts[-1]}"
return ".".join(parts)
if serialized_object["type"] == "method":
if serialized_object["async"] is True:
start_method = copy(serialized_object)
start_method["full_access_path"] = add_prefix_to_last_path_element(
start_method["full_access_path"], "start"
)
stop_method = copy(serialized_object)
stop_method["full_access_path"] = add_prefix_to_last_path_element(
stop_method["full_access_path"], "stop"
)
self._add_method_proxy(f"start_{attr_name}", start_method)
self._add_method_proxy(f"stop_{attr_name}", stop_method)
else:
self._add_method_proxy(attr_name, serialized_object)
self._add_method_proxy(attr_name, serialized_object)
def _add_method_proxy(
self, attr_name: str, serialized_object: SerializedObject

View File

@@ -1,5 +1,6 @@
import inspect
import logging
from collections.abc import Callable
from enum import Enum
from typing import Any
@@ -68,7 +69,18 @@ class DataService(AbstractDataService):
if not issubclass(
value_class,
(int | float | bool | str | list | dict | Enum | u.Quantity | Observable),
(
int
| float
| bool
| str
| list
| dict
| Enum
| u.Quantity
| Observable
| Callable
),
) and not is_descriptor(__value):
logger.warning(
"Class '%s' does not inherit from DataService. This may lead to"

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

View File

@@ -3,13 +3,20 @@
<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." />
<script type="module" crossorigin src="/assets/index-BjsjosWf.js"></script>
<script type="module" crossorigin src="/assets/index-BqF7l_R8.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D2aktF3W.css">
</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>

View File

@@ -1,3 +1,4 @@
import inspect
import logging
from typing import TYPE_CHECKING
@@ -7,9 +8,11 @@ import aiohttp_middlewares.error
from pydase.data_service.state_manager import StateManager
from pydase.server.web_server.api.v1.endpoints import (
get_value,
trigger_async_method,
trigger_method,
update_value,
)
from pydase.utils.helpers import get_object_attr_from_path
from pydase.utils.serialization.serializer import dump
if TYPE_CHECKING:
@@ -17,54 +20,79 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
API_VERSION = "v1"
STATUS_OK = 200
STATUS_FAILED = 400
async def _get_value(
state_manager: StateManager, request: aiohttp.web.Request
) -> aiohttp.web.Response:
logger.info("Handle api request: %s", request)
access_path = request.rel_url.query["access_path"]
status = STATUS_OK
try:
result = get_value(state_manager, access_path)
except Exception as e:
logger.exception(e)
result = dump(e)
status = STATUS_FAILED
return aiohttp.web.json_response(result, status=status)
async def _update_value(
state_manager: StateManager, request: aiohttp.web.Request
) -> aiohttp.web.Response:
data: UpdateDict = await request.json()
try:
update_value(state_manager, data)
return aiohttp.web.json_response()
except Exception as e:
logger.exception(e)
return aiohttp.web.json_response(dump(e), status=STATUS_FAILED)
async def _trigger_method(
state_manager: StateManager, request: aiohttp.web.Request
) -> aiohttp.web.Response:
data: TriggerMethodDict = await request.json()
method = get_object_attr_from_path(state_manager.service, data["access_path"])
try:
if inspect.iscoroutinefunction(method):
method_return = await trigger_async_method(
state_manager=state_manager, data=data
)
else:
method_return = trigger_method(state_manager=state_manager, data=data)
return aiohttp.web.json_response(method_return)
except Exception as e:
logger.exception(e)
return aiohttp.web.json_response(dump(e), status=STATUS_FAILED)
def create_api_application(state_manager: StateManager) -> aiohttp.web.Application:
api_application = aiohttp.web.Application(
middlewares=(aiohttp_middlewares.error.error_middleware(),)
)
async def _get_value(request: aiohttp.web.Request) -> aiohttp.web.Response:
logger.info("Handle api request: %s", request)
access_path = request.rel_url.query["access_path"]
status = STATUS_OK
try:
result = get_value(state_manager, access_path)
except Exception as e:
logger.exception(e)
result = dump(e)
status = STATUS_FAILED
return aiohttp.web.json_response(result, status=status)
async def _update_value(request: aiohttp.web.Request) -> aiohttp.web.Response:
data: UpdateDict = await request.json()
try:
update_value(state_manager, data)
return aiohttp.web.json_response()
except Exception as e:
logger.exception(e)
return aiohttp.web.json_response(dump(e), status=STATUS_FAILED)
async def _trigger_method(request: aiohttp.web.Request) -> aiohttp.web.Response:
data: TriggerMethodDict = await request.json()
try:
return aiohttp.web.json_response(trigger_method(state_manager, data))
except Exception as e:
logger.exception(e)
return aiohttp.web.json_response(dump(e), status=STATUS_FAILED)
api_application.router.add_get("/get_value", _get_value)
api_application.router.add_put("/update_value", _update_value)
api_application.router.add_put("/trigger_method", _trigger_method)
api_application.router.add_get(
"/get_value",
lambda request: _get_value(state_manager=state_manager, request=request),
)
api_application.router.add_put(
"/update_value",
lambda request: _update_value(state_manager=state_manager, request=request),
)
api_application.router.add_put(
"/trigger_method",
lambda request: _trigger_method(state_manager=state_manager, request=request),
)
return api_application

View File

@@ -1,4 +1,4 @@
from typing import Any
from typing import TYPE_CHECKING, Any
import pydase.utils.serialization.deserializer
import pydase.utils.serialization.serializer
@@ -7,6 +7,9 @@ from pydase.server.web_server.sio_setup import TriggerMethodDict, UpdateDict
from pydase.utils.helpers import get_object_attr_from_path
from pydase.utils.serialization.types import SerializedObject
if TYPE_CHECKING:
from collections.abc import Awaitable, Callable
loads = pydase.utils.serialization.deserializer.loads
Serializer = pydase.utils.serialization.serializer.Serializer
@@ -36,3 +39,19 @@ def trigger_method(state_manager: StateManager, data: TriggerMethodDict) -> Any:
kwargs: dict[str, Any] = loads(serialized_kwargs) if serialized_kwargs else {}
return Serializer.serialize_object(method(*args, **kwargs))
async def trigger_async_method(
state_manager: StateManager, data: TriggerMethodDict
) -> Any:
method: Callable[..., Awaitable[Any]] = get_object_attr_from_path(
state_manager.service, data["access_path"]
)
serialized_args = data.get("args", None)
args = loads(serialized_args) if serialized_args else []
serialized_kwargs = data.get("kwargs", None)
kwargs: dict[str, Any] = loads(serialized_kwargs) if serialized_kwargs else {}
return Serializer.serialize_object(await method(*args, **kwargs))

View File

@@ -1,8 +1,11 @@
import asyncio
import inspect
import logging
import sys
from typing import Any, TypedDict
from pydase.utils.helpers import get_object_attr_from_path
if sys.version_info < (3, 11):
from typing_extensions import NotRequired
else:
@@ -11,11 +14,11 @@ else:
import click
import socketio # type: ignore[import-untyped]
import pydase.server.web_server.api.v1.endpoints
import pydase.utils.serialization.deserializer
import pydase.utils.serialization.serializer
from pydase.data_service.data_service_observer import DataServiceObserver
from pydase.data_service.state_manager import StateManager
from pydase.server.web_server.api.v1 import endpoints
from pydase.utils.logging import SocketIOHandler
from pydase.utils.serialization.serializer import SerializedObject
@@ -155,9 +158,7 @@ def setup_sio_events(sio: socketio.AsyncServer, state_manager: StateManager) ->
@sio.event
async def update_value(sid: str, data: UpdateDict) -> SerializedObject | None:
try:
pydase.server.web_server.api.v1.endpoints.update_value(
state_manager=state_manager, data=data
)
endpoints.update_value(state_manager=state_manager, data=data)
except Exception as e:
logger.exception(e)
return dump(e)
@@ -166,7 +167,7 @@ def setup_sio_events(sio: socketio.AsyncServer, state_manager: StateManager) ->
@sio.event
async def get_value(sid: str, access_path: str) -> SerializedObject:
try:
return pydase.server.web_server.api.v1.endpoints.get_value(
return endpoints.get_value(
state_manager=state_manager, access_path=access_path
)
except Exception as e:
@@ -175,10 +176,14 @@ def setup_sio_events(sio: socketio.AsyncServer, state_manager: StateManager) ->
@sio.event
async def trigger_method(sid: str, data: TriggerMethodDict) -> Any:
method = get_object_attr_from_path(state_manager.service, data["access_path"])
try:
return pydase.server.web_server.api.v1.endpoints.trigger_method(
state_manager=state_manager, data=data
)
if inspect.iscoroutinefunction(method):
return await endpoints.trigger_async_method(
state_manager=state_manager, data=data
)
return endpoints.trigger_method(state_manager=state_manager, data=data)
except Exception as e:
logger.error(e)
return dump(e)

View File

@@ -1,4 +1,5 @@
import asyncio
import html
import json
import logging
from pathlib import Path
@@ -6,6 +7,7 @@ from typing import Any
import aiohttp.web
import aiohttp_middlewares.cors
import anyio
from pydase.config import ServiceConfig, WebServerConfig
from pydase.data_service.data_service_observer import DataServiceObserver
@@ -20,7 +22,6 @@ from pydase.utils.helpers import (
from pydase.utils.serialization.serializer import generate_serialized_data_paths
logger = logging.getLogger(__name__)
API_VERSION = "v1"
class WebServer:
@@ -59,6 +60,8 @@ class WebServer:
css:
Path to a custom CSS file for styling the frontend. If None, no custom
styles are applied. Defaults to None.
favicon_path:
Path to a custom favicon.ico file. Defaults to None.
enable_cors:
Flag to enable or disable CORS policy. When True, CORS is enabled, allowing
cross-origin requests. Defaults to True.
@@ -77,7 +80,9 @@ class WebServer:
data_service_observer: DataServiceObserver,
host: str,
port: int,
*,
css: str | Path | None = None,
favicon_path: str | Path | None = None,
enable_cors: bool = True,
config_dir: Path = ServiceConfig().config_dir,
generate_web_settings: bool = WebServerConfig().generate_web_settings,
@@ -91,6 +96,11 @@ class WebServer:
self.css = css
self.enable_cors = enable_cors
self.frontend_src = frontend_src
self.favicon_path: Path | str = favicon_path # type: ignore
if self.favicon_path is None:
self.favicon_path = self.frontend_src / "favicon.ico"
self._service_config_dir = config_dir
self._generate_web_settings = generate_web_settings
self._loop: asyncio.AbstractEventLoop
@@ -100,7 +110,49 @@ class WebServer:
self._loop = asyncio.get_running_loop()
self._sio = setup_sio_server(self.observer, self.enable_cors, self._loop)
async def index(request: aiohttp.web.Request) -> aiohttp.web.FileResponse:
async def index(
request: aiohttp.web.Request,
) -> aiohttp.web.Response | aiohttp.web.FileResponse:
forwarded_proto = request.headers.get("X-Forwarded-Proto", "http")
escaped_proto = html.escape(forwarded_proto)
# Read the index.html file
index_file_path = self.frontend_src / "index.html"
async with await anyio.open_file(index_file_path) as f:
html_content = await f.read()
# Inject the escaped forwarded protocol into the HTML
modified_html = html_content.replace(
'window.__FORWARDED_PROTO__ = "";',
f'window.__FORWARDED_PROTO__ = "{escaped_proto}";',
)
# Read the X-Forwarded-Prefix header from the request
forwarded_prefix = request.headers.get("X-Forwarded-Prefix", "")
if forwarded_prefix != "":
# Escape the forwarded prefix to prevent XSS
escaped_prefix = html.escape(forwarded_prefix)
# Inject the escaped forwarded prefix into the HTML
modified_html = modified_html.replace(
'window.__FORWARDED_PREFIX__ = "";',
f'window.__FORWARDED_PREFIX__ = "{escaped_prefix}";',
)
modified_html = modified_html.replace(
"/assets/",
f"{escaped_prefix}/assets/",
)
modified_html = modified_html.replace(
"/favicon.ico",
f"{escaped_prefix}/favicon.ico",
)
return aiohttp.web.Response(
text=modified_html, content_type="text/html"
)
return aiohttp.web.FileResponse(self.frontend_src / "index.html")
app = aiohttp.web.Application()
@@ -114,6 +166,7 @@ class WebServer:
# Define routes
self._sio.attach(app, socketio_path="/ws/socket.io")
app.router.add_static("/assets", self.frontend_src / "assets")
app.router.add_get("/favicon.ico", self._favicon_route)
app.router.add_get("/service-properties", self._service_properties_route)
app.router.add_get("/web-settings", self._web_settings_route)
app.router.add_get("/custom.css", self._styles_route)
@@ -131,6 +184,12 @@ class WebServer:
shutdown_timeout=0.1,
)
async def _favicon_route(
self,
request: aiohttp.web.Request,
) -> aiohttp.web.FileResponse:
return aiohttp.web.FileResponse(self.favicon_path)
async def _service_properties_route(
self,
request: aiohttp.web.Request,

View File

@@ -33,7 +33,7 @@ LOGGING_CONFIG = {
"default": {
"formatter": "default",
"class": "logging.StreamHandler",
"stream": "ext://sys.stderr",
"stream": "ext://sys.stdout",
},
},
"loggers": {

View File

@@ -42,6 +42,8 @@ from pydase.utils.serialization.types import (
if TYPE_CHECKING:
from collections.abc import Callable
from pydase.client.proxy_class import ProxyClass
logger = logging.getLogger(__name__)
@@ -74,6 +76,7 @@ class Serializer:
Returns:
Dictionary representation of `obj`.
"""
from pydase.client.client import ProxyClass
result: SerializedObject
@@ -83,6 +86,9 @@ class Serializer:
elif isinstance(obj, datetime):
result = cls._serialize_datetime(obj, access_path=access_path)
elif isinstance(obj, ProxyClass):
result = cls._serialize_proxy_class(obj, access_path=access_path)
elif isinstance(obj, AbstractDataService):
result = cls._serialize_data_service(obj, access_path=access_path)
@@ -322,6 +328,13 @@ class Serializer:
"doc": doc,
}
@classmethod
def _serialize_proxy_class(
cls, obj: ProxyClass, access_path: str = ""
) -> SerializedDataService:
# Get serialization value from the remote service and adapt the full_access_path
return add_prefix_to_full_access_path(obj.serialize(), access_path)
def dump(obj: Any) -> SerializedObject:
"""Serialize `obj` to a
@@ -572,6 +585,62 @@ def generate_serialized_data_paths(
return paths
def add_prefix_to_full_access_path(
serialized_obj: SerializedObject, prefix: str
) -> Any:
"""Recursively adds a specified prefix to all full access paths of the serialized
object.
Args:
serialized_obj:
The serialized object to process.
prefix:
The prefix string to prepend to each full access path.
Returns:
The modified serialized object with the prefix added to all full access paths.
Example:
```python
>>> serialized_obj = {
... "full_access_path": "",
... "value": {
... "item": {
... "full_access_path": "some_item_path",
... "value": 1.0
... }
... }
... }
...
... modified_data = add_prefix_to_full_access_path(serialized_obj, 'prefix')
{"full_access_path": "prefix", "value": {"item": {"full_access_path":
"prefix.some_item_path", "value": 1.0}}}
```
"""
try:
if serialized_obj.get("full_access_path", None) is not None:
serialized_obj["full_access_path"] = (
prefix + "." + serialized_obj["full_access_path"]
if serialized_obj["full_access_path"] != ""
else prefix
)
if isinstance(serialized_obj["value"], list):
for value in serialized_obj["value"]:
add_prefix_to_full_access_path(cast(SerializedObject, value), prefix)
elif isinstance(serialized_obj["value"], dict):
for value in cast(
dict[str, SerializedObject], serialized_obj["value"]
).values():
add_prefix_to_full_access_path(cast(SerializedObject, value), prefix)
except (TypeError, KeyError, AttributeError):
# passed dictionary is not a serialized object
pass
return serialized_obj
def serialized_dict_is_nested_object(serialized_dict: SerializedObject) -> bool:
value = serialized_dict["value"]
# We are excluding Quantity here as the value corresponding to the "value" key is

View File

@@ -41,6 +41,9 @@ def pydase_client() -> Generator[pydase.Client, None, Any]:
def my_method(self, input_str: str) -> str:
return input_str
async def my_async_method(self, input_str: str) -> str:
return input_str
server = pydase.Server(MyService(), web_port=9999)
thread = threading.Thread(target=server.run, daemon=True)
thread.start()
@@ -79,6 +82,14 @@ def test_method_execution(pydase_client: pydase.Client) -> None:
pydase_client.proxy.my_method(kwarg="hello")
def test_async_method_execution(pydase_client: pydase.Client) -> None:
assert pydase_client.proxy.my_async_method("My return string") == "My return string"
assert (
pydase_client.proxy.my_async_method(input_str="My return string")
== "My return string"
)
def test_nested_service(pydase_client: pydase.Client) -> None:
assert pydase_client.proxy.sub_service.name == "SubService"
pydase_client.proxy.sub_service.name = "New name"
@@ -138,3 +149,15 @@ def test_tab_completion(pydase_client: pydase.Client) -> None:
"sub_service",
]
)
def test_context_manager(pydase_client: pydase.Client) -> None:
client = pydase.Client(url="ws://localhost:9999")
assert client.proxy.connected
with client:
client.proxy.my_property = 1337.01
assert client.proxy.my_property == 1337.01
assert not client.proxy.connected

View File

@@ -0,0 +1,107 @@
import threading
from collections.abc import Callable, Generator
from typing import Any
import pydase
import pytest
import socketio.exceptions
@pytest.fixture(scope="function")
def pydase_restartable_server() -> (
Generator[
tuple[
pydase.Server,
threading.Thread,
pydase.DataService,
Callable[
[pydase.Server, threading.Thread, pydase.DataService],
tuple[pydase.Server, threading.Thread],
],
],
None,
Any,
]
):
class MyService(pydase.DataService):
def __init__(self) -> None:
super().__init__()
self._name = "MyService"
self._my_property = 12.1
@property
def my_property(self) -> float:
return self._my_property
@my_property.setter
def my_property(self, value: float) -> None:
self._my_property = value
@property
def name(self) -> str:
return self._name
service_instance = MyService()
server = pydase.Server(service_instance, web_port=9999)
thread = threading.Thread(target=server.run, daemon=True)
thread.start()
def restart(
server: pydase.Server,
thread: threading.Thread,
service_instance: pydase.DataService,
) -> tuple[pydase.Server, threading.Thread]:
server.handle_exit()
thread.join()
server = pydase.Server(service_instance, web_port=9999)
new_thread = threading.Thread(target=server.run, daemon=True)
new_thread.start()
return server, new_thread
yield server, thread, service_instance, restart
server.handle_exit()
thread.join()
def test_reconnection(
pydase_restartable_server: tuple[
pydase.Server,
threading.Thread,
pydase.DataService,
Callable[
[pydase.Server, threading.Thread, pydase.DataService],
tuple[pydase.Server, threading.Thread],
],
],
) -> None:
client = pydase.Client(
url="ws://localhost:9999",
sio_client_kwargs={
"reconnection": False,
},
)
client_2 = pydase.Client(
url="ws://localhost:9999",
sio_client_kwargs={
"reconnection_attempts": 1,
},
)
server, thread, service_instance, restart = pydase_restartable_server
service_instance._name = "New service name"
server, thread = restart(server, thread, service_instance)
with pytest.raises(socketio.exceptions.BadNamespaceError):
client.proxy.name
client_2.proxy.name
client.proxy.reconnect()
client_2.proxy.reconnect()
# the service proxies successfully reconnect and get the new service name
assert client.proxy.name == "New service name"
assert client_2.proxy.name == "New service name"

View File

@@ -6,6 +6,7 @@ from typing import Any
import aiohttp
import pydase
import pytest
from pydase.utils.serialization.deserializer import Deserializer
@pytest.fixture()
@@ -40,7 +41,10 @@ def pydase_server() -> Generator[None, None, None]:
return self._readonly_attr
def my_method(self, input_str: str) -> str:
return input_str
return f"{input_str}: my_method"
async def my_async_method(self, input_str: str) -> str:
return f"{input_str}: my_async_method"
server = pydase.Server(MyService(), web_port=9998)
thread = threading.Thread(target=server.run, daemon=True)
@@ -192,3 +196,57 @@ async def test_update_value(
resp = await session.get(f"/api/v1/get_value?access_path={access_path}")
content = json.loads(await resp.text())
assert content == new_value
@pytest.mark.parametrize(
"access_path, expected, ok",
[
(
"my_method",
"Hello from function: my_method",
True,
),
(
"my_async_method",
"Hello from function: my_async_method",
True,
),
(
"invalid_method",
None,
False,
),
],
)
@pytest.mark.asyncio()
async def test_trigger_method(
access_path: str,
expected: Any,
ok: bool,
pydase_server: pydase.DataService,
) -> None:
async with aiohttp.ClientSession("http://localhost:9998") as session:
resp = await session.put(
"/api/v1/trigger_method",
json={
"access_path": access_path,
"kwargs": {
"full_access_path": "",
"type": "dict",
"value": {
"input_str": {
"docs": None,
"full_access_path": "",
"readonly": False,
"type": "str",
"value": "Hello from function",
},
},
},
},
)
assert resp.ok == ok
if resp.ok:
content = Deserializer.deserialize(json.loads(await resp.text()))
assert content == expected

View File

@@ -12,6 +12,7 @@ from pydase.utils.decorators import frontend
from pydase.utils.serialization.serializer import (
SerializationPathError,
SerializedObject,
add_prefix_to_full_access_path,
dump,
generate_serialized_data_paths,
get_container_item_by_key,
@@ -1070,3 +1071,156 @@ def test_get_data_paths_from_serialized_object(obj: Any, expected: list[str]) ->
)
def test_generate_serialized_data_paths(obj: Any, expected: list[str]) -> None:
assert generate_serialized_data_paths(dump(obj=obj)["value"]) == expected
@pytest.mark.parametrize(
"serialized_obj, prefix, expected",
[
(
{
"full_access_path": "new_attr",
"value": {
"name": {
"full_access_path": "new_attr.name",
"value": "MyService",
}
},
},
"prefix",
{
"full_access_path": "prefix.new_attr",
"value": {
"name": {
"full_access_path": "prefix.new_attr.name",
"value": "MyService",
}
},
},
),
(
{
"full_access_path": "new_attr",
"value": [
{
"full_access_path": "new_attr[0]",
"value": 1.0,
}
],
},
"prefix",
{
"full_access_path": "prefix.new_attr",
"value": [
{
"full_access_path": "prefix.new_attr[0]",
"value": 1.0,
}
],
},
),
(
{
"full_access_path": "new_attr",
"value": {
"key": {
"full_access_path": 'new_attr["key"]',
"value": 1.0,
}
},
},
"prefix",
{
"full_access_path": "prefix.new_attr",
"value": {
"key": {
"full_access_path": 'prefix.new_attr["key"]',
"value": 1.0,
}
},
},
),
(
{
"full_access_path": "new_attr",
"value": {"magnitude": 10, "unit": "meter"},
},
"prefix",
{
"full_access_path": "prefix.new_attr",
"value": {"magnitude": 10, "unit": "meter"},
},
),
(
{
"full_access_path": "quantity_list",
"value": [
{
"full_access_path": "quantity_list[0]",
"value": {"magnitude": 1.0, "unit": "A"},
}
],
},
"prefix",
{
"full_access_path": "prefix.quantity_list",
"value": [
{
"full_access_path": "prefix.quantity_list[0]",
"value": {"magnitude": 1.0, "unit": "A"},
}
],
},
),
(
{
"full_access_path": "",
"value": {
"dict_attr": {
"type": "dict",
"full_access_path": "dict_attr",
"value": {
"foo": {
"full_access_path": 'dict_attr["foo"]',
"type": "dict",
"value": {
"some_int": {
"full_access_path": 'dict_attr["foo"].some_int',
"type": "int",
"value": 1,
},
},
},
},
}
},
},
"prefix",
{
"full_access_path": "prefix",
"value": {
"dict_attr": {
"type": "dict",
"full_access_path": "prefix.dict_attr",
"value": {
"foo": {
"full_access_path": 'prefix.dict_attr["foo"]',
"type": "dict",
"value": {
"some_int": {
"full_access_path": 'prefix.dict_attr["foo"].some_int',
"type": "int",
"value": 1,
},
},
},
},
}
},
},
),
],
)
def test_add_prefix_to_full_access_path(
serialized_obj: SerializedObject, prefix: str, expected: SerializedObject
) -> None:
assert add_prefix_to_full_access_path(serialized_obj, prefix) == expected