Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36ab8ab68b | ||
|
|
27a832bbd1 | ||
|
|
18df9e288a | ||
|
|
7b786be892 | ||
|
|
374a930745 | ||
|
|
6d12e5c939 | ||
|
|
bcf37067ad | ||
|
|
a1ac0c2f88 | ||
|
|
cfe190ca5b | ||
|
|
c002d04328 | ||
|
|
0d1df4f9e5 | ||
|
|
59cc834a81 | ||
|
|
dc54d9faef | ||
|
|
89bf5cb3f1 | ||
|
|
c72ea9eb20 | ||
|
|
897387e39e | ||
|
|
4454a10f78 | ||
|
|
c9814f7cdc | ||
|
|
187d8bcf28 | ||
|
|
204d426663 | ||
|
|
29e9afa47e | ||
|
|
a6943c027f | ||
|
|
70e4fa73e1 | ||
|
|
579fa4715b | ||
|
|
0100bab04f | ||
|
|
bdf97fa181 | ||
|
|
eb1587fa7d | ||
|
|
5827cda316 | ||
|
|
0e9ec7a66a | ||
|
|
155957f0c5 | ||
|
|
a8b46f191b | ||
|
|
3862ce3405 | ||
|
|
5403b51a5b | ||
|
|
1270400e95 | ||
|
|
3d2bb1c528 | ||
|
|
7c68f02cfd | ||
|
|
ccd6447869 | ||
|
|
056c02c5a5 | ||
|
|
52a798e4c8 | ||
|
|
fdfdef5837 | ||
|
|
ff301f225c | ||
|
|
87f720f567 | ||
|
|
fecb46c02c | ||
|
|
cce2399b07 | ||
|
|
df1db99ec0 | ||
|
|
5f2619500b | ||
|
|
843675fa1e | ||
|
|
2aa370c8ac | ||
|
|
c25ff4a3aa | ||
|
|
5e32a70c3e | ||
|
|
3f6692a1cd |
44
README.md
@@ -1,5 +1,5 @@
|
||||
<!--introduction-start-->
|
||||
# pydase <!-- omit from toc -->
|
||||

|
||||
|
||||
[](https://pypi.org/project/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 server’s 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
|
After Width: | Height: | Size: 27 KiB |
11
docs/images/logo-bw.svg
Normal 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
|
After Width: | Height: | Size: 77 KiB |
153
docs/images/logo-colour.svg
Normal 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 |
BIN
docs/images/logo-with-text.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
145
docs/images/logo-with-text.svg
Normal 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 |
@@ -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
|
||||
|
||||
211
docs/user-guide/Configuration.md
Normal 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, we’ll 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 server’s 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 service’s 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.
|
||||
59
docs/user-guide/advanced/Reverse-Proxy.md
Normal 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 don’t 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 don’t need to make any changes to your application code or frontend resources.
|
||||
@@ -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.
|
||||
|
||||
Here’s 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
|
||||
|
||||
|
||||
@@ -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
|
After Width: | Height: | Size: 77 KiB |
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
37
poetry.lock
generated
@@ -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"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "pydase"
|
||||
version = "0.10.6"
|
||||
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
|
||||
|
||||
@@ -2,6 +2,8 @@ import asyncio
|
||||
import logging
|
||||
import sys
|
||||
import threading
|
||||
import urllib.parse
|
||||
from types import TracebackType
|
||||
from typing import TYPE_CHECKING, Any, TypedDict, cast
|
||||
|
||||
import socketio # type: ignore
|
||||
@@ -83,6 +85,16 @@ class Client:
|
||||
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(**sio_client_kwargs)
|
||||
self._loop = asyncio.new_event_loop()
|
||||
@@ -98,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:
|
||||
@@ -121,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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
BIN
src/pydase/frontend/favicon.ico
Normal file
|
After Width: | Height: | Size: 77 KiB |
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -592,7 +592,7 @@ def add_prefix_to_full_access_path(
|
||||
object.
|
||||
|
||||
Args:
|
||||
data:
|
||||
serialized_obj:
|
||||
The serialized object to process.
|
||||
prefix:
|
||||
The prefix string to prepend to each full access path.
|
||||
@@ -602,7 +602,7 @@ def add_prefix_to_full_access_path(
|
||||
|
||||
Example:
|
||||
```python
|
||||
>>> data = {
|
||||
>>> serialized_obj = {
|
||||
... "full_access_path": "",
|
||||
... "value": {
|
||||
... "item": {
|
||||
@@ -612,7 +612,7 @@ def add_prefix_to_full_access_path(
|
||||
... }
|
||||
... }
|
||||
...
|
||||
... modified_data = add_prefix_to_full_access_path(data, 'prefix')
|
||||
... 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}}}
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||