further prototyping, added first DB interactions

This commit is contained in:
Florian Förster 2026-04-23 15:57:39 +02:00
parent e4ebb1ee7f
commit c5aadd502d
12 changed files with 1196 additions and 283 deletions

283
pdm.lock generated
View File

@ -5,7 +5,7 @@
groups = ["default", "dev", "lint", "nb", "tests"] groups = ["default", "dev", "lint", "nb", "tests"]
strategy = ["inherit_metadata"] strategy = ["inherit_metadata"]
lock_version = "4.5.0" lock_version = "4.5.0"
content_hash = "sha256:8bbf5d5c10eaccb24b54658cd427a2e389ec46a845545ccab37e930ea0348598" content_hash = "sha256:eb4aaeb1bc7efec0c69be5e4cde53b2d92fdacfcc170db0204ef694828f9e99d"
[[metadata.targets]] [[metadata.targets]]
requires_python = ">=3.11,<3.14" requires_python = ">=3.11,<3.14"
@ -962,6 +962,20 @@ files = [
{file = "docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968"}, {file = "docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968"},
] ]
[[package]]
name = "dopt-basics"
version = "0.2.4"
requires_python = ">=3.11"
summary = "basic cross-project tools for Python-based d-opt projects"
groups = ["default"]
dependencies = [
"tzdata>=2025.1",
]
files = [
{file = "dopt_basics-0.2.4-py3-none-any.whl", hash = "sha256:b7d05b80dde1f856b352580aeac500fc7505e4513ed162791d8735cdc182ebc1"},
{file = "dopt_basics-0.2.4.tar.gz", hash = "sha256:c21fbe183bec5eab4cfd1404e10baca670035801596960822d0019e6e885983f"},
]
[[package]] [[package]]
name = "execnet" name = "execnet"
version = "2.1.2" version = "2.1.2"
@ -1145,6 +1159,47 @@ files = [
{file = "frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad"}, {file = "frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad"},
] ]
[[package]]
name = "greenlet"
version = "3.4.0"
requires_python = ">=3.10"
summary = "Lightweight in-process concurrent programming"
groups = ["default"]
marker = "platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\""
files = [
{file = "greenlet-3.4.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:805bebb4945094acbab757d34d6e1098be6de8966009ab9ca54f06ff492def58"},
{file = "greenlet-3.4.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:439fc2f12b9b512d9dfa681c5afe5f6b3232c708d13e6f02c845e0d9f4c2d8c6"},
{file = "greenlet-3.4.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a70ed1cb0295bee1df57b63bf7f46b4e56a5c93709eea769c1fec1bb23a95875"},
{file = "greenlet-3.4.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c5696c42e6bb5cfb7c6ff4453789081c66b9b91f061e5e9367fa15792644e76"},
{file = "greenlet-3.4.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c660bce1940a1acae5f51f0a064f1bc785d07ea16efcb4bc708090afc4d69e83"},
{file = "greenlet-3.4.0-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:89995ce5ddcd2896d89615116dd39b9703bfa0c07b583b85b89bf1b5d6eddf81"},
{file = "greenlet-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ee407d4d1ca9dc632265aee1c8732c4a2d60adff848057cdebfe5fe94eb2c8a2"},
{file = "greenlet-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:956215d5e355fffa7c021d168728321fd4d31fd730ac609b1653b450f6a4bc71"},
{file = "greenlet-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:5cb614ace7c27571270354e9c9f696554d073f8aa9319079dcba466bbdead711"},
{file = "greenlet-3.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:04403ac74fe295a361f650818de93be11b5038a78f49ccfb64d3b1be8fbf1267"},
{file = "greenlet-3.4.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:1a54a921561dd9518d31d2d3db4d7f80e589083063ab4d3e2e950756ef809e1a"},
{file = "greenlet-3.4.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16dec271460a9a2b154e3b1c2fa1050ce6280878430320e85e08c166772e3f97"},
{file = "greenlet-3.4.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:90036ce224ed6fe75508c1907a77e4540176dcf0744473627785dd519c6f9996"},
{file = "greenlet-3.4.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6f0def07ec9a71d72315cf26c061aceee53b306c36ed38c35caba952ea1b319d"},
{file = "greenlet-3.4.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a1c4f6b453006efb8310affb2d132832e9bbb4fc01ce6df6b70d810d38f1f6dc"},
{file = "greenlet-3.4.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:0e1254cf0cbaa17b04320c3a78575f29f3c161ef38f59c977108f19ffddaf077"},
{file = "greenlet-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b2d9a138ffa0e306d0e2b72976d2fb10b97e690d40ab36a472acaab0838e2de"},
{file = "greenlet-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8424683caf46eb0eb6f626cb95e008e8cc30d0cb675bdfa48200925c79b38a08"},
{file = "greenlet-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0a53fb071531d003b075c444014ff8f8b1a9898d36bb88abd9ac7b3524648a2"},
{file = "greenlet-3.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:f38b81880ba28f232f1f675893a39cf7b6db25b31cc0a09bb50787ecf957e85e"},
{file = "greenlet-3.4.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:43748988b097f9c6f09364f260741aa73c80747f63389824435c7a50bfdfd5c1"},
{file = "greenlet-3.4.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5566e4e2cd7a880e8c27618e3eab20f3494452d12fd5129edef7b2f7aa9a36d1"},
{file = "greenlet-3.4.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1054c5a3c78e2ab599d452f23f7adafef55062a783a8e241d24f3b633ba6ff82"},
{file = "greenlet-3.4.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:98eedd1803353daf1cd9ef23eef23eda5a4d22f99b1f998d273a8b78b70dd47f"},
{file = "greenlet-3.4.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f82cb6cddc27dd81c96b1506f4aa7def15070c3b2a67d4e46fd19016aacce6cf"},
{file = "greenlet-3.4.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:b7857e2202aae67bc5725e0c1f6403c20a8ff46094ece015e7d474f5f7020b55"},
{file = "greenlet-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:227a46251ecba4ff46ae742bc5ce95c91d5aceb4b02f885487aff269c127a729"},
{file = "greenlet-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5b99e87be7eba788dd5b75ba1cde5639edffdec5f91fe0d734a249535ec3408c"},
{file = "greenlet-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:849f8bc17acd6295fcb5de8e46d55cc0e52381c56eaf50a2afd258e97bc65940"},
{file = "greenlet-3.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:9390ad88b652b1903814eaabd629ca184db15e0eeb6fe8a390bbf8b9106ae15a"},
{file = "greenlet-3.4.0.tar.gz", hash = "sha256:f50a96b64dafd6169e595a5c56c9146ef80333e67d4476a65a9c55f400fc22ff"},
]
[[package]] [[package]]
name = "h11" name = "h11"
version = "0.16.0" version = "0.16.0"
@ -2397,6 +2452,38 @@ files = [
{file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"},
] ]
[[package]]
name = "polars"
version = "1.40.1"
requires_python = ">=3.10"
summary = "Blazingly fast DataFrame library"
groups = ["default"]
dependencies = [
"polars-runtime-32==1.40.1",
]
files = [
{file = "polars-1.40.1-py3-none-any.whl", hash = "sha256:c0f861219d1319cdea45c4ce4d30355a47176b8f98dcedf95ea8269f131b8abd"},
{file = "polars-1.40.1.tar.gz", hash = "sha256:ab2694134b137596b5a59bfd7b4c54ebbc9b59f9403127f18e32d363777552e8"},
]
[[package]]
name = "polars-runtime-32"
version = "1.40.1"
requires_python = ">=3.10"
summary = "Blazingly fast DataFrame library"
groups = ["default"]
files = [
{file = "polars_runtime_32-1.40.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:b748ef652270cc49e9e69f99a035e0eb4d5f856d42bcd6ac4d9d80a40142aa1e"},
{file = "polars_runtime_32-1.40.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:d249b3743e05986060cec0a7aaa542d020df6c6b876e556023a310efd581f9be"},
{file = "polars_runtime_32-1.40.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5987b30e7aa1059d069498496e8dda35afd592b0ac3d46ed87e3ff8df1ad652c"},
{file = "polars_runtime_32-1.40.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d7f42a8b3f16fc66002cc0f6516f7dd7653396886ae0ed362ab95c0b3408b59"},
{file = "polars_runtime_32-1.40.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e5f7becc237a7ec9d9a10878dc8e54b73bbf4e2d94a2991c37d7a0b38590d8f9"},
{file = "polars_runtime_32-1.40.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:992d14cf191dde043d36fbdbc98a65e43fbc7e9a5024cecd45f838ac4988c1ee"},
{file = "polars_runtime_32-1.40.1-cp310-abi3-win_amd64.whl", hash = "sha256:f78bb2abd00101cbb23cc0cb068f7e36e081057a15d2ec2dde3dda280709f030"},
{file = "polars_runtime_32-1.40.1-cp310-abi3-win_arm64.whl", hash = "sha256:b5cbfaf6b085b420b4bfcbe24e8f665076d1cccfdb80c0484c02a023ce205537"},
{file = "polars_runtime_32-1.40.1.tar.gz", hash = "sha256:37f3065615d1bf90d03b5326222df4c5c1f8a5d33e50470aa588e3465e6eb814"},
]
[[package]] [[package]]
name = "prometheus-client" name = "prometheus-client"
version = "0.25.0" version = "0.25.0"
@ -2597,24 +2684,24 @@ files = [
[[package]] [[package]]
name = "pydantic" name = "pydantic"
version = "2.13.0" version = "2.13.3"
requires_python = ">=3.9" requires_python = ">=3.9"
summary = "Data validation using Python type hints" summary = "Data validation using Python type hints"
groups = ["default", "dev"] groups = ["default", "dev"]
dependencies = [ dependencies = [
"annotated-types>=0.6.0", "annotated-types>=0.6.0",
"pydantic-core==2.46.0", "pydantic-core==2.46.3",
"typing-extensions>=4.14.1", "typing-extensions>=4.14.1",
"typing-inspection>=0.4.2", "typing-inspection>=0.4.2",
] ]
files = [ files = [
{file = "pydantic-2.13.0-py3-none-any.whl", hash = "sha256:ab0078b90da5f3e2fd2e71e3d9b457ddcb35d0350854fbda93b451e28d56baaf"}, {file = "pydantic-2.13.3-py3-none-any.whl", hash = "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927"},
{file = "pydantic-2.13.0.tar.gz", hash = "sha256:b89b575b6e670ebf6e7448c01b41b244f471edd276cd0b6fe02e7e7aca320070"}, {file = "pydantic-2.13.3.tar.gz", hash = "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d"},
] ]
[[package]] [[package]]
name = "pydantic-core" name = "pydantic-core"
version = "2.46.0" version = "2.46.3"
requires_python = ">=3.9" requires_python = ">=3.9"
summary = "Core functionality for Pydantic validation and serialization" summary = "Core functionality for Pydantic validation and serialization"
groups = ["default", "dev"] groups = ["default", "dev"]
@ -2622,90 +2709,60 @@ dependencies = [
"typing-extensions>=4.14.1", "typing-extensions>=4.14.1",
] ]
files = [ files = [
{file = "pydantic_core-2.46.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0027da787ae711f7fbd5a76cb0bb8df526acba6c10c1e44581de1b838db10b7b"}, {file = "pydantic_core-2.46.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ab124d49d0459b2373ecf54118a45c28a1e6d4192a533fbc915e70f556feb8e5"},
{file = "pydantic_core-2.46.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:63e288fc18d7eaeef5f16c73e65c4fd0ad95b25e7e21d8a5da144977b35eb997"}, {file = "pydantic_core-2.46.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cca67d52a5c7a16aed2b3999e719c4bcf644074eac304a5d3d62dd70ae7d4b2c"},
{file = "pydantic_core-2.46.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:080a3bdc6807089a1fe1fbc076519cea287f1a964725731d80b49d8ecffaa217"}, {file = "pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c024e08c0ba23e6fd68c771a521e9d6a792f2ebb0fa734296b36394dc30390e"},
{file = "pydantic_core-2.46.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c065f1c3e54c3e79d909927a8cb48ccbc17b68733552161eba3e0628c38e5d19"}, {file = "pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6645ce7eec4928e29a1e3b3d5c946621d105d3e79f0c9cddf07c2a9770949287"},
{file = "pydantic_core-2.46.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7e2db58ab46cfe602d4255381cce515585998c3b6699d5b1f909f519bc44a5aa"}, {file = "pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a712c7118e6c5ea96562f7b488435172abb94a3c53c22c9efc1412264a45cbbe"},
{file = "pydantic_core-2.46.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c660974890ec1e4c65cff93f5670a5f451039f65463e9f9c03ad49746b49fc78"}, {file = "pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69a868ef3ff206343579021c40faf3b1edc64b1cc508ff243a28b0a514ccb050"},
{file = "pydantic_core-2.46.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3be91482a8db77377c902cca87697388a4fb68addeb3e943ac74f425201a099"}, {file = "pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc7e8c32db809aa0f6ea1d6869ebc8518a65d5150fdfad8bcae6a49ae32a22e2"},
{file = "pydantic_core-2.46.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:1c72de82115233112d70d07f26a48cf6996eb86f7e143423ec1a182148455a9d"}, {file = "pydantic_core-2.46.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3481bd1341dc85779ee506bc8e1196a277ace359d89d28588a9468c3ecbe63fa"},
{file = "pydantic_core-2.46.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7904e58768cd79304b992868d7710bfc85dc6c7ed6163f0f68dbc1dcd72dc231"}, {file = "pydantic_core-2.46.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8690eba565c6d68ffd3a8655525cbdd5246510b44a637ee2c6c03a7ebfe64d3c"},
{file = "pydantic_core-2.46.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1af8d88718005f57bb4768f92f4ff16bf31a747d39dfc919b22211b84e72c053"}, {file = "pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4de88889d7e88d50d40ee5b39d5dac0bcaef9ba91f7e536ac064e6b2834ecccf"},
{file = "pydantic_core-2.46.0-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:a5b891301b02770a5852253f4b97f8bd192e5710067bc129e20d43db5403ede2"}, {file = "pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:e480080975c1ef7f780b8f99ed72337e7cc5efea2e518a20a692e8e7b278eb8b"},
{file = "pydantic_core-2.46.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:48b671fe59031fd9754c7384ac05b3ed47a0cccb7d4db0ec56121f0e6a541b90"}, {file = "pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:de3a5c376f8cd94da9a1b8fd3dd1c16c7a7b216ed31dc8ce9fd7a22bf13b836e"},
{file = "pydantic_core-2.46.0-cp311-cp311-win32.whl", hash = "sha256:0a52b7262b6cc67033823e9549a41bb77580ac299dc964baae4e9c182b2e335c"}, {file = "pydantic_core-2.46.3-cp311-cp311-win32.whl", hash = "sha256:fc331a5314ffddd5385b9ee9d0d2fee0b13c27e0e02dad71b1ae5d6561f51eeb"},
{file = "pydantic_core-2.46.0-cp311-cp311-win_amd64.whl", hash = "sha256:4103fea1beeef6b3a9fed8515f27d4fa30c929a1973655adf8f454dc49ee0662"}, {file = "pydantic_core-2.46.3-cp311-cp311-win_amd64.whl", hash = "sha256:b5b9c6cf08a8a5e502698f5e153056d12c34b8fb30317e0c5fd06f45162a6346"},
{file = "pydantic_core-2.46.0-cp311-cp311-win_arm64.whl", hash = "sha256:3137cd88938adb8e567c5e938e486adc7e518ffc96b4ae1ec268e6a4275704d7"}, {file = "pydantic_core-2.46.3-cp311-cp311-win_arm64.whl", hash = "sha256:5dfd51cf457482f04ec49491811a2b8fd5b843b64b11eecd2d7a1ee596ea78a6"},
{file = "pydantic_core-2.46.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:66ccedb02c934622612448489824955838a221b3a35875458970521ef17b2f9c"}, {file = "pydantic_core-2.46.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b11b59b3eee90a80a36701ddb4576d9ae31f93f05cb9e277ceaa09e6bf074a67"},
{file = "pydantic_core-2.46.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a44f27f4d2788ef9876ec47a43739b118c5904d74f418f53398f6ced3bbcacf2"}, {file = "pydantic_core-2.46.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af8653713055ea18a3abc1537fe2ebc42f5b0bbb768d1eb79fd74eb47c0ac089"},
{file = "pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f26a1032bcce6ca4b4670eb3f7d8195bd0a8b8f255f1307823e217ca3cfa7c27"}, {file = "pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75a519dab6d63c514f3a81053e5266c549679e4aa88f6ec57f2b7b854aceb1b0"},
{file = "pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b8d1412f725060527e56675904b17a2d421dddcf861eecf7c75b9dda47921a4"}, {file = "pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6cd87cb1575b1ad05ba98894c5b5c96411ef678fa2f6ed2576607095b8d9789"},
{file = "pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc3d1569edd859cabaa476cabce9eecd05049a7966af7b4a33b541bfd4ca1104"}, {file = "pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f80a55484b8d843c8ada81ebf70a682f3f00a3d40e378c06cf17ecb44d280d7d"},
{file = "pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:38108976f2d8afaa8f5067fd1390a8c9f5cc580175407cda636e76bc76e88054"}, {file = "pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3861f1731b90c50a3266316b9044f5c9b405eecb8e299b0a7120596334e4fe9c"},
{file = "pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5a06d8ed01dad5575056b5187e5959b336793c6047920a3441ee5b03533836"}, {file = "pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb528e295ed31570ac3dcc9bfdd6e0150bc11ce6168ac87a8082055cf1a67395"},
{file = "pydantic_core-2.46.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:04017ace142da9ce27cafd423a480872571b5c7e80382aec22f7d715ca8eb870"}, {file = "pydantic_core-2.46.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:367508faa4973b992b271ba1494acaab36eb7e8739d1e47be5035fb1ea225396"},
{file = "pydantic_core-2.46.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2629ad992ed1b1c012e6067f5ffafd3336fcb9b54569449fabb85621f1444ed3"}, {file = "pydantic_core-2.46.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ad3c826fe523e4becf4fe39baa44286cff85ef137c729a2c5e269afbfd0905d"},
{file = "pydantic_core-2.46.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3068b1e7bd986aebc88f6859f8353e72072538dcf92a7fb9cf511a0f61c5e729"}, {file = "pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ec638c5d194ef8af27db69f16c954a09797c0dc25015ad6123eb2c73a4d271ca"},
{file = "pydantic_core-2.46.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:1e366916ff69ff700aa9326601634e688581bc24c5b6b4f8738d809ec7d72611"}, {file = "pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:28ed528c45446062ee66edb1d33df5d88828ae167de76e773a3c7f64bd14e976"},
{file = "pydantic_core-2.46.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:485a23e8f4618a1b8e23ac744180acde283fffe617f96923d25507d5cade62ec"}, {file = "pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aed19d0c783886d5bd86d80ae5030006b45e28464218747dcf83dabfdd092c7b"},
{file = "pydantic_core-2.46.0-cp312-cp312-win32.whl", hash = "sha256:520940e1b702fe3b33525d0351777f25e9924f1818ca7956447dabacf2d339fd"}, {file = "pydantic_core-2.46.3-cp312-cp312-win32.whl", hash = "sha256:06d5d8820cbbdb4147578c1fe7ffcd5b83f34508cb9f9ab76e807be7db6ff0a4"},
{file = "pydantic_core-2.46.0-cp312-cp312-win_amd64.whl", hash = "sha256:90d2048e0339fa365e5a66aefe760ddd3b3d0a45501e088bc5bc7f4ed9ff9571"}, {file = "pydantic_core-2.46.3-cp312-cp312-win_amd64.whl", hash = "sha256:c3212fda0ee959c1dd04c60b601ec31097aaa893573a3a1abd0a47bcac2968c1"},
{file = "pydantic_core-2.46.0-cp312-cp312-win_arm64.whl", hash = "sha256:a70247649b7dffe36648e8f34be5ce8c5fa0a27ff07b071ea780c20a738c05ce"}, {file = "pydantic_core-2.46.3-cp312-cp312-win_arm64.whl", hash = "sha256:f1f8338dd7a7f31761f1f1a3c47503a9a3b34eea3c8b01fa6ee96408affb5e72"},
{file = "pydantic_core-2.46.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:a05900c37264c070c683c650cbca8f83d7cbb549719e645fcd81a24592eac788"}, {file = "pydantic_core-2.46.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:12bc98de041458b80c86c56b24df1d23832f3e166cbaff011f25d187f5c62c37"},
{file = "pydantic_core-2.46.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8de8e482fd4f1e3f36c50c6aac46d044462615d8f12cfafc6bebeaa0909eea22"}, {file = "pydantic_core-2.46.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:85348b8f89d2c3508b65b16c3c33a4da22b8215138d8b996912bb1532868885f"},
{file = "pydantic_core-2.46.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c525ecf8a4cdf198327b65030a7d081867ad8e60acb01a7214fff95cf9832d47"}, {file = "pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1105677a6df914b1fb71a81b96c8cce7726857e1717d86001f29be06a25ee6f8"},
{file = "pydantic_core-2.46.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f14581aeb12e61542ce73b9bfef2bca5439d65d9ab3efe1a4d8e346b61838f9b"}, {file = "pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87082cd65669a33adeba5470769e9704c7cf026cc30afb9cc77fd865578ebaad"},
{file = "pydantic_core-2.46.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c108067f2f7e190d0dbd81247d789ec41f9ea50ccd9265a3a46710796ac60530"}, {file = "pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e5f66e12c4f5212d08522963380eaaeac5ebd795826cfd19b2dfb0c7a52b9c"},
{file = "pydantic_core-2.46.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ac10967e9a7bb1b96697374513f9a1a90a59e2fb41566b5e00ee45392beac59"}, {file = "pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6cdf19bf84128d5e7c37e8a73a0c5c10d51103a650ac585d42dd6ae233f2b7f"},
{file = "pydantic_core-2.46.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7897078fe8a13b73623c0955dfb2b3d2c9acb7177aac25144758c9e5a5265aaa"}, {file = "pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031bb17f4885a43773c8c763089499f242aee2ea85cf17154168775dccdecf35"},
{file = "pydantic_core-2.46.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:e69ce405510a419a082a78faed65bb4249cfb51232293cc675645c12f7379bf7"}, {file = "pydantic_core-2.46.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:bcf2a8b2982a6673693eae7348ef3d8cf3979c1d63b54fca7c397a635cc68687"},
{file = "pydantic_core-2.46.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fd28d13eea0d8cf351dc1fe274b5070cc8e1cca2644381dee5f99de629e77cf3"}, {file = "pydantic_core-2.46.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28e8cf2f52d72ced402a137145923a762cbb5081e48b34312f7a0c8f55928ec3"},
{file = "pydantic_core-2.46.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:ee1547a6b8243e73dd10f585555e5a263395e55ce6dea618a078570a1e889aef"}, {file = "pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:17eaface65d9fc5abb940003020309c1bf7a211f5f608d7870297c367e6f9022"},
{file = "pydantic_core-2.46.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:c3dc68dcf62db22a18ddfc3ad4960038f72b75908edc48ae014d7ac8b391d57a"}, {file = "pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:93fd339f23408a07e98950a89644f92c54d8729719a40b30c0a30bb9ebc55d23"},
{file = "pydantic_core-2.46.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:004a2081c881abfcc6854a4623da6a09090a0d7c1398a6ae7133ca1256cee70b"}, {file = "pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:23cbdb3aaa74dfe0837975dbf69b469753bbde8eacace524519ffdb6b6e89eb7"},
{file = "pydantic_core-2.46.0-cp313-cp313-win32.whl", hash = "sha256:59d24ec8d5eaabad93097525a69d0f00f2667cb353eb6cda578b1cfff203ceef"}, {file = "pydantic_core-2.46.3-cp313-cp313-win32.whl", hash = "sha256:610eda2e3838f401105e6326ca304f5da1e15393ae25dacae5c5c63f2c275b13"},
{file = "pydantic_core-2.46.0-cp313-cp313-win_amd64.whl", hash = "sha256:71186dad5ac325c64d68fe0e654e15fd79802e7cc42bc6f0ff822d5ad8b1ab25"}, {file = "pydantic_core-2.46.3-cp313-cp313-win_amd64.whl", hash = "sha256:68cc7866ed863db34351294187f9b729964c371ba33e31c26f478471c52e1ed0"},
{file = "pydantic_core-2.46.0-cp313-cp313-win_arm64.whl", hash = "sha256:8e4503f3213f723842c9a3b53955c88a9cfbd0b288cbd1c1ae933aebeec4a1b4"}, {file = "pydantic_core-2.46.3-cp313-cp313-win_arm64.whl", hash = "sha256:f64b5537ac62b231572879cd08ec05600308636a5d63bcbdb15063a466977bec"},
{file = "pydantic_core-2.46.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:4fc801c290342350ffc82d77872054a934b2e24163727263362170c1db5416ca"}, {file = "pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:13afdd885f3d71280cf286b13b310ee0f7ccfefd1dbbb661514a474b726e2f25"},
{file = "pydantic_core-2.46.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0a36f2cc88170cc177930afcc633a8c15907ea68b59ac16bd180c2999d714940"}, {file = "pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f91c0aff3e3ee0928edd1232c57f643a7a003e6edf1860bc3afcdc749cb513f3"},
{file = "pydantic_core-2.46.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a3912e0c568a1f99d4d6d3e41def40179d61424c0ca1c8c87c4877d7f6fd7fb"}, {file = "pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6529d1d128321a58d30afcc97b49e98836542f68dd41b33c2e972bb9e5290536"},
{file = "pydantic_core-2.46.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3534c3415ed1a19ab23096b628916a827f7858ec8db49ad5d7d1e44dc13c0d7b"}, {file = "pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:975c267cff4f7e7272eacbe50f6cc03ca9a3da4c4fbd66fffd89c94c1e311aa1"},
{file = "pydantic_core-2.46.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21067396fc285609323a4db2f63a87570044abe0acddfcca8b135fc7948e3db7"}, {file = "pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2b8e4f2bbdf71415c544b4b1138b8060db7b6611bc927e8064c769f64bed651c"},
{file = "pydantic_core-2.46.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2afd85b7be186e2fe7cdbb09a3d964bcc2042f65bbcc64ad800b3c7915032655"}, {file = "pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e61ea8e9fff9606d09178f577ff8ccdd7206ff73d6552bcec18e1033c4254b85"},
{file = "pydantic_core-2.46.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67e2c2e171b78db8154da602de72ffdc473c6ee51de8a9d80c0f1cd4051abfc7"}, {file = "pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b504bda01bafc69b6d3c7a0c7f039dcf60f47fab70e06fe23f57b5c75bdc82b8"},
{file = "pydantic_core-2.46.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c16ae1f3170267b1a37e16dba5c297bdf60c8b5657b147909ca8774ce7366644"}, {file = "pydantic_core-2.46.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b00b76f7142fc60c762ce579bd29c8fa44aaa56592dd3c54fab3928d0d4ca6ff"},
{file = "pydantic_core-2.46.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:133b69e1c1ba34d3702eed73f19f7f966928f9aa16663b55c2ebce0893cca42e"}, {file = "pydantic_core-2.46.3.tar.gz", hash = "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c"},
{file = "pydantic_core-2.46.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:15ed8e5bde505133d96b41702f31f06829c46b05488211a5b1c7877e11de5eb5"},
{file = "pydantic_core-2.46.0-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:8cfc29a1c66a7f0fcb36262e92f353dd0b9c4061d558fceb022e698a801cb8ae"},
{file = "pydantic_core-2.46.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e1155708540f13845bf68d5ac511a55c76cfe2e057ed12b4bf3adac1581fc5c2"},
{file = "pydantic_core-2.46.0-cp314-cp314-win32.whl", hash = "sha256:de5635a48df6b2eef161d10ea1bc2626153197333662ba4cd700ee7ec1aba7f5"},
{file = "pydantic_core-2.46.0-cp314-cp314-win_amd64.whl", hash = "sha256:f07a5af60c5e7cf53dd1ff734228bd72d0dc9938e64a75b5bb308ca350d9681e"},
{file = "pydantic_core-2.46.0-cp314-cp314-win_arm64.whl", hash = "sha256:e7a77eca3c7d5108ff509db20aae6f80d47c7ed7516d8b96c387aacc42f3ce0f"},
{file = "pydantic_core-2.46.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:5e7cdd4398bee1aaeafe049ac366b0f887451d9ae418fd8785219c13fea2f928"},
{file = "pydantic_core-2.46.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5c2c92d82808e27cef3f7ab3ed63d657d0c755e0dbe5b8a58342e37bdf09bd2e"},
{file = "pydantic_core-2.46.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bab80af91cd7014b45d1089303b5f844a9d91d7da60eabf3d5f9694b32a6655"},
{file = "pydantic_core-2.46.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1e49ffdb714bc990f00b39d1ad1d683033875b5af15582f60c1f34ad3eeccfaa"},
{file = "pydantic_core-2.46.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ca877240e8dbdeef3a66f751dc41e5a74893767d510c22a22fc5c0199844f0ce"},
{file = "pydantic_core-2.46.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87e6843f89ecd2f596d7294e33196c61343186255b9880c4f1b725fde8b0e20d"},
{file = "pydantic_core-2.46.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e20bc5add1dd9bc3b9a3600d40632e679376569098345500799a6ad7c5d46c72"},
{file = "pydantic_core-2.46.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:ee6ff79a5f0289d64a9d6696a3ce1f98f925b803dd538335a118231e26d6d827"},
{file = "pydantic_core-2.46.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:52d35cfb58c26323101c7065508d7bb69bb56338cda9ea47a7b32be581af055d"},
{file = "pydantic_core-2.46.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d14cc5a6f260fa78e124061eebc5769af6534fc837e9a62a47f09a2c341fa4ea"},
{file = "pydantic_core-2.46.0-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:4f7ff859d663b6635f6307a10803d07f0d09487e16c3d36b1744af51dbf948b2"},
{file = "pydantic_core-2.46.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:8ef749be6ed0d69dba31902aaa8255a9bb269ae50c93888c4df242d8bb7acd9e"},
{file = "pydantic_core-2.46.0-cp314-cp314t-win32.whl", hash = "sha256:d93ca72870133f86360e4bb0c78cd4e6ba2a0f9f3738a6486909ffc031463b32"},
{file = "pydantic_core-2.46.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6ebb2668afd657e2127cb40f2ceb627dd78e74e9dfde14d9bf6cdd532a29ff59"},
{file = "pydantic_core-2.46.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4864f5bbb7993845baf9209bae1669a8a76769296a018cb569ebda9dcb4241f5"},
{file = "pydantic_core-2.46.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:be3e04979ba4d68183f247202c7f4f483f35df57690b3f875c06340a1579b47c"},
{file = "pydantic_core-2.46.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b1eae8d7d9b8c2a90b34d3d9014804dca534f7f40180197062634499412ea14e"},
{file = "pydantic_core-2.46.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a95a2773680dd4b6b999d4eccdd1b577fd71c31739fb4849f6ada47eabb9c56"},
{file = "pydantic_core-2.46.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25988c3159bb097e06abfdf7b21b1fcaf90f187c74ca6c7bb842c1f72ce74fa8"},
{file = "pydantic_core-2.46.0-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:747d89bd691854c719a3381ba46b6124ef916ae85364c79e11db9c84995d8d03"},
{file = "pydantic_core-2.46.0-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:909a7327b83ca93b372f7d48df0ebc7a975a5191eb0b6e024f503f4902c24124"},
{file = "pydantic_core-2.46.0-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:2f7e6a3752378a69fadf3f5ee8bc5fa082f623703eec0f4e854b12c548322de0"},
{file = "pydantic_core-2.46.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:ef47ee0a3ac4c2bb25a083b3acafb171f65be4a0ac1e84edef79dd0016e25eaa"},
{file = "pydantic_core-2.46.0.tar.gz", hash = "sha256:82d2498c96be47b47e903e1378d1d0f770097ec56ea953322f39936a7cf34977"},
] ]
[[package]] [[package]]
@ -3585,6 +3642,49 @@ files = [
{file = "soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349"}, {file = "soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349"},
] ]
[[package]]
name = "sqlalchemy"
version = "2.0.49"
requires_python = ">=3.7"
summary = "Database Abstraction Library"
groups = ["default"]
dependencies = [
"greenlet>=1; platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\"",
"importlib-metadata; python_version < \"3.8\"",
"typing-extensions>=4.6.0",
]
files = [
{file = "sqlalchemy-2.0.49-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5070135e1b7409c4161133aa525419b0062088ed77c92b1da95366ec5cbebbe"},
{file = "sqlalchemy-2.0.49-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ac7a3e245fd0310fd31495eb61af772e637bdf7d88ee81e7f10a3f271bff014"},
{file = "sqlalchemy-2.0.49-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d4e5a0ceba319942fa6b585cf82539288a61e314ef006c1209f734551ab9536"},
{file = "sqlalchemy-2.0.49-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3ddcb27fb39171de36e207600116ac9dfd4ae46f86c82a9bf3934043e80ebb88"},
{file = "sqlalchemy-2.0.49-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:32fe6a41ad97302db2931f05bb91abbcc65b5ce4c675cd44b972428dd2947700"},
{file = "sqlalchemy-2.0.49-cp311-cp311-win32.whl", hash = "sha256:46d51518d53edfbe0563662c96954dc8fcace9832332b914375f45a99b77cc9a"},
{file = "sqlalchemy-2.0.49-cp311-cp311-win_amd64.whl", hash = "sha256:951d4a210744813be63019f3df343bf233b7432aadf0db54c75802247330d3af"},
{file = "sqlalchemy-2.0.49-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bbccb45260e4ff1b7db0be80a9025bb1e6698bdb808b83fff0000f7a90b2c0b"},
{file = "sqlalchemy-2.0.49-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb37f15714ec2652d574f021d479e78cd4eb9d04396dca36568fdfffb3487982"},
{file = "sqlalchemy-2.0.49-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb9ec6436a820a4c006aad1ac351f12de2f2dbdaad171692ee457a02429b672"},
{file = "sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8d6efc136f44a7e8bc8088507eaabbb8c2b55b3dbb63fe102c690da0ddebe55e"},
{file = "sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e06e617e3d4fd9e51d385dfe45b077a41e9d1b033a7702551e3278ac597dc750"},
{file = "sqlalchemy-2.0.49-cp312-cp312-win32.whl", hash = "sha256:83101a6930332b87653886c01d1ee7e294b1fe46a07dd9a2d2b4f91bcc88eec0"},
{file = "sqlalchemy-2.0.49-cp312-cp312-win_amd64.whl", hash = "sha256:618a308215b6cececb6240b9abde545e3acdabac7ae3e1d4e666896bf5ba44b4"},
{file = "sqlalchemy-2.0.49-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df2d441bacf97022e81ad047e1597552eb3f83ca8a8f1a1fdd43cd7fe3898120"},
{file = "sqlalchemy-2.0.49-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8e20e511dc15265fb433571391ba313e10dd8ea7e509d51686a51313b4ac01a2"},
{file = "sqlalchemy-2.0.49-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47604cb2159f8bbd5a1ab48a714557156320f20871ee64d550d8bf2683d980d3"},
{file = "sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:22d8798819f86720bc646ab015baff5ea4c971d68121cb36e2ebc2ee43ead2b7"},
{file = "sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9b1c058c171b739e7c330760044803099c7fff11511e3ab3573e5327116a9c33"},
{file = "sqlalchemy-2.0.49-cp313-cp313-win32.whl", hash = "sha256:a143af2ea6672f2af3f44ed8f9cd020e9cc34c56f0e8db12019d5d9ecf41cb3b"},
{file = "sqlalchemy-2.0.49-cp313-cp313-win_amd64.whl", hash = "sha256:12b04d1db2663b421fe072d638a138460a51d5a862403295671c4f3987fb9148"},
{file = "sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24bd94bb301ec672d8f0623eba9226cc90d775d25a0c92b5f8e4965d7f3a1518"},
{file = "sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a51d3db74ba489266ef55c7a4534eb0b8db9a326553df481c11e5d7660c8364d"},
{file = "sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:55250fe61d6ebfd6934a272ee16ef1244e0f16b7af6cd18ab5b1fc9f08631db0"},
{file = "sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:46796877b47034b559a593d7e4b549aba151dae73f9e78212a3478161c12ab08"},
{file = "sqlalchemy-2.0.49-cp313-cp313t-win32.whl", hash = "sha256:9c4969a86e41454f2858256c39bdfb966a20961e9b58bf8749b65abf447e9a8d"},
{file = "sqlalchemy-2.0.49-cp313-cp313t-win_amd64.whl", hash = "sha256:b9870d15ef00e4d0559ae10ee5bc71b654d1f20076dbe8bc7ed19b4c0625ceba"},
{file = "sqlalchemy-2.0.49-py3-none-any.whl", hash = "sha256:ec44cfa7ef1a728e88ad41674de50f6db8cfdb3e2af84af86e0041aaf02d43d0"},
{file = "sqlalchemy-2.0.49.tar.gz", hash = "sha256:d15950a57a210e36dd4cec1aac22787e2a4d57ba9318233e2ef8b2daf9ff2d5f"},
]
[[package]] [[package]]
name = "stack-data" name = "stack-data"
version = "0.6.3" version = "0.6.3"
@ -3716,8 +3816,7 @@ name = "tzdata"
version = "2026.1" version = "2026.1"
requires_python = ">=2" requires_python = ">=2"
summary = "Provider of IANA time zone data" summary = "Provider of IANA time zone data"
groups = ["nb"] groups = ["default", "nb"]
marker = "python_version >= \"3.9\""
files = [ files = [
{file = "tzdata-2026.1-py2.py3-none-any.whl", hash = "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9"}, {file = "tzdata-2026.1-py2.py3-none-any.whl", hash = "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9"},
{file = "tzdata-2026.1.tar.gz", hash = "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98"}, {file = "tzdata-2026.1.tar.gz", hash = "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98"},

142
prototypes/db_access.py Normal file
View File

@ -0,0 +1,142 @@
# %%
import importlib
from pathlib import Path
import polars as pl
import sqlalchemy as sql
from wce_crm import db
importlib.reload(db)
# %%
PTH_DATA_DB = Path.cwd().parent / "data/db"
assert PTH_DATA_DB.exists()
assert PTH_DATA_DB.is_dir()
# %%
DB_KL = PTH_DATA_DB / "wce_kontaktliste.db"
DB_CRM = PTH_DATA_DB / "wce_crm.db"
assert DB_KL.exists()
assert DB_CRM.exists()
# %%
engine = sql.create_engine(f"sqlite:///{DB_CRM}")
# %%
db.df_crm_master
# %%
stmt = sql.select(db.ext_crm_master)
str(stmt.compile(engine))
df = pl.read_database(stmt, engine, schema_overrides=db.ext_crm_master_schema)
# df = pl.concat([df, df[:2]])
# %%
df.select("ma_unternehmensname").is_duplicated().sum()
# %%
q = df.lazy()
counter = pl.int_range(0, pl.len()).over(pl.col.ma_unternehmensname)
q = q.with_columns(
ma_unternehmensname_dedupl=pl.when(counter == 0)
.then(pl.col.ma_unternehmensname)
.otherwise(pl.format("{} ({})", pl.col.ma_unternehmensname, counter))
)
df = q.collect()
df.select("ma_unternehmensname_dedupl").is_duplicated().sum()
# %%
# mapping dedupl text to idx
df.head()
# dict(zip(df["ma_unternehmensname_dedupl"], df["ma_id"]))
# %%
sub = df[0]
sub
# sub.with_columns(
# # pl.when(pl.col(pl.Boolean)).then(pl.lit("Ja")).otherwise(pl.lit("Nein"))
# pl.when(pl.col(pl.Boolean)).then(pl.lit("Ja")).otherwise(pl.lit("Nein")).name.keep()
# )
# %%
q = (
sub.lazy()
.with_columns(
pl.col(pl.Datetime).dt.to_string("%d.%m.%Y"),
pl.col(pl.Date).dt.to_string("%d.%m.%Y"),
pl.when(pl.col(pl.Boolean)).then(pl.lit("Ja")).otherwise(pl.lit("Nein")).name.keep(),
)
.with_columns(pl.all().cast(pl.String))
)
sub = q.collect()
sub
# %%
df.row(0, named=True)
# %%
db.df_crm_master.estimated_size("mb")
# %%
# // CRM Nutzer
stmt = sql.select(db.ext_crm_nutzer).limit(20)
str(stmt.compile(engine))
df = pl.read_database(stmt, engine, schema_overrides=db.ext_crm_nutzer_schema)
# %%
stmt = sql.text("""SELECT ma_unternehmensname, ma_ersteintrag_datum, ma_aktualisierung_datum
FROM Master
WHERE ma_ersteintrag_datum LIKE '%ff'
LIMIT 10;""")
with engine.connect() as con:
res = con.execute(stmt)
print(res.fetchall())
# %%
# ----------------------------------------------------------------
engine = sql.create_engine(f"sqlite:///{DB_KL}")
stmt = sql.select(db.ext_kl_unternehmen.c.u_firmenname).limit(20)
with engine.connect() as con:
res = con.execute(stmt)
res.scalars().all()
# %%
for _ in res.mappings():
print(_)
# %%
# %%
stmt = sql.select(db.ext_kl_unternehmen)
df = pl.read_database(stmt, engine, schema_overrides=db.ext_kl_unternehmen_schema)
# %%
df
# %%
df.estimated_size("mb")
# %%
df.height
# %%
db.df_kontaktliste
# %%
sub = db.df_kontaktliste.select(["u_id", "u_firmenname"]).lazy()
# %%
counter = pl.int_range(0, pl.len()).over(pl.col.u_firmenname)
sub = sub.with_columns(
t=pl.when(counter == 0)
.then(pl.col.u_firmenname)
.otherwise(pl.format("{} ({})", pl.col.u_firmenname, counter))
)
# %%
sub.collect()
# %%
# 1. Create a sample DataFrame
df = pl.DataFrame({"text_col": ["TEST", "APPLE", "TEST", "TEST", "BANANA", "APPLE"]})
# 2. Define the window function to count occurrences
# This generates a sequence [0, 1, 2...] for each unique string
counter = pl.int_range(0, pl.len()).over("text_col")
# 3. Apply the conditional formatting
df = df.with_columns(
updated_col=pl.when(counter == 0)
.then(pl.col("text_col")) # Keep original for the first occurrence
.otherwise(pl.format("{} ({})", pl.col("text_col"), counter)) # Format duplicates
)
# %%
df

View File

@ -1,24 +1,30 @@
from __future__ import annotations from __future__ import annotations
import dataclasses as dc import dataclasses as dc
import enum
import sys import sys
import time
from collections.abc import Sequence
from PySide6.QtCore import Qt, Signal # Signal ist wichtig! from PySide6.QtCore import QDate, Qt, QTimer, Signal # Signal ist wichtig!
from PySide6.QtGui import QAction from PySide6.QtGui import QAction
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QApplication, QApplication,
QComboBox, QComboBox,
QCompleter, QCompleter,
QDateEdit,
QDialog, QDialog,
QDialogButtonBox, QDialogButtonBox,
QFormLayout, QFormLayout,
QFrame, QFrame,
QGridLayout, QGridLayout,
QGroupBox,
QHBoxLayout, QHBoxLayout,
QLabel, QLabel,
QLineEdit, QLineEdit,
QListWidget, QListWidget,
QMainWindow, QMainWindow,
QMessageBox,
QPlainTextEdit, QPlainTextEdit,
QPushButton, QPushButton,
QScrollArea, QScrollArea,
@ -28,6 +34,22 @@ from PySide6.QtWidgets import (
QWidget, QWidget,
) )
from wce_crm.backend import initial_recording as be_init_rec
QSS = """
*[styleClass="stempel"] {
background-color: #f1f5f9;
color: #333D4B;
border: 1px dashed #cbd5e1;
border-radius: 4px;
padding: 5px;
}
*[styleClass="stempel"]:focus {
border: 1px dashed #cbd5e1;
}
"""
@dc.dataclass(slots=True) @dc.dataclass(slots=True)
class Address: class Address:
@ -134,16 +156,24 @@ class AddressForm_Search(QWidget):
super().__init__() super().__init__()
main_layout = QVBoxLayout(self) main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0)
form_layout = QFormLayout() form_layout = QFormLayout()
form_layout.setSpacing(15) form_layout.setSpacing(10)
# title
title = QLabel("--- Suche Unternehmen ---")
title.setStyleSheet("font-size: 14px; font-style: italic;") # font-weight: bold;
main_layout.addWidget(title)
self.search_input = QLineEdit(placeholderText="Tippen zum Suchen...") self.search_input = QLineEdit(placeholderText="Tippen zum Suchen...")
form_layout.addRow("Suche:", self.search_input) form_layout.addRow("Suche:", self.search_input)
search_data = [addr.name for addr in ADDRESSES] # search_data = [addr.name for addr in ADDRESSES]
self.SEARCH_MAP = {addr.name: addr for addr in ADDRESSES} # self.SEARCH_MAP = {addr.name: addr for addr in ADDRESSES}
self.completer = QCompleter(search_data) self.SEARCH_MAP = be_init_rec.comp_search_choice_mapping()
self.completer.setCaseSensitivity(Qt.CaseInsensitive) self.search_data = tuple(self.SEARCH_MAP.keys())
self.completer.setFilterMode(Qt.MatchContains) self.completer = QCompleter(self.search_data)
self.completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
self.completer.setFilterMode(Qt.MatchFlag.MatchContains)
self.search_input.setCompleter(self.completer) self.search_input.setCompleter(self.completer)
self.completer.activated.connect(self.search_result_selected) self.completer.activated.connect(self.search_result_selected)
@ -207,15 +237,23 @@ class AddressForm_Search(QWidget):
} }
""") """)
def fill_out(self, address: Address): def fill_out(self, comp_info: be_init_rec.CompanyInfo):
addr_ = address.export() # addr_ = address.export()
for field, value in zip(self.autofilled_fields, addr_.values()): # for field, value in zip(self.autofilled_fields, addr_.values()):
field.setText(value) # field.setText(value)
self.company_input.setText(comp_info["ma_unternehmensname"])
self.street_input.setText(comp_info["ma_strasse"])
self.number_input.setText(comp_info["ma_hausnummer"])
self.zip_input.setText(comp_info["ma_plz"])
self.city_input.setText(comp_info["ma_ort"])
def search_result_selected(self, name): def search_result_selected(self, name):
address = self.SEARCH_MAP[name]
self.fill_out(address) comp_info = be_init_rec.comp_search_get_info(
ma_id=self.SEARCH_MAP[name],
)
self.fill_out(comp_info)
class DropdownSearch(QWidget): class DropdownSearch(QWidget):
@ -242,11 +280,11 @@ class DropdownSearch(QWidget):
# --- WICHTIGE EINSTELLUNGEN --- # --- WICHTIGE EINSTELLUNGEN ---
# Ignoriert Groß-/Kleinschreibung (sehr wichtig für eine gute Suche!) # Ignoriert Groß-/Kleinschreibung (sehr wichtig für eine gute Suche!)
self.completer.setCaseSensitivity(Qt.CaseInsensitive) self.completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
# 'MatchContains' sorgt dafür, dass "pha" auch "Projekt Alpha" findet. # 'MatchContains' sorgt dafür, dass "pha" auch "Projekt Alpha" findet.
# Standard ist 'MatchStartsWith' (findet nur Worte am Anfang). # Standard ist 'MatchStartsWith' (findet nur Worte am Anfang).
self.completer.setFilterMode(Qt.MatchContains) self.completer.setFilterMode(Qt.MatchFlag.MatchContains)
# 4. Den Completer an das Eingabefeld binden # 4. Den Completer an das Eingabefeld binden
self.search_input.setCompleter(self.completer) self.search_input.setCompleter(self.completer)
@ -261,78 +299,405 @@ class DropdownSearch(QWidget):
# Hier könntest du z.B. deine Detail-Seite für dieses Projekt öffnen # Hier könntest du z.B. deine Detail-Seite für dieses Projekt öffnen
class FormFieldType(enum.StrEnum):
TEXT = enum.auto()
LONGTEXT = enum.auto()
DATE = enum.auto()
DATETIME = enum.auto()
@dc.dataclass(slots=True)
class FormField:
key: str
label: str
type: FormFieldType
required: bool
placeholder: str | None = None
fill_value: str | None = None
readonly: bool = False
def __post_init__(self) -> None:
self.label = self.label.strip()
if not self.label.endswith(":"):
self.label += ":"
if self.required:
self.label += "*"
@dc.dataclass(slots=True)
class FormFieldGroup:
key: str
label: str
fields: Sequence[FormField]
FORM_FIELD_DEF = [
FormField("name", "Projektname", FormFieldType.TEXT, True, "Bitte füllen..."),
FormField("descr", "Beschreibung", FormFieldType.LONGTEXT, False),
FormField(
"ext_data",
"Externe Daten",
FormFieldType.TEXT,
True,
fill_value="Lorem ipsum und so weiter...",
readonly=True,
),
FormField("init_date", "Auftragsdatum", FormFieldType.DATE, True),
FormField("start_date", "Startdatum", FormFieldType.DATE, True),
FormField(
"ms_date1",
"MS Datum 1",
FormFieldType.DATE,
False,
"",
fill_value="26.07.2026",
readonly=True,
),
FormField(
"ms_date2",
"MS Datum 2",
FormFieldType.DATE,
False,
"",
fill_value="30.08.2026",
readonly=False,
),
FormField(
"important:notes",
"Wichtige Notizen",
FormFieldType.LONGTEXT,
True,
"Text eingeben...",
),
]
FORM_FIELD_DEF2 = [
FormField("name", "Projektname", FormFieldType.TEXT, True, "Bitte füllen..."),
FormField("descr", "Beschreibung", FormFieldType.LONGTEXT, False),
FormField(
"ext_data",
"Externe Daten",
FormFieldType.TEXT,
True,
fill_value="Lorem ipsum und so weiter...",
readonly=True,
),
FormField("init_date", "Auftragsdatum", FormFieldType.DATE, True),
FormField("start_date", "Startdatum", FormFieldType.DATE, True),
FormField(
"ms_date1",
"MS Datum 1",
FormFieldType.DATE,
False,
"",
fill_value="26.07.2026",
readonly=True,
),
FormField(
"ms_date2",
"MS Datum 2",
FormFieldType.DATE,
False,
"",
fill_value="30.08.2026",
readonly=False,
),
FormField(
"important:notes",
"Wichtige Notizen",
FormFieldType.LONGTEXT,
True,
"Text eingeben...",
),
]
FORM_FIELD_GROUPS = [
FormFieldGroup("group1", "Test-1", FORM_FIELD_DEF),
FormFieldGroup("group2", "Test-2", FORM_FIELD_DEF2),
]
class MyForm(QWidget): class MyForm(QWidget):
def __init__(self): def __init__(
self,
form_field_groups: Sequence[FormFieldGroup],
add_buttons: bool = True,
) -> None:
super().__init__() super().__init__()
# --- LAYOUT ---
self.main_layout = QVBoxLayout(self)
self.main_layout.setContentsMargins(0, 0, 0, 0)
self.form_field_groups = form_field_groups
# Das Herzstück: Das Form-Layout for fg in form_field_groups:
# Es kümmert sich automatisch darum, dass alle Labels links widget = MyFormPart(fg.fields, fg.label, add_buttons=False)
# und alle Felder rechts perfekt bündig untereinander stehen. self.main_layout.addWidget(widget)
self.form_layout = QFormLayout(self)
self.form_layout.setSpacing(15) # Abstand zwischen den Zeilen
# Definition deiner Felder # buttons
# 'key' ist der Name, unter dem du die Daten später abrufst # self.add_buttons = add_buttons
# 'label' ist der Text, der angezeigt wird # if self.add_buttons:
# 'type' bestimmt, welches Widget erstellt wird # self.layout_btn = QHBoxLayout()
self.field_definitions = [ # self.main_layout.addLayout(self.layout_btn)
{"key": "name", "label": "Projektname:", "type": "text"}, # self.save_btn_txt_enabled = "Speichern (Strg + S)"
{"key": "date", "label": "Datum:", "type": "date", "value": "22.04.2026"}, # self.save_btn_txt_disabled = "Wird gespeichert..."
{ # self.save_btn = QPushButton(self.save_btn_txt_enabled)
"key": "status", # self.save_btn.setShortcut("Ctrl+S")
"label": "Status:", # self.save_btn.setFixedHeight(50)
"type": "text", # self.save_btn.setSizePolicy(
"placeholder": "z.B. Aktiv", # QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
}, # )
{"key": "desc", "label": "Beschreibung:", "type": "longtext"}, # self.save_btn.clicked.connect(self.on_save_clicked)
{"key": "notes", "label": "Interne Notizen:", "type": "longtext"}, # self.layout_btn.addWidget(self.save_btn)
] # self.reset_btn = QPushButton("Zurücksetzen (Strg + Z)")
# self.reset_btn.setShortcut("Ctrl+Z")
# self.reset_btn.setFixedHeight(50)
# self.reset_btn.setSizePolicy(
# QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
# )
# self.reset_btn.clicked.connect(self.reset_form)
# self.layout_btn.addWidget(self.reset_btn)
# Dictionary, um die erstellten Widgets zu speichern (für späteren Zugriff)
self.widgets = {}
# Automatischer Aufbau des Formulars class MyFormPart(QWidget):
def __init__(
self,
field_definitons: Sequence[FormField],
group_name: str | None = None,
add_buttons: bool = False,
):
super().__init__()
self.setStyleSheet("""
QGroupBox {
font-size: 16px;
font-weight: bold;
border: 1px solid #cbd5e1; /* Heller, moderner Rahmen */
border-radius: 8px; /* Abgerundete Ecken */
margin-top: 15px; /* Platz für die Überschrift schaffen */
padding-top: 15px; /* Abstand zwischen Rahmen und erstem Feld */
}
QGroupBox::title {
subcontrol-origin: margin;
subcontrol-position: top left; /* Überschrift oben links */
padding: 0 5px; /* Etwas Luft links und rechts vom Text */
color: #334155; /* Dunkelgraue Schrift */
}
""")
# --- LAYOUT ---
self.main_layout = QVBoxLayout(self)
self.main_layout.setContentsMargins(0, 0, 0, 0)
self.form_layout = QFormLayout()
self.group_box: QGroupBox | None = None
self.group_name = group_name
if self.group_name:
self.group_box = QGroupBox(group_name)
self.main_layout.addWidget(self.group_box)
self.group_box.setLayout(self.form_layout)
else:
self.main_layout.addLayout(self.form_layout)
self.form_layout.setSpacing(10) # Abstand zwischen den Zeilen
self.field_definitions = field_definitons
self.widgets: dict[str, QWidget] = {}
# automatic build
self.create_form_fields() self.create_form_fields()
def create_form_fields(self): # buttons
self.add_buttons = add_buttons
if self.add_buttons:
self.layout_btn = QHBoxLayout()
self.main_layout.addLayout(self.layout_btn)
self.save_btn_txt_enabled = "Speichern (Strg + S)"
self.save_btn_txt_disabled = "Wird gespeichert..."
self.save_btn = QPushButton(self.save_btn_txt_enabled)
self.save_btn.setShortcut("Ctrl+S")
self.save_btn.setFixedHeight(50)
self.save_btn.setSizePolicy(
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
)
self.save_btn.clicked.connect(self.on_save_clicked)
self.layout_btn.addWidget(self.save_btn)
self.reset_btn = QPushButton("Zurücksetzen (Strg + Z)")
self.reset_btn.setShortcut("Ctrl+Z")
self.reset_btn.setFixedHeight(50)
self.reset_btn.setSizePolicy(
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
)
self.reset_btn.clicked.connect(self.reset_form)
self.layout_btn.addWidget(self.reset_btn)
def create_form_fields(self) -> None:
for field in self.field_definitions: for field in self.field_definitions:
widget = None widget = None
# Entscheidung: Welches Widget wird benötigt? match field.type:
if field["type"] == "text": case FormFieldType.TEXT:
widget = QLineEdit() widget = QLineEdit()
if "placeholder" in field: if field.placeholder:
widget.setPlaceholderText(field["placeholder"]) widget.setPlaceholderText(field.placeholder)
if "value" in field: if field.fill_value:
widget.setText(field["value"]) widget.setText(field.fill_value)
if field.readonly:
widget.setReadOnly(True) # Falls es ein Fixwert ist widget.setReadOnly(True) # Falls es ein Fixwert ist
widget.setProperty("styleClass", "stempel")
elif field["type"] == "longtext": case FormFieldType.LONGTEXT:
widget = QPlainTextEdit() widget = QPlainTextEdit()
widget.setMaximumHeight(80) # Kompakte Höhe für Formulare widget.setMaximumHeight(80) # Kompakte Höhe für Formulare
if field.placeholder:
widget.setPlaceholderText(field.placeholder)
if field.readonly:
widget.setReadOnly(True) # Falls es ein Fixwert ist
widget.setProperty("styleClass", "stempel")
elif field["type"] == "date": case FormFieldType.DATE:
widget = QLineEdit() # Oder QDateEdit widget = QDateEdit() # Oder QDateEdit
widget.setText(field.get("value", "")) widget.setCalendarPopup(True)
widget.setReadOnly(True) cal = widget.calendarWidget()
widget.setStyleSheet("background-color: #f1f5f9; border: 1px dashed #cbd5e1;") if cal:
cal.setFirstDayOfWeek(Qt.DayOfWeek.Monday)
cal.setGridVisible(True)
cal.setStyleSheet("""
QCalendarWidget QAbstractItemView {
selection-background-color: #2980b9;
selection-color: white;
}
""")
if field.fill_value:
set_date = QDate.fromString(field.fill_value, "dd.MM.yyyy")
if not set_date.isValid():
raise ValueError(
f"Could not parse date field value >{field.fill_value}<"
)
widget.setDate(set_date)
else:
widget.setDate(QDate.currentDate())
if field.readonly:
widget = QLineEdit()
widget.setReadOnly(True) # Falls es ein Fixwert ist
widget.setProperty("styleClass", "stempel")
if field.fill_value:
widget.setText(field.fill_value)
case _:
raise NotImplementedError(f"Not supported field type: {field.type.value}")
if widget: if widget:
# Widget im Dictionary speichern, um später darauf zuzugreifen self.widgets[field.key] = widget
self.widgets[field["key"]] = widget self.form_layout.addRow(field.label, widget)
# Dem Form-Layout hinzufügen (Label links, Widget rechts)
self.form_layout.addRow(field["label"], widget)
def get_form_data(self): def get_form_data(self) -> ...:
"""Liest alle Felder automatisch aus""" """Liest alle Felder automatisch aus"""
data = {} data = {}
for key, widget in self.widgets.items(): for key, widget in self.widgets.items():
if isinstance(widget, QLineEdit): if isinstance(widget, (QLineEdit, QDateEdit)):
data[key] = widget.text() data[key] = widget.text()
elif isinstance(widget, QPlainTextEdit): elif isinstance(widget, QPlainTextEdit):
data[key] = widget.toPlainText() data[key] = widget.toPlainText()
return data return data
def _disable_save(self) -> None:
self.save_btn.setEnabled(False)
self.save_btn.setText(self.save_btn_txt_disabled)
def _enable_save(
self,
timeout: int = 3000,
) -> None:
QTimer.singleShot(timeout, lambda: self.save_btn.setEnabled(True))
QTimer.singleShot(timeout + 1, lambda: self.save_btn.setShortcut("Ctrl+S"))
self.save_btn.setText(self.save_btn_txt_enabled)
def on_save_clicked(self) -> None:
self._disable_save()
errors = [] # Hier sammeln wir die Namen der fehlenden Felder
# time.sleep(0.5)
# Wir gehen unsere Feld-Definitionen durch
for field in self.field_definitions:
widget = self.widgets[field.key]
# 1. Zuerst setzen wir das Design des Feldes wieder auf "Normal" zurück.
# Falls der Nutzer den Fehler vorher schon korrigiert hat, muss der rote Rand weg!
if not field.readonly:
widget.setStyleSheet("")
# 2. Ist es überhaupt ein Pflichtfeld?
if not field.required:
continue
is_empty = False
if isinstance(widget, (QLineEdit, QDateEdit)):
if not widget.text().strip():
is_empty = True
elif isinstance(widget, QPlainTextEdit):
if not widget.toPlainText().strip():
is_empty = True
if not is_empty:
continue
errors.append(
field.label.replace("*", "").replace(":", "")
) # Sternchen für die Fehlermeldung entfernen
# Optisches Feedback: Heller roter Hintergrund und roter Rand
widget.setStyleSheet("""
border: 1px solid #ef4444;
background-color: #ffe9e9;
padding: 4px;
border-radius: 4px;
""")
# --- ERGEBNIS AUSWERTEN ---
if errors:
# Es gibt Fehler! Speichern abbrechen und Pop-up anzeigen.
error_text = "Bitte fülle die folgenden Pflichtfelder aus:\n\n- " + "\n- ".join(
errors
)
QMessageBox.warning(self, "Fehlende Angaben", error_text)
self._enable_save()
return
# Wenn wir hier ankommen, ist die Liste 'errors' leer. Alles ist korrekt ausgefüllt!
# time.sleep(0.5)
print("Erfolg! Alle Daten sind valide.")
self.reset_form()
self._enable_save()
def reset_form(self) -> None:
for field in self.field_definitions:
widget = self.widgets[field.key]
if field.readonly:
continue
if isinstance(widget, QLineEdit):
widget.clear()
if field.fill_value:
widget.setText(field.fill_value)
elif isinstance(widget, QPlainTextEdit):
widget.clear()
elif isinstance(widget, QDateEdit):
if field.fill_value:
set_date = QDate.fromString(field.fill_value, "dd.MM.yyyy")
if not set_date.isValid():
raise ValueError(
f"Could not parse date field value >{field.fill_value}<"
)
widget.setDate(set_date)
else:
widget.setDate(QDate.currentDate())
widget.setStyleSheet("")
class ClickableCell(QFrame): class ClickableCell(QFrame):
# Wir definieren ein Signal, das ein Dictionary (die Daten) mitschickt # Wir definieren ein Signal, das ein Dictionary (die Daten) mitschickt
@ -355,11 +720,11 @@ class ClickableCell(QFrame):
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
label = QLabel(text) label = QLabel(text)
label.setWordWrap(True) label.setWordWrap(True)
label.setAlignment(Qt.AlignCenter) label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(label) layout.addWidget(label)
def mousePressEvent(self, event): def mousePressEvent(self, event):
if event.button() == Qt.LeftButton: if event.button() == Qt.MouseButton.LeftButton:
# Wenn geklickt wird, senden wir die Daten aus # Wenn geklickt wird, senden wir die Daten aus
self.clicked.emit(self.data_record) self.clicked.emit(self.data_record)
@ -369,7 +734,7 @@ class HeaderCell(QLabel):
super().__init__(text) super().__init__(text)
# Textausrichtung zentrieren # Textausrichtung zentrieren
self.setAlignment(Qt.AlignCenter) self.setAlignment(Qt.AlignmentFlag.AlignCenter)
# Styling: Fetter Text, grauer Hintergrund (entspricht slate-200), leicht abgerundet # Styling: Fetter Text, grauer Hintergrund (entspricht slate-200), leicht abgerundet
self.setStyleSheet(""" self.setStyleSheet("""
@ -407,7 +772,9 @@ class NewEntryDialog(QDialog):
layout.addRow("Datum:", self.input_date) layout.addRow("Datum:", self.input_date)
# Standard-Buttons (OK und Abbrechen) # Standard-Buttons (OK und Abbrechen)
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
)
buttons.accepted.connect(self.accept) # Schließt Dialog und meldet "Erfolg" buttons.accepted.connect(self.accept) # Schließt Dialog und meldet "Erfolg"
buttons.rejected.connect(self.reject) # Schließt Dialog und meldet "Abbruch" buttons.rejected.connect(self.reject) # Schließt Dialog und meldet "Abbruch"
layout.addWidget(buttons) layout.addWidget(buttons)
@ -431,7 +798,7 @@ class DetailView(QWidget):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
layout.setAlignment(Qt.AlignTop | Qt.AlignLeft) layout.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft)
# Zurück-Button # Zurück-Button
back_btn = QPushButton("← Zurück zur Tabelle") back_btn = QPushButton("← Zurück zur Tabelle")
@ -470,7 +837,7 @@ class NewEntrySelect_view(QWidget):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
layout.setAlignment(Qt.AlignTop | Qt.AlignLeft) layout.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft)
# Zurück-Button # Zurück-Button
back_btn = QPushButton("← Zurück zur Übersicht") back_btn = QPushButton("← Zurück zur Übersicht")
@ -524,38 +891,65 @@ class SearchFormPage(QWidget):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
# Hauptlayout der Seite # Hauptlayout der Seite
main_layout = QVBoxLayout(self) outer_layout = QHBoxLayout(self)
vert_layout = QVBoxLayout()
# main_layout.setContentsMargins(0, 0, 0, 0) # main_layout.setContentsMargins(0, 0, 0, 0)
outer_layout.addStretch(1)
outer_layout.addLayout(vert_layout, stretch=100)
# outer_layout.addWidget(scroll_area, stretch=100)
outer_layout.addStretch(1)
# Optional: Damit der Container oben am Rand klebt
outer_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
# --- 1. HEADER --- # --- HEADER ---
header_layout = QVBoxLayout() header_container = QWidget()
upper_button_group_v = QVBoxLayout() header_container.setMinimumWidth(700)
upper_button_group = QHBoxLayout() header_container.setMaximumWidth(1000)
upper_button_group.addLayout(upper_button_group_v) header_container.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
upper_button_group.addStretch() header_layout = QVBoxLayout(header_container)
# header_layout = QGridLayout() header_layout.setContentsMargins(0, 0, 0, 10)
# header_layout.setColumnStretch(0, 1)
# header_layout.setColumnStretch(1, 1)
back_btn_main = QPushButton("← Zurück zur Übersicht") back_btn_main = QPushButton("← Zurück zur Übersicht")
back_btn_main.clicked.connect(lambda: self.back_main_requested.emit()) back_btn_main.clicked.connect(lambda: self.back_main_requested.emit())
back_btn_main.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
back_btn_main.setMinimumWidth(200)
back_btn_main.setMaximumWidth(200)
back_btn_step = QPushButton("← Zurück") back_btn_step = QPushButton("← Zurück")
back_btn_step.clicked.connect(lambda: self.back_requested.emit()) back_btn_step.clicked.connect(lambda: self.back_requested.emit())
back_btn_step.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
back_btn_step.setMinimumWidth(200)
back_btn_step.setMaximumWidth(200)
title = QLabel("Grunderfassung Unternehmen") title = QLabel("Grunderfassung Unternehmen")
title.setStyleSheet("font-size: 20px; font-weight: bold;") title.setStyleSheet("font-size: 20px; font-weight: bold;")
upper_button_group_v.addWidget(back_btn_step) header_layout.setSpacing(5)
upper_button_group_v.addWidget(back_btn_main) header_layout.addWidget(back_btn_step)
header_layout.addWidget(back_btn_main)
header_layout.addLayout(upper_button_group)
header_layout.addSpacing(15)
# header_layout.addWidget(back_btn_step)
header_layout.addWidget(title) header_layout.addWidget(title)
# header_layout.addStretch() # Drückt den Titel nach links vert_layout.addWidget(header_container)
main_layout.addLayout(header_layout)
# --- HAUPTINHALT ---
container = QWidget()
# SCROLL-BEREICH
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(
True
) # WICHTIG: Erlaubt dem Grid im Inneren, sich an die Breite anzupassen
scroll_area.setMinimumWidth(700)
scroll_area.setMaximumWidth(1000)
scroll_area.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
# Optional: Rahmen der ScrollArea entfernen, damit es "flacher" und moderner aussieht
scroll_area.setFrameShape(QFrame.Shape.NoFrame)
scroll_area.setWidget(container)
# vert_layout.addSpacing(20)
vert_layout.addWidget(scroll_area)
# --- KOPF Metadaten ---
container_layout = QVBoxLayout(container)
container_layout.setContentsMargins(0, 0, 0, 0)
# --- KOPF Unternehmen ---
inf_block_1 = QHBoxLayout() inf_block_1 = QHBoxLayout()
inhalte = [ inhalte = [
"Fall-Nr.:", "Fall-Nr.:",
@ -565,7 +959,7 @@ class SearchFormPage(QWidget):
] ]
for entry in inhalte: for entry in inhalte:
label = QLabel(entry) label = QLabel(entry)
label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) label.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
field = QLineEdit(placeholderText="...") field = QLineEdit(placeholderText="...")
field.setText("22.04.2026") field.setText("22.04.2026")
field.setReadOnly(True) field.setReadOnly(True)
@ -582,107 +976,47 @@ class SearchFormPage(QWidget):
border: 1px #cbd5e1; border: 1px #cbd5e1;
} }
""") """)
field.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) field.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
inf_block_1.addWidget(label) inf_block_1.addWidget(label)
inf_block_1.addWidget(field) inf_block_1.addWidget(field)
inf_block_1.addStretch() container_layout.addLayout(inf_block_1)
main_layout.addLayout(inf_block_1)
# --- NOTIZEN Unternehmen --- # --- NOTIZEN Unternehmen ---
# eventuell später verknüpft # eventuell später verknüpft
inf_block_2 = QHBoxLayout() inf_block_2 = QHBoxLayout()
label = QLabel("Notizen:") label = QLabel("Notizen:")
label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) label.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
inf_block_2.addWidget(label, alignment=Qt.AlignmentFlag.AlignTop) inf_block_2.addWidget(label, alignment=Qt.AlignmentFlag.AlignTop)
inf_block_2.addWidget(QPlainTextEdit(placeholderText="Notizen ergänzen...")) inf_block_2.addWidget(QPlainTextEdit(placeholderText="Notizen ergänzen..."))
main_layout.addLayout(inf_block_2) container_layout.addLayout(inf_block_2)
main_layout.addSpacing(10) container_layout.addSpacing(10)
# --- Suche mit Namen # --- Test Formularlayout ---
# inf_block_3 = ( container_layout.addSpacing(30)
# QVBoxLayout() title = QLabel("--- Automatische Form ---")
# ) # Horizontal, damit Suchen und Filtern nebeneinander stehen title.setStyleSheet("font-size: 14px; font-style: italic;") # font-weight: bold;
# name_input = QLineEdit(placeholderText="Tippe zum Suchen...") container_layout.addWidget(title)
# name_input.setMinimumWidth(150) # container_layout.addWidget(MyFormPart(FORM_FIELD_DEF, "Test-Gruppe"))
# name_input.setMaximumWidth(600) container_layout.addWidget(MyForm(FORM_FIELD_GROUPS))
# inf_block_3_1 = QHBoxLayout()
# inf_block_3_1.addWidget(QLabel("Name"))
# inf_block_3_1.addWidget(name_input, stretch=100)
# inf_block_3_1.addStretch()
# inf_block_3_2 = QHBoxLayout() container_layout.addSpacing(30)
# inf_block_3_3 = QHBoxLayout()
# demo_data = {
# "name": "Test UG",
# "Straße": "Teststraße",
# "Hausnummer": "12",
# "PLZ": "09111",
# "Ort": "Chemnitz",
# }
# current_block = inf_block_3_2
# for entry in ("Straße", "Hausnummer", "PLZ", "Ort"):
# if entry == "PLZ":
# current_block = inf_block_3_3
# label = QLabel(entry)
# label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
# field = QLineEdit()
# field.setText(demo_data[entry])
# field.setReadOnly(True)
# field.setStyleSheet("""
# QLineEdit {
# background-color: #f1f5f9; /* Helles System-Grau */
# color: #333D4B; /* Etwas blassere Schrift */
# border: 1px dashed #cbd5e1; /* Ein gestrichelter Rand wirkt oft wie ein "Stempel" */
# border-radius: 4px;
# padding: 5px;
# }
# /* Wenn das Feld fokussiert wird, keinen blauen Rand anzeigen */
# QLineEdit:focus {
# border: 1px dashed #cbd5e1;
# }
# """)
# if entry in ("Hausnummer", "PLZ"):
# field.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
# field.setMinimumWidth(50)
# field.setMaximumWidth(50)
# else:
# field.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
# field.setMinimumWidth(100)
# field.setMaximumWidth(300)
# current_block.addWidget(label)
# current_block.addWidget(field, stretch=100)
# inf_block_3_2.addStretch()
# inf_block_3_3.addStretch()
# inf_block_3.addLayout(inf_block_3_1)
# inf_block_3.addLayout(inf_block_3_2)
# inf_block_3.addLayout(inf_block_3_3)
# main_layout.addLayout(inf_block_3)
main_layout.addSpacing(30)
main_layout.addWidget(MyForm())
main_layout.addSpacing(30)
# --- SUCHE MIT NAMEN ---
# addr = Address("Test UG", "Teststraße", 202, "09111", "Chemnitz") # addr = Address("Test UG", "Teststraße", 202, "09111", "Chemnitz")
# addr_widget = AddressForm() # addr_widget = AddressForm()
# addr_widget.fill_out(addr) # addr_widget.fill_out(addr)
addr_widget = AddressForm_Search() addr_widget = AddressForm_Search()
main_layout.addWidget(addr_widget) container_layout.addWidget(addr_widget)
main_layout.addSpacing(30) container_layout.addSpacing(30)
main_layout.addWidget(DropdownSearch()) # container_layout.addWidget(DropdownSearch())
main_layout.addSpacing(30) container_layout.addSpacing(30)
# --- 2. SUCH-FORMULAR --- # --- SUCH-FORMULAR ---
form_layout = ( form_layout = (
QHBoxLayout() QHBoxLayout()
) # Horizontal, damit Suchen und Filtern nebeneinander stehen ) # Horizontal, damit Suchen und Filtern nebeneinander stehen
@ -702,12 +1036,12 @@ class SearchFormPage(QWidget):
form_layout.addWidget(QLabel("Status:")) form_layout.addWidget(QLabel("Status:"))
form_layout.addWidget(self.status_filter, stretch=1) form_layout.addWidget(self.status_filter, stretch=1)
main_layout.addLayout(form_layout) container_layout.addLayout(form_layout)
# --- 3. ERGEBNIS-BEREICH --- # --- 3. ERGEBNIS-BEREICH ---
# Für den Anfang ein einfaches Listen-Widget # Für den Anfang ein einfaches Listen-Widget
self.results_list = QListWidget() self.results_list = QListWidget()
main_layout.addWidget( container_layout.addWidget(
self.results_list, stretch=100 self.results_list, stretch=100
) # Nimmt den restlichen Platz ein ) # Nimmt den restlichen Platz ein
@ -740,7 +1074,7 @@ class MainWindow(QMainWindow):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.setWindowTitle("Master") self.setWindowTitle("Master")
self.resize(1800, 200) self.resize(1800, 1000)
# --- 1. DAS MENÜ ERSTELLEN --- # --- 1. DAS MENÜ ERSTELLEN ---
self.create_menu() self.create_menu()
@ -801,9 +1135,9 @@ class MainWindow(QMainWindow):
scroll_area.setMaximumWidth( scroll_area.setMaximumWidth(
1500 1500
) # Die Breiten-Begrenzung wandert nun auf die ScrollArea ) # Die Breiten-Begrenzung wandert nun auf die ScrollArea
scroll_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) scroll_area.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
# Optional: Rahmen der ScrollArea entfernen, damit es "flacher" und moderner aussieht # Optional: Rahmen der ScrollArea entfernen, damit es "flacher" und moderner aussieht
scroll_area.setFrameShape(QFrame.NoFrame) scroll_area.setFrameShape(QFrame.Shape.NoFrame)
scroll_area.setWidget(container) scroll_area.setWidget(container)
vert_layout.addWidget(new_btn) vert_layout.addWidget(new_btn)
@ -817,7 +1151,7 @@ class MainWindow(QMainWindow):
# outer_layout.addWidget(scroll_area, stretch=100) # outer_layout.addWidget(scroll_area, stretch=100)
outer_layout.addStretch(1) outer_layout.addStretch(1)
# Optional: Damit der Container oben am Rand klebt # Optional: Damit der Container oben am Rand klebt
outer_layout.setAlignment(Qt.AlignTop) outer_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
# Wir geben dem Container ein vertikales Layout # Wir geben dem Container ein vertikales Layout
container_layout = QVBoxLayout(container) container_layout = QVBoxLayout(container)
@ -961,6 +1295,7 @@ class MainWindow(QMainWindow):
if __name__ == "__main__": if __name__ == "__main__":
app = QApplication(sys.argv) app = QApplication(sys.argv)
app.setStyleSheet(QSS)
window = MainWindow() window = MainWindow()
window.show() window.show()
sys.exit(app.exec()) sys.exit(app.exec())

View File

@ -1,28 +1,45 @@
# %% # %%
import dataclasses as dc import dataclasses as dc
import enum
from PySide6.QtCore import QDate, Qt
class FormFieldType(enum.StrEnum):
TEXT = enum.auto()
LONGTEXT = enum.auto()
DATE = enum.auto()
DATETIME = enum.auto()
# %%
@dc.dataclass(slots=True) @dc.dataclass(slots=True)
class Address: class FormField:
street: str key: str
number: int label: str
postal_code: str type: FormFieldType
city: str required: bool
def export(self): def __post_init__(self) -> None:
data = {} self.label = self.label.strip()
for f in dc.fields(self): if not self.label.endswith(":"):
val = getattr(self, f.name) self.label += ":"
if f.type is int: if self.required:
val = str(val) self.label += "*"
data[f.name] = val
return data
# %% # %%
addr = Address("Teststraße", 202, "09111", "Chemnitz") FormField("name", "Projektbeschreibung", FormFieldType.LONGTEXT, required=True)
# %%
FormField("name", "Projektbeschreibung:", FormFieldType.LONGTEXT, required=True)
# %%
FormField("name", "Projektbeschreibung", FormFieldType.LONGTEXT, required=False)
# %%
FormField("name", "Projektbeschreibung:", FormFieldType.LONGTEXT, required=False)
# %% # %%
addr.export() addr.export()
# %% # %%
set_date = QDate.fromString("26.07.2026", "dd.MM.yyyy")
# %%
Qt.Tet

View File

@ -5,7 +5,7 @@ description = "GUI for CRM of NAFKA project with WCE"
authors = [ authors = [
{name = "d-opt GmbH, resp. Florian Förster", email = "f.foerster@d-opt.com"}, {name = "d-opt GmbH, resp. Florian Förster", email = "f.foerster@d-opt.com"},
] ]
dependencies = ["nicegui>=3.10.0", "pyside6>=6.11.0"] dependencies = ["nicegui>=3.10.0", "pyside6>=6.11.0", "sqlalchemy>=2.0.49", "polars>=1.40.1", "dopt-basics>=0.2.4"]
requires-python = "<3.14,>=3.11" requires-python = "<3.14,>=3.11"
readme = "README.md" readme = "README.md"
license = {text = "LicenseRef-Proprietary"} license = {text = "LicenseRef-Proprietary"}

View File

@ -0,0 +1,3 @@
import wce_crm.env
wce_crm.env.setup()

View File

View File

@ -0,0 +1,78 @@
from __future__ import annotations
from typing import TypedDict, cast
import polars as pl
from wce_crm import db
class CompanyInfo(TypedDict):
ma_id: str
wce_id: str
ma_unternehmensname: str
ma_branche: str
ma_strasse: str
ma_hausnummer: str
ma_plz: str
ma_ort: str
ma_plz_postfach: str
ma_postfach: str
ma_website: str
ma_mail: str
ma_telefonnummer: str
ma_faxnummer: str
ma_ersteintrag_datum: str
ma_aktualisierung_datum: str
ma_aktualisierung_nutzer: str
ma_sollprozess: str
ma_auslaendische_mitarbeiter: str
ma_quelle_information: str
ma_bemerkung: str
ma_kontakt: str
ma_schlagworte: str
ma_archiviert: str
def _transform_for_gui_output(
data: pl.DataFrame,
) -> pl.DataFrame:
q = (
data.lazy()
.with_columns(
pl.col(pl.Datetime).dt.to_string("%d.%m.%Y"),
pl.col(pl.Date).dt.to_string("%d.%m.%Y"),
pl.when(pl.col(pl.Boolean))
.then(pl.lit("Ja"))
.otherwise(pl.lit("Nein"))
.name.keep(),
)
.with_columns(pl.all().cast(pl.String))
)
return q.collect()
def comp_search_choice_mapping() -> dict[str, int]:
# TODO no reload functionality
q = db.df_crm_master.lazy()
counter = pl.int_range(0, pl.len()).over(pl.col.ma_unternehmensname)
q = q.with_columns(
ma_unternehmensname_dedupl=pl.when(counter == 0)
.then(pl.col.ma_unternehmensname)
.otherwise(pl.format("{} ({})", pl.col.ma_unternehmensname, counter))
)
df = q.collect()
return dict(zip(df["ma_unternehmensname_dedupl"], df["ma_id"]))
def comp_search_get_info(
ma_id: int,
) -> CompanyInfo:
df = db.df_crm_master.filter(pl.col.ma_id == ma_id)
if df.height > 1 or df.height == 0:
raise ValueError(f"Größe des zurückgelieferten Datenpakets ungültig: {df.height}")
df = _transform_for_gui_output(df)
return cast(CompanyInfo, df.row(0, named=True))

213
src/wce_crm/db.py Normal file
View File

@ -0,0 +1,213 @@
from __future__ import annotations
import os
import re
from datetime import datetime
from pathlib import Path
import polars as pl
import sqlalchemy as sql
from sqlalchemy import Column, String, Table, TypeDecorator
from wce_crm import types as t
class SafeDateTime(TypeDecorator):
"""Cleans non-standard ISO strings before parsing."""
impl = String # We treat the underlying data as a String first
def process_result_value(self, value, dialect):
if value is None:
return None
# 1. Remove the trailing 'ff' (or any trailing letters)
# 2. Replace comma with dot (SQLAlchemy prefers . over ,)
clean_value = re.sub(r"[a-zA-Z]+$", "", value).replace(",", ".")
try:
return datetime.fromisoformat(clean_value)
except ValueError:
# Fallback if it's still weird
return None
md_kontaktliste = sql.MetaData()
md_crm = sql.MetaData()
ext_kl_unternehmen: sql.Table = Table(
"Unternehmen",
md_kontaktliste,
Column("u_id", sql.Integer, nullable=False, unique=True),
Column("u_zeitstempel_eintrag", sql.DateTime, nullable=False),
Column("u_rechtsform", sql.Text, nullable=False),
Column("u_firmenname", sql.Text, nullable=False),
Column("u_strasse", sql.Text, nullable=False),
Column("u_hausnummer", sql.Text, nullable=False),
Column("u_adresszusatz", sql.Text, nullable=True),
Column("u_plz", sql.Text, nullable=False),
Column("u_ort", sql.Text, nullable=False),
Column("u_postfach", sql.Text, nullable=True),
Column("u_website", sql.Text, nullable=True),
Column("u_anrede", sql.Text, nullable=False),
Column("u_titel", sql.Text, nullable=True),
Column("u_vorname", sql.Text, nullable=False),
Column("u_nachname", sql.Text, nullable=False),
Column("u_funktion", sql.Text, nullable=False),
Column("u_mail", sql.Text, nullable=False),
Column("u_telefon", sql.Text, nullable=False),
Column("u_plz_postfach", sql.Text, nullable=True),
Column("u_einwilligung_inhaber", sql.Boolean, nullable=True),
Column("u_einwilligung_ansprechpartner", sql.Boolean, nullable=True),
Column("u_aktiv", sql.Boolean, nullable=False, default=1),
)
ext_kl_unternehmen_schema: t.PolarsSchema = {
"u_id": pl.UInt64,
"u_zeitstempel_eintrag": pl.Datetime,
"u_rechtsform": pl.String,
"u_firmenname": pl.String,
"u_strasse": pl.String,
"u_hausnummer": pl.String,
"u_adresszusatz": pl.String,
"u_plz": pl.String,
"u_ort": pl.String,
"u_postfach": pl.String,
"u_website": pl.String,
"u_anrede": pl.String,
"u_titel": pl.String,
"u_vorname": pl.String,
"u_nachname": pl.String,
"u_funktion": pl.String,
"u_mail": pl.String,
"u_telefon": pl.String,
"u_plz_postfach": pl.String,
"u_einwilligung_inhaber": pl.Boolean,
"u_einwilligung_ansprechpartner": pl.Boolean,
"u_aktiv": pl.Boolean,
}
def get_ext_kontaktliste(
db_path: Path | None,
) -> pl.DataFrame:
if db_path is None:
ENV_PTH = os.environ.get("DOPT_DB_KONTAKTLISTE", None)
if ENV_PTH is None:
raise ValueError("No database path provided or found as ENV var.")
db_path = Path(ENV_PTH)
if not db_path.exists():
raise FileNotFoundError(f"Database not found under >{db_path}<")
engine = sql.create_engine(f"sqlite:///{db_path}")
stmt = sql.select(ext_kl_unternehmen)
return pl.read_database(stmt, engine, schema_overrides=ext_kl_unternehmen_schema)
df_kontaktliste = get_ext_kontaktliste(None)
ext_crm_master: sql.Table = Table(
"Master",
md_crm,
Column("ma_id", sql.Integer, nullable=False, unique=True),
Column("wce_id", sql.ForeignKey("Nutzer.wce_id")),
Column("ma_unternehmensname", sql.Text, nullable=True),
Column("ma_branche", sql.Text, nullable=True),
Column("ma_strasse", sql.Text, nullable=True),
Column("ma_hausnummer", sql.Text, nullable=True),
Column("ma_plz", sql.Text, nullable=True),
Column("ma_ort", sql.Text, nullable=True),
Column("ma_plz_postfach", sql.Text, nullable=True),
Column("ma_postfach", sql.Text, nullable=True),
Column("ma_website", sql.Text, nullable=True),
Column("ma_mail", sql.Text, nullable=True),
Column("ma_telefonnummer", sql.Text, nullable=True),
Column("ma_faxnummer", sql.Text, nullable=True),
Column("ma_ersteintrag_datum", SafeDateTime, nullable=True),
Column("ma_aktualisierung_datum", SafeDateTime, nullable=True),
Column("ma_aktualisierung_nutzer", sql.Text, nullable=True),
Column("ma_sollprozess", sql.Text, nullable=True),
Column("ma_auslaendische_mitarbeiter", sql.Text, nullable=True),
Column("ma_quelle_information", sql.Text, nullable=True),
Column("ma_bemerkung", sql.Text, nullable=True),
Column("ma_kontakt", sql.Boolean, nullable=True),
Column("ma_schlagworte", sql.Text, nullable=True),
Column("ma_archiviert", sql.Boolean, nullable=True, default=False),
)
ext_crm_master_schema: t.PolarsSchema = {
"ma_id": pl.UInt64,
"wce_id": pl.UInt64,
"ma_unternehmensname": pl.String,
"ma_branche": pl.String,
"ma_strasse": pl.String,
"ma_hausnummer": pl.String,
"ma_plz": pl.String,
"ma_ort": pl.String,
"ma_plz_postfach": pl.String,
"ma_postfach": pl.String,
"ma_website": pl.String,
"ma_mail": pl.String,
"ma_telefonnummer": pl.String,
"ma_faxnummer": pl.String,
"ma_ersteintrag_datum": pl.Datetime,
"ma_aktualisierung_datum": pl.Datetime,
"ma_aktualisierung_nutzer": pl.String,
"ma_sollprozess": pl.String,
"ma_auslaendische_mitarbeiter": pl.String,
"ma_quelle_information": pl.String,
"ma_bemerkung": pl.String,
"ma_kontakt": pl.Boolean,
"ma_schlagworte": pl.String,
"ma_archiviert": pl.Boolean,
}
def get_ext_crm_master(
db_path: Path | None,
) -> pl.DataFrame:
if db_path is None:
ENV_PTH = os.environ.get("DOPT_DB_CRM", None)
if ENV_PTH is None:
raise ValueError("No database path provided or found as ENV var.")
db_path = Path(ENV_PTH)
if not db_path.exists():
raise FileNotFoundError(f"Database not found under >{db_path}<")
engine = sql.create_engine(f"sqlite:///{db_path}")
stmt = sql.select(ext_crm_master)
return pl.read_database(stmt, engine, schema_overrides=ext_crm_master_schema)
df_crm_master = get_ext_crm_master(None)
ext_crm_nutzer: sql.Table = Table(
"Nutzer",
md_crm,
Column("wce_id", sql.Integer, nullable=False, unique=True),
Column("wce_name", sql.Text, nullable=True),
Column("wce_vorname", sql.Text, nullable=True),
Column("wce_kuerzel", sql.Text, nullable=True),
Column("wce_passwort", sql.Text, nullable=True),
Column("wce_angelegt_am", sql.DateTime, nullable=True),
Column("wce_rolle", sql.Text, nullable=True),
Column("wce_angelegt_von", sql.Text, nullable=True),
Column("wce_aktiv", sql.Boolean, nullable=True),
Column("wce_letzter_login", sql.DateTime, nullable=True),
)
ext_crm_nutzer_schema: t.PolarsSchema = {
"wce_id": pl.UInt64,
"wce_name": pl.String,
"wce_vorname": pl.String,
"wce_kuerzel": pl.String,
"wce_passwort": pl.String,
"wce_angelegt_am": pl.Datetime,
"wce_rolle": pl.String,
"wce_angelegt_von": pl.String,
"wce_aktiv": pl.Boolean,
"wce_letzter_login": pl.Datetime,
}

15
src/wce_crm/env.py Normal file
View File

@ -0,0 +1,15 @@
import os
from pathlib import Path
PROJECT_ROOT = Path(__file__).parents[2]
DB_PATH = PROJECT_ROOT / "data/db"
DB_KONTAKTLISTE = DB_PATH / "wce_kontaktliste.db"
assert DB_KONTAKTLISTE.exists()
DB_CRM = DB_PATH / "wce_crm.db"
assert DB_CRM.exists()
def setup():
os.environ["DOPT_DB_KONTAKTLISTE"] = str(DB_KONTAKTLISTE)
os.environ["DOPT_DB_CRM"] = str(DB_CRM)

2
src/wce_crm/env_vars.txt Normal file
View File

@ -0,0 +1,2 @@
DOPT_DB_KONTAKTLISTE: Pfad zur Datenbank der Kontaktliste, falls nicht direkt übergeben (Prototypenphase)
DOPT_DB_CRM: Pfad zur CRM-Datenbank, falls nicht direkt übergeben (Prototypenphase)

9
src/wce_crm/types.py Normal file
View File

@ -0,0 +1,9 @@
from __future__ import annotations
from typing import TYPE_CHECKING, TypeAlias
if TYPE_CHECKING:
import polars as pl
PolarsSchema: TypeAlias = dict[str, type["pl.DataType"]]