xmas-app 0.13.1__py3-none-any.whl → 0.14.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
xmas_app/db.py CHANGED
@@ -1,77 +1,15 @@
1
1
  import logging
2
2
 
3
3
  from nicegui.observables import ObservableSet
4
- from sqlalchemy import select, text
5
- from sqlalchemy.exc import SQLAlchemyError
6
- from sqlalchemy.orm import aliased, selectinload
7
- from xplan_tools.interface.db import DBRepository
4
+ from sqlalchemy import select
5
+ from sqlalchemy.orm import selectinload
8
6
  from xplan_tools.model import model_factory
9
7
  from xplan_tools.model.base import BaseFeature
10
8
  from xplan_tools.model.orm import Feature, Refs
11
9
 
12
10
  from xmas_app.settings import get_mappings, get_settings
13
11
 
14
- logger = logging.getLogger(__name__)
15
-
16
-
17
- def get_db_feature_ids(
18
- repo: DBRepository,
19
- typenames: str | list | None = None,
20
- featuretype_regex: str | None = None,
21
- value_prop: str | None = None,
22
- ) -> dict:
23
- """
24
- Query the database for features matching the provided type(s) or regex, and
25
- return a mapping from feature IDs to label (from `value_prop`) or a fallback string.
26
-
27
- Args:
28
- repo (DBRepository): The database repository instance.
29
- typenames (str | list | None): Single typename or list of typenames to filter on.
30
- featuretype_regex (str | None): Optional regex to match featuretype.
31
- value_prop (str | None): The property to use for the label in the result dict.
32
-
33
- Returns:
34
- dict: Mapping from stringified feature ID to label or fallback.
35
- """
36
- logger.info("Entered db.get_db_feature_ids().")
37
- results = {}
38
- try:
39
- with repo.Session() as session:
40
- stmt = select(Feature)
41
- if typenames:
42
- names_list = [typenames] if isinstance(typenames, str) else typenames
43
- stmt = stmt.where(Feature.featuretype.in_(names_list))
44
- logger.info(f"Filtering for typenames: {names_list}")
45
- if featuretype_regex:
46
- stmt = stmt.where(Feature.featuretype.regexp_match(featuretype_regex))
47
- logger.info(f"Filtering with regex: {featuretype_regex}")
48
- try:
49
- db_result = session.execute(stmt)
50
- features = db_result.unique().scalars().all()
51
- logger.info(f"Found {len(features)} feature(s) in DB.")
52
- except Exception as db_ex:
53
- logger.error(f"Database query failed: {db_ex}", exc_info=True)
54
- return {}
55
-
56
- for feature in features:
57
- label = None
58
- try:
59
- label = (
60
- feature.properties.get(value_prop, None) if value_prop else None
61
- )
62
- except Exception as prop_ex:
63
- logger.warning(
64
- f"Error accessing properties of feature ID {feature.id}: {prop_ex}",
65
- exc_info=True,
66
- )
67
- key = str(feature.id)
68
- value = label if label else f"{feature.featuretype}::{feature.id}"
69
- results[key] = value
70
-
71
- except Exception as ex:
72
- logger.error(f"Failed to get feature IDs: {ex}", exc_info=True)
73
- raise
74
- return results
12
+ logger = logging.getLogger("xmas_app")
75
13
 
76
14
 
77
15
  def get_db_plans() -> list[dict]:
@@ -113,34 +51,11 @@ def get_db_plans() -> list[dict]:
113
51
  return results
114
52
 
115
53
 
116
- def delete_db_association(base_id: str, related_id: str, repo: DBRepository):
117
- try:
118
- with repo.Session() as session:
119
- stmt = text("""
120
- DELETE FROM public.refs
121
- WHERE base_id = :base_id AND related_id = :related_id
122
- """)
123
- try:
124
- result = session.execute(
125
- stmt, {"base_id": base_id, "related_id": related_id}
126
- )
127
- session.commit()
128
- logger.info(
129
- f"Deleted {result.rowcount} record(s) from public.refs with base_id={base_id} and related_id={related_id}"
130
- )
131
- return {"deleted_rows": result.rowcount}
132
- except SQLAlchemyError as db_ex:
133
- session.rollback()
134
- logger.error(f"Database delete failed: {db_ex}", exc_info=True)
135
- return {"error": str(db_ex)}
136
- except Exception as ex:
137
- logger.error(f"Failed to delete association: {ex}", exc_info=True)
138
- raise
139
-
140
-
141
54
  def get_nodes(
142
55
  id: str, features_set: ObservableSet
143
56
  ) -> tuple[dict, str, str, str] | None:
57
+ """Build tree nodes for ui.tree element."""
58
+
144
59
  def build_node(feature: Feature, path: str) -> dict:
145
60
  features_set.add(feature.id)
146
61
  node = {
@@ -280,38 +195,9 @@ def get_ref_candidates(
280
195
  rel_inv = model.get_property_info(rel)["assoc_info"]["reverse"]
281
196
 
282
197
  with get_settings().repo.Session() as session:
283
- # collect all plan features while applying WHERE clauses to filter on the DB side
284
- Plan = aliased(Feature, name="plan")
285
- DirectFeature = aliased(Feature, name="direct_feature")
286
- IndirectFeature = aliased(Feature, name="indirect_feature")
287
-
288
- RefPlanDirect = aliased(Refs, name="ref_plan_direct")
289
- RefDirectIndirect = aliased(Refs, name="ref_direct_indirect")
290
- direct = (
291
- select(DirectFeature)
292
- .select_from(Plan)
293
- .join(RefPlanDirect, RefPlanDirect.base_id == Plan.id)
294
- .join(DirectFeature, DirectFeature.id == RefPlanDirect.related_id)
295
- .where(Plan.id == plan_id)
296
- .where(DirectFeature.featuretype.in_(featuretypes))
297
- .where(DirectFeature.id.not_in(refs))
298
- )
299
-
300
- indirect = (
301
- select(IndirectFeature)
302
- .select_from(Plan)
303
- .join(RefPlanDirect, RefPlanDirect.base_id == Plan.id)
304
- .join(DirectFeature, DirectFeature.id == RefPlanDirect.related_id)
305
- .join(RefDirectIndirect, RefDirectIndirect.base_id == DirectFeature.id)
306
- .join(IndirectFeature, IndirectFeature.id == RefDirectIndirect.related_id)
307
- .where(Plan.id == plan_id)
308
- .where(IndirectFeature.featuretype.in_(featuretypes))
309
- .where(IndirectFeature.id.not_in(refs))
310
- )
311
- stmt = direct.union(indirect)
312
-
313
- candidates = (
314
- session.execute(select(Feature).from_statement(stmt)).scalars().all()
198
+ plan = session.get(Feature, plan_id)
199
+ candidates = plan.related_features(
200
+ session, featuretypes=featuretypes, exclude_ids=refs
315
201
  )
316
202
  return [
317
203
  _feature_to_table_row(feature)
xmas_app/form.py CHANGED
@@ -1112,6 +1112,11 @@ class ModelForm:
1112
1112
  )
1113
1113
  .classes("w-full q-pa-none q-gutter-none")
1114
1114
  .props("square filled dense")
1115
+ .bind_value(
1116
+ bind_obj,
1117
+ key,
1118
+ backward=lambda x: None if not x else x,
1119
+ )
1115
1120
  )
1116
1121
  targets = await run.io_bound(
1117
1122
  self._get_association_targets,
@@ -1132,11 +1137,6 @@ class ModelForm:
1132
1137
  input.set_value(parent_id)
1133
1138
  elif len((keys := list(targets.keys()))) == 1:
1134
1139
  input.set_value(keys[0])
1135
- input.bind_value(
1136
- bind_obj,
1137
- key,
1138
- backward=lambda x: None if not x else x,
1139
- )
1140
1140
  input.bind_enabled_from(
1141
1141
  self,
1142
1142
  "editable",
@@ -1222,3 +1222,4 @@ class ModelForm:
1222
1222
  feature[model_geom] = geom
1223
1223
  self.feature = self._get_bindable_object(self.model)(**feature)
1224
1224
  await self._build_model_form(self.model, self.feature, main_form, preview)
1225
+ self._propagate_data()
xmas_app/main.py CHANGED
@@ -6,10 +6,11 @@ import logging
6
6
  import re
7
7
  from contextlib import asynccontextmanager
8
8
  from functools import partial
9
+ from importlib import metadata
9
10
  from pathlib import Path
10
11
  from tempfile import NamedTemporaryFile, gettempdir
11
- from typing import Any, Literal
12
- from uuid import uuid4
12
+ from typing import Literal
13
+ from uuid import UUID
13
14
 
14
15
  import pydantic
15
16
  import pydantic_core
@@ -17,7 +18,6 @@ from fastapi import APIRouter, Depends, HTTPException, Request, status
17
18
  from nicegui import app, run, ui
18
19
  from nicegui.events import ClickEventArguments, ValueChangeEventArguments
19
20
  from nicegui.observables import ObservableSet
20
- from pydantic import ValidationError
21
21
  from starlette.applications import Starlette
22
22
  from xplan_tools.interface import repo_factory
23
23
  from xplan_tools.interface.db import DBRepository
@@ -30,7 +30,7 @@ from xplan_tools.util import (
30
30
  )
31
31
 
32
32
  from xmas_app.components.buttons import FeatureInteractionButton
33
- from xmas_app.db import get_db_feature_ids, get_db_plans, get_nodes
33
+ from xmas_app.db import get_db_plans, get_nodes
34
34
  from xmas_app.deps.version_guard import enforce_plugin_version
35
35
  from xmas_app.form import ModelForm
36
36
  from xmas_app.models.crud import InsertPayload, UpdatePayload
@@ -39,6 +39,8 @@ from xmas_app.services import crud
39
39
  from xmas_app.settings import get_appschema, get_settings
40
40
  from xmas_app.split_service import PlanSplitService, SplitValidationError
41
41
 
42
+ __version__ = metadata.version("xmas_app")
43
+
42
44
 
43
45
  def _resolve_log_dir() -> Path:
44
46
  if get_settings().app_mode == "prod":
@@ -108,42 +110,19 @@ async def get_model_for_select(
108
110
  return False
109
111
  return True
110
112
 
113
+ options = [
114
+ model.get_name()
115
+ for _, model in inspect.getmembers(
116
+ importlib.import_module(
117
+ f"xplan_tools.model.appschema.{appschema + appschema_version.replace('.', '')}"
118
+ ),
119
+ filter_members,
120
+ )
121
+ ]
111
122
  # update the input element options
112
- model_select.set_options(
113
- [
114
- model.get_name()
115
- for _, model in inspect.getmembers(
116
- importlib.import_module(
117
- f"xplan_tools.model.appschema.{appschema + appschema_version.replace('.', '')}"
118
- ),
119
- filter_members,
120
- )
121
- ]
122
- )
123
-
124
-
125
- @ui.page("/", dependencies=[Depends(enforce_plugin_version)])
126
- def index():
127
- """
128
- Render the main index page of the XPlan-GUI application.
129
- Args:
130
- request (Request): The incoming HTTP request object (FastAPI).
131
- """
132
- # print(request.headers)
133
- ui.colors(primary="rgb(157 157 156)", secondary="rgb(57 92 127)")
134
- with ui.header():
135
- ui.label("XPlan-GUI")
136
- ui.button(
137
- "Neues Objekt", on_click=lambda: ui.navigate.to(f"/feature/{str(uuid4())}")
138
- )
139
- ui.select(
140
- dict(
141
- sorted(get_db_feature_ids(get_settings().repo).items(), key=lambda x: x[1])
142
- ),
143
- label="Feature auswählen",
144
- with_input=True,
145
- on_change=lambda x: ui.navigate.to(f"/feature/{x.value}"),
146
- )
123
+ model_select.set_options(options)
124
+ if len(options) == 1:
125
+ model_select.set_value(options[0])
147
126
 
148
127
 
149
128
  @ui.page(
@@ -153,15 +132,21 @@ def index():
153
132
  )
154
133
  async def plan_tree(
155
134
  request: Request,
156
- id: str,
135
+ id: UUID,
157
136
  ):
137
+ id = str(id)
138
+
158
139
  async def update_nodes():
159
- result = await get_nodes(feature)
140
+ result = await run.io_bound(get_nodes, id, app.storage.client["features"])
160
141
  if result:
161
142
  nodes, *_ = result
162
- nodes[0]["children"] = nodes
143
+ app.storage.client["nodes"] = nodes
163
144
  tree.update()
164
145
 
146
+ ui.colors(primary="rgb(157 157 156)", secondary="rgb(57 92 127)")
147
+ ui.button.default_props("flat square no-caps")
148
+ ui.button_group.default_props("flat square no-caps")
149
+
165
150
  app.storage.client["user_agent"] = request.headers.get("user-agent", None)
166
151
  qgis = app.storage.client["user_agent"].startswith(get_settings().qgis_plugin_name)
167
152
  if qgis:
@@ -182,21 +167,6 @@ async def plan_tree(
182
167
  ui.separator()
183
168
  action_dialog = ui.dialog().on("hide", lambda e: e.sender.clear())
184
169
 
185
- ui.colors(primary="rgb(157 157 156)", secondary="rgb(57 92 127)")
186
- ui.button.default_props("flat square no-caps")
187
- ui.button_group.default_props("flat square no-caps")
188
-
189
- # # --- Create the dialog placeholder once ---
190
- # with ui.dialog() as confirm_removeAssociation_dialog, ui.card():
191
- # removeAssociation_label = (
192
- # ui.label()
193
- # ) # placeholder text we can update dynamically
194
- # with ui.row().classes("justify-end w-full"):
195
- # cancel_removeAssociation_button = ui.button(
196
- # "Abbrechen", on_click=confirm_removeAssociation_dialog.close
197
- # )
198
- # removeAssociation_confirm_button = ui.button("Aufheben", color="red")
199
-
200
170
  # --- Create the dialog placeholder once ---
201
171
  with ui.dialog() as confirm_deleteObject_dialog, ui.card(align_items="stretch"):
202
172
  label = ui.label(
@@ -212,16 +182,6 @@ async def plan_tree(
212
182
  confirm_button.on("click", partial(delete_feature, target))
213
183
  confirm_deleteObject_dialog.open()
214
184
 
215
- # # --- Function to open the dialog dynamically ---
216
- # def confirm_delete_association(origin, target):
217
- # removeAssociation_label.text = f"Möchten Sie diese Beziehung wirklich aufheben?"
218
- # removeAssociation_confirm_button.on(
219
- # "click",
220
- # lambda: remove_association(origin, target),
221
- # confirm_removeAssociation_dialog.close(),
222
- # )
223
- # confirm_removeAssociation_dialog.open()
224
-
225
185
  async def show_menu(e: ValueChangeEventArguments):
226
186
  action_dialog.clear()
227
187
  if e.value:
@@ -377,9 +337,6 @@ async def plan_tree(
377
337
  def update_notification(e):
378
338
  notification.message = f"{len(e.sender)} Features geladen"
379
339
 
380
- if not id:
381
- return
382
-
383
340
  await ui.context.client.connected()
384
341
  logger.debug("Feature called by ID:")
385
342
  logger.debug(id)
@@ -398,8 +355,9 @@ async def plan_tree(
398
355
  nodes, featuretype, id, appschema = result
399
356
  else:
400
357
  return
358
+ app.storage.client["nodes"] = nodes
401
359
  tree = ui.tree(
402
- nodes,
360
+ app.storage.client["nodes"],
403
361
  on_select=show_menu,
404
362
  tick_strategy=None,
405
363
  ).props("accordion no-transition")
@@ -760,13 +718,13 @@ async def plans(
760
718
  @ui.page("/feature/{id}", dependencies=[Depends(enforce_plugin_version)])
761
719
  async def feature(
762
720
  request: Request,
763
- id: str,
764
- planId: str | None = None,
765
- parentId: str | None = None,
721
+ id: UUID,
722
+ planId: UUID | None = None,
723
+ parentId: UUID | None = None,
766
724
  featureType: str | None = None,
767
725
  featuretypeRegex: str | None = None,
768
- appschema: str | None = None,
769
- version: str | None = None,
726
+ appschema: str = get_settings().appschema,
727
+ version: str = get_settings().appschema_version,
770
728
  ):
771
729
  """
772
730
  Render and manage a single feature view for the XPlan-GUI application.
@@ -797,8 +755,7 @@ async def feature(
797
755
  # form.feature.update(data)
798
756
  # form._get_art_options()
799
757
 
800
- appschema = appschema or get_settings().appschema
801
- version = version or get_settings().appschema_version
758
+ id = str(id)
802
759
 
803
760
  async def get_qgis_feature():
804
761
  return await ui.run_javascript(
@@ -810,20 +767,6 @@ async def feature(
810
767
  timeout=3,
811
768
  )
812
769
 
813
- def validate_feature():
814
- form: ModelForm = app.storage.client["form"]
815
- if model_instance := form.model_instance:
816
- feature_data = model_instance.model_dump(
817
- mode="json",
818
- exclude_none=True,
819
- exclude={form.model.get_geom_field()},
820
- ) | {"featuretype": form.model.get_name()}
821
- ui.run_javascript(f"""new QWebChannel(qt.webChannelTransport, function (channel) {{
822
- channel.objects.handler.receive_feature({feature_data})
823
- }});""")
824
- else:
825
- form.radio_filter.set_value("mandatory")
826
-
827
770
  async def add_form(
828
771
  feature_type: str,
829
772
  feature: dict,
@@ -885,15 +828,14 @@ async def feature(
885
828
 
886
829
  ui.colors(primary="rgb(157 157 156)", secondary="rgb(57 92 127)")
887
830
  app.storage.client["form"] = None
888
- app.storage.client["plan_id"] = planId
889
- app.storage.client["parent_id"] = parentId
831
+ app.storage.client["plan_id"] = str(planId) if planId else None
832
+ app.storage.client["parent_id"] = str(parentId) if parentId else None
890
833
  app.storage.client["user_agent"] = request.headers.get("user-agent", None)
891
834
 
892
835
  qgis = app.storage.client["user_agent"].startswith(get_settings().qgis_plugin_name)
893
836
 
894
837
  if qgis:
895
838
  ui.add_head_html('<script src="qrc:///qtwebchannel/qwebchannel.js"></script>')
896
- ui.on("validateFeature", validate_feature)
897
839
 
898
840
  try:
899
841
  model = get_settings().repo.get(id)
@@ -982,194 +924,22 @@ async def feature(
982
924
  await add_form(feature_type, feature)
983
925
 
984
926
 
985
- @ui.page("/feature/{id}/associations", dependencies=[Depends(enforce_plugin_version)])
986
- def get_associations(
987
- request: Request,
988
- id: str,
989
- # wkbType: str | None = None,
990
- # planId: str | None = None,
991
- # parentId: str | None = None,
992
- # editable: bool = True,
993
- # featureType: str | None = None,
994
- # featuretypeRegex: str | None = None,
995
- appschema: str = get_settings().appschema,
996
- version: str = get_settings().appschema_version,
997
- ):
998
- """
999
- Render the associations view for a given feature in the XPlan‑GUI application.
1000
-
1001
- Args:
1002
- request (Request): Incoming FastAPI HTTP request object.
1003
- id (str): Identifier or UUID of the feature whose associations will be displayed.
1004
- appschema (str): Application schema name (defaults from settings).
1005
- version (str): Schema version string (defaults from settings).
1006
- """
1007
-
1008
- def transfer_feature_data():
1009
- ui.run_javascript(f"""new QWebChannel(qt.webChannelTransport, function (channel) {{
1010
- channel.objects.handler.handle_feature_creation({feature_data})
1011
- }});""")
1012
-
1013
- def handle_assoc_select(e: ValueChangeEventArguments):
1014
- stepper.next()
1015
- feature_data["source"] = {
1016
- "property": e.value,
1017
- "value": feature[e.value],
1018
- "list": model.get_property_info(e.value)["list"],
1019
- }
1020
- prop_info = model.get_property_info(e.value)
1021
- types = prop_info["typename"]
1022
- type_select.set_options(types if isinstance(types, list) else [types])
1023
- if isinstance(types, str):
1024
- type_select.set_value(types)
1025
-
1026
- def handle_geom_select(geom_regex: str):
1027
- feature_data["geom_regex"] = geom_regex
1028
- transfer_feature_data()
1029
-
1030
- def handle_type_select(e: ValueChangeEventArguments):
1031
- featuretype = e.value
1032
- feature_data["featuretype"] = featuretype
1033
- source_prop = prop_select.value
1034
- target_model = model_factory(featuretype, version, appschema)
1035
- if (assoc_info := model.get_property_info(source_prop)["assoc_info"]) and (
1036
- target_prop := assoc_info["reverse"]
1037
- ):
1038
- target_prop_info = target_model.get_property_info(target_prop)
1039
- feature_data[target_prop] = [id] if target_prop_info["list"] else id
1040
- geom_types = target_model.get_geom_types()
1041
- if not geom_types:
1042
- transfer_feature_data()
1043
- else:
1044
- geom_required = not target_model.get_property_info(
1045
- target_model.get_geom_field()
1046
- )["nullable"]
1047
- type_regex_mapping = {
1048
- geom_type.get_name(): geom_type.model_fields["wkt"].metadata[0].pattern
1049
- for geom_type in geom_types
1050
- }
1051
- if not geom_required:
1052
- type_regex_mapping["Keine Geometrie"] = "NOGEOMETRY"
1053
- if len(geom_types) == 1 and geom_required:
1054
- feature_data["geom_regex"] = list(type_regex_mapping.values())[0]
1055
- transfer_feature_data()
1056
- else:
1057
- with stepper:
1058
- with ui.step("Geometrieart auswählen") as geom_step:
1059
- ui.label("Geometrieart des neuen Features auswählen")
1060
- ui.radio(
1061
- {
1062
- geom_type: geom_type.replace("PolygonObject", "Fläche")
1063
- .replace("PointObject", "Punkt")
1064
- .replace("LineObject", "Linie")
1065
- for geom_type in type_regex_mapping.keys()
1066
- },
1067
- on_change=lambda e: handle_geom_select(
1068
- type_regex_mapping[e.value]
1069
- ),
1070
- ).props("inline")
1071
- stepper.set_value(geom_step)
1072
-
1073
- ui.colors(primary="rgb(157 157 156)", secondary="rgb(57 92 127)")
1074
- ui.button.default_props("flat square no-caps")
1075
- ui.dropdown_button.default_props("flat square no-caps")
1076
- ui.select.default_props("square filled dense").default_classes("w-full")
1077
-
1078
- app.storage.client["user_agent"] = request.headers.get("user-agent", None)
1079
-
1080
- qgis = app.storage.client["user_agent"].startswith(get_settings().qgis_plugin_name)
1081
-
1082
- feature_data = {}
1083
-
1084
- if qgis:
1085
- ui.add_head_html('<script src="qrc:///qtwebchannel/qwebchannel.js"></script>')
1086
-
1087
- try:
1088
- model = get_settings().repo.get(id)
1089
- # feature_type = model.get_name()
1090
- feature = model.model_dump(mode="json")
1091
- except ValueError:
1092
- ui.label("Feature nicht gefunden")
1093
- return
1094
- assocs = model.get_associations()
1095
- if not assocs:
1096
- ui.label("Feature verfügt über keine Assoziationen")
1097
- return
1098
- with ui.stepper().props("vertical flat").classes("w-full") as stepper:
1099
- with ui.step("Attribut auswählen"):
1100
- ui.label(
1101
- "Assoziationsattribut, für das ein neues Feature erzeugt werden soll, auswählen"
1102
- )
1103
- with ui.stepper_navigation():
1104
- prop_select = ui.select(
1105
- assocs, on_change=handle_assoc_select, with_input=True
1106
- )
1107
- with ui.step("Featuretype auswählen"):
1108
- ui.label("Featuretype des neuen Features auswählen")
1109
- with ui.stepper_navigation():
1110
- type_select = ui.select(
1111
- [], on_change=handle_type_select, with_input=True
1112
- )
1113
- # with ui.step("Bake"):
1114
- # ui.label("Bake for 20 minutes")
1115
- # with ui.stepper_navigation():
1116
- # ui.button("Done", on_click=lambda: ui.notify("Yay!", type="positive"))
1117
- # ui.button("Back", on_click=stepper.previous).props("flat")
1118
-
1119
-
1120
927
  @app.get("/health_check")
1121
- def health_check() -> str:
1122
- """
1123
- Health check endpoint to verify that the service is running.
928
+ def health_check() -> dict:
929
+ """Health check endpoint to verify that the service is running.
1124
930
 
1125
931
  Returns:
1126
- str: A simple "OK" response indicating the application is healthy.
1127
- """
1128
- return "OK"
1129
-
1130
-
1131
- # TODO replace with QWebChannel funcionality
1132
- @app.post("/validate/{featuretype}")
1133
- def validate_featuretype(
1134
- featuretype: str,
1135
- featureData: dict[str, Any],
1136
- appschema: str = get_settings().appschema,
1137
- version: str = get_settings().appschema_version,
1138
- ) -> str:
1139
- """
1140
- Validate feature data against the model.
1141
-
1142
- Args:
1143
- featuretype (str): The type of the feature to validate (path parameter).
1144
- featureData (dict[str, Any]): Raw feature data to be validated.
1145
- appschema (str): Application schema name (default from settings).
1146
- version (str): Schema version string (default from settings).
932
+ dict: A simple dictionary with the app's name and version.
1147
933
  """
1148
- # print(featureData)
1149
- featureData.pop("featuretype", None)
1150
- try:
1151
- model = model_factory(featuretype, version, appschema)
1152
- if (geom_field := model.get_geom_field()) and (
1153
- geom := featureData.pop("geometry", None)
1154
- ):
1155
- featureData[geom_field] = geom
1156
- model.model_validate(featureData)
1157
- except ValidationError as e:
1158
- raise HTTPException(
1159
- status_code=400,
1160
- detail={
1161
- "errors": e.errors(),
1162
- },
1163
- )
1164
- return "OK"
934
+ return {"name": "XMAS-App", "version": __version__}
1165
935
 
1166
936
 
1167
937
  @app.post(
1168
938
  "/insert-features", status_code=201, dependencies=[Depends(enforce_plugin_version)]
1169
939
  )
1170
- async def insert_features(payload: InsertPayload, planId: str):
940
+ async def insert_features(payload: InsertPayload, planId: UUID):
1171
941
  """Insert a number of features."""
1172
- await crud.create(payload, planId)
942
+ await crud.create(payload, str(planId))
1173
943
 
1174
944
 
1175
945
  @app.post("/update-features", dependencies=[Depends(enforce_plugin_version)])
xmas_app/settings.py CHANGED
@@ -63,7 +63,7 @@ class Settings(BaseSettings):
63
63
  "https://registry.gdi-de.org/codelist/de.xleitstelle.xplanung"
64
64
  )
65
65
  qgis_plugin_name: str = "XMAS-Plugin"
66
- qgis_plugin_min_version: _VersionPydanticAnnotation = Version(major=0, minor=15)
66
+ qgis_plugin_min_version: _VersionPydanticAnnotation = Version(major=0, minor=16)
67
67
  mappings_toml: str = str(Path(__file__).parent / "resources/mappings.toml")
68
68
 
69
69
  model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xmas-app
3
- Version: 0.13.1
3
+ Version: 0.14.0
4
4
  Summary: The XLeitstelle model-driven application schema app.
5
5
  License: EUPL-1.2-or-later
6
6
  License-File: LICENSE
@@ -15,7 +15,7 @@ Classifier: Programming Language :: Python :: 3
15
15
  Requires-Dist: nicegui (>=2.20,<3.0)
16
16
  Requires-Dist: pydantic-extra-types[semver]
17
17
  Requires-Dist: pydantic-settings (>=2.0,<3.0)
18
- Requires-Dist: xplan-tools (>=1.11.1,<1.12.0)
18
+ Requires-Dist: xplan-tools (>=1.12.1,<1.13.0)
19
19
  Project-URL: Homepage, https://gitlab.opencode.de/xleitstelle/xmas-app
20
20
  Project-URL: Issues, https://gitlab.opencode.de/xleitstelle/xmas-app/-/issues
21
21
  Description-Content-Type: text/markdown
@@ -2,11 +2,11 @@ xmas_app/components/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSu
2
2
  xmas_app/components/association.py,sha256=xZUP-_S8IOmWEx_vWf5eipDjGWOSELdc_TEov8x-R9w,10270
3
3
  xmas_app/components/buttons.py,sha256=siwpqDFKcXxEMdaOglzgYeeuWKbiVLYW8BHi0Wx0HGk,2255
4
4
  xmas_app/components/label.py,sha256=WkqcHK4sGRFENiR_afrrkCDMGq_AZAT63zbN1GSpgv0,424
5
- xmas_app/db.py,sha256=bKzdCGW3yw-Nkf-wZAhXtrwDpRlLs0tJY5WhAA6xVoQ,12148
5
+ xmas_app/db.py,sha256=J-D7FjobO3x8wvBfJdN9ZXfztLmaoN9ZopTNDflWZPk,7235
6
6
  xmas_app/db_uow.py,sha256=usvqo1bO4dQnMbiub5PyQAAgu8Af-HYAJTsFNYS3Le0,761
7
7
  xmas_app/deps/version_guard.py,sha256=ZcTtReXLsT-_dpQyELkajU2HbLvqIJgeU16pMHFAMyc,928
8
- xmas_app/form.py,sha256=hXqtEW_xuC0LdlpviuzVXjX-bg9SJQTLbLD1n9Xq_T8,58993
9
- xmas_app/main.py,sha256=9yOGyMPz85ohP6CYuN90MmHf5EU0ZuqMzPEd1e1x4OU,52327
8
+ xmas_app/form.py,sha256=D1GQy77QBqmFUjHtSzEuUqwLvSxRq0kLWddx0jPXoRk,59039
9
+ xmas_app/main.py,sha256=Eqfh3k2B5R45hMbVcTBYfsqgQeVyeGYxLxE4QUcvb4E,43121
10
10
  xmas_app/models/crud.py,sha256=Rtz-v2FpMM4AcTc_wcye_XjZtssTydgtSQcKuIK1yiM,418
11
11
  xmas_app/models/mappings.py,sha256=SwCbo_TQKiTDUy5-xkNEEIB3J7VFh-VsDbR2_0wyv0Q,213
12
12
  xmas_app/pygeoapi/config.yaml,sha256=lmb08zXk8-dB0sh_4EQ1enVBIqgBI-v6QcoIiHyH3N8,7992
@@ -15,12 +15,12 @@ xmas_app/pygeoapi/provider.py,sha256=uKHK_Hsqale1C2pNRKYpyka9GnNHYvGBmbzfcSi05NE
15
15
  xmas_app/resources/mappings.toml,sha256=VbDrooIdNmJOGLJtdg1Kh1bo_-DyUT7JL7aQMrBhRMU,97
16
16
  xmas_app/schema.py,sha256=3P-i14caMqydGsxNw6SOAcanEg1Qr4qxqciAow1aCUM,1507
17
17
  xmas_app/services/crud.py,sha256=lxH9kFIuOjaM1gEuWb9fbZ4a_hD2PI1_gjUwnfChD5g,4305
18
- xmas_app/settings.py,sha256=I4Q5JQRa0sEIBNX2Q8s3v_cr-bX0UiOjspCQ7_ThWCM,3419
18
+ xmas_app/settings.py,sha256=Fb7KRnOSND1n9IhRwgnOt5txwOCpgaUSsFBlwNhrtgg,3419
19
19
  xmas_app/split_service.py,sha256=Ag_Ix2vMgT04OsQ0DIGK7WFKQlbktmmTqWlESO2ZgVs,26701
20
20
  xmas_app/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
21
  xmas_app/util/codelist.py,sha256=dUn7mOgTmV9Zi8A9iePyOyByMIe0Cf3ERn-n4eHrzM8,1961
22
- xmas_app-0.13.1.dist-info/METADATA,sha256=BcBSxfz_x6u-lnbXPIqPPJSCTURt8KxkJbyZ3l-HHFA,5323
23
- xmas_app-0.13.1.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
24
- xmas_app-0.13.1.dist-info/entry_points.txt,sha256=uve2b0hgPbVfiD047-hOI6_dD97ZytvCPWk7HsN9Xng,53
25
- xmas_app-0.13.1.dist-info/licenses/LICENSE,sha256=b8nnCcy_4Nd_v_okJ6mDKCvi64jkexzbSfIag7TR5mU,13827
26
- xmas_app-0.13.1.dist-info/RECORD,,
22
+ xmas_app-0.14.0.dist-info/METADATA,sha256=7kqqKNFmy5lhqdfyo85IvLd6h1SVJJVJ9QbtTAkVkVs,5323
23
+ xmas_app-0.14.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
24
+ xmas_app-0.14.0.dist-info/entry_points.txt,sha256=uve2b0hgPbVfiD047-hOI6_dD97ZytvCPWk7HsN9Xng,53
25
+ xmas_app-0.14.0.dist-info/licenses/LICENSE,sha256=b8nnCcy_4Nd_v_okJ6mDKCvi64jkexzbSfIag7TR5mU,13827
26
+ xmas_app-0.14.0.dist-info/RECORD,,