xplan-tools 1.12.1__py3-none-any.whl → 1.13.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.
@@ -24,9 +24,6 @@ logger = logging.getLogger(__name__)
24
24
  def repo_factory(
25
25
  datasource: str = "",
26
26
  repo_type: Literal["gml", "jsonfg", "shape", "db"] | None = None,
27
- schema: str | None = None,
28
- srid: int = 25832,
29
- views: bool = False,
30
27
  ) -> "BaseRepository":
31
28
  """Factory method for Repositories.
32
29
 
@@ -111,7 +108,7 @@ def repo_factory(
111
108
  case "db":
112
109
  logger.debug("initializing DB repository")
113
110
  return locate("xplan_tools.interface.db.DBRepository")(
114
- datasource, schema, srid, views
111
+ datasource,
115
112
  )
116
113
  case _:
117
114
  raise ValueError("Unknown datasource")
@@ -3,7 +3,7 @@
3
3
  # import json
4
4
  import logging
5
5
  from pathlib import Path
6
- from typing import Iterable
6
+ from typing import Iterable, Literal
7
7
 
8
8
  from alembic import command, config, script
9
9
  from geoalchemy2 import load_spatialite_gpkg
@@ -32,7 +32,7 @@ from sqlalchemy.orm import sessionmaker
32
32
  from xplan_tools.model import model_factory
33
33
  from xplan_tools.model.base import BaseCollection, BaseFeature
34
34
  from xplan_tools.model.orm import Base, Feature, Geometry, Refs
35
- from xplan_tools.util import check_schema_accessibility
35
+ from xplan_tools.settings import get_settings
36
36
 
37
37
  # from xplan_tools.util import linearize_geom
38
38
  from .base import BaseRepository
@@ -46,9 +46,6 @@ class DBRepository(BaseRepository):
46
46
  def __init__(
47
47
  self,
48
48
  datasource: str = "",
49
- schema: str | None = None,
50
- srid: int = 25832,
51
- with_views: bool = False,
52
49
  ) -> None:
53
50
  """Initializes the DB Repository.
54
51
 
@@ -59,40 +56,89 @@ class DBRepository(BaseRepository):
59
56
 
60
57
  Args:
61
58
  datasource: A connection string which will be transformed to a URL instance.
62
- schema: Schema name for DB repository. If not specified, the default schema is used. Only for PostgreSQL.
63
- srid: the EPSG code for spatial data
64
- with_views: whether to create geometrytype-specific views (postgres only)
65
59
  """
60
+ settings = get_settings()
66
61
  self.datasource: URL = make_url(datasource)
67
62
  self.content = None
68
- self.schema = schema
63
+ self.schema = settings.db_schema
64
+ self.srid = settings.db_srid
69
65
  self.dialect = self.datasource.get_dialect().name
70
66
  self.Session = sessionmaker(bind=self._engine)
71
- # self.session = self.Session()
72
- self.srid = srid
73
- self.with_views = with_views
74
67
 
75
68
  self.alembic_cfg = config.Config()
76
69
  self.alembic_cfg.set_main_option(
77
70
  "script_location", "xplan_tools:model:migrations"
78
71
  )
79
- self.alembic_cfg.set_main_option("srid", str(srid))
80
- if with_views:
81
- self.alembic_cfg.set_main_option("with_views", "1")
82
72
  self.alembic_cfg.set_main_option(
83
73
  "sqlalchemy.url",
84
74
  datasource.replace("gpkg:", "sqlite:").replace(
85
75
  "postgresql:", "postgresql+psycopg:"
86
76
  ),
87
77
  )
88
- if self.schema and self.dialect == "postgresql":
89
- check_schema_accessibility(self._engine, self.schema)
90
- self.alembic_cfg.set_main_option("custom_schema", self.schema)
78
+ self._ensure_repo()
79
+
80
+ def _ensure_repo(self) -> None:
81
+ """Runs initial connection/schema tests and ensures DB revision."""
82
+
83
+ def _check_schema_accessibility(privilege: Literal["USAGE", "CREATE"]) -> None:
84
+ """Raises an exception if the schema does not exist or is not accessible to the current user."""
85
+ # Check if the schema exists and is accessible
86
+ user = conn.execute(text("SELECT current_user")).scalar()
87
+ result = conn.execute(
88
+ text("""
89
+ SELECT has_schema_privilege(:user, :schema, :privilege)
90
+ """),
91
+ {
92
+ "user": user,
93
+ "schema": self.schema,
94
+ "privilege": privilege,
95
+ },
96
+ )
97
+ if not result.scalar():
98
+ raise RuntimeError(
99
+ f"User {user} lacks {privilege} on schema '{self.schema}'"
100
+ )
101
+
102
+ def _check_db_srid() -> None:
103
+ """Raises an exception if the DB SRID differs from the one of the repo."""
104
+ if self.dialect == "geopackage":
105
+ geometry_columns = Table(
106
+ "gpkg_geometry_columns",
107
+ MetaData(),
108
+ Column("table_name"),
109
+ Column("srs_id"),
110
+ )
111
+ stmt = select(geometry_columns.c.srs_id).where(
112
+ geometry_columns.c.table_name == "coretable"
113
+ )
114
+ else:
115
+ geometry_columns = Table(
116
+ "geometry_columns",
117
+ MetaData(),
118
+ Column("f_table_name"),
119
+ Column("srid"),
120
+ )
121
+ stmt = select(geometry_columns.c.srid).where(
122
+ geometry_columns.c.f_table_name == "coretable"
123
+ )
124
+ if self.dialect == "postgresql":
125
+ geometry_columns.append_column(Column("f_table_schema"))
126
+ stmt = stmt.where(
127
+ geometry_columns.c.f_table_schema == (self.schema or "public")
128
+ )
129
+ srid = conn.execute(stmt).scalar_one()
130
+ if srid != self.srid:
131
+ raise RuntimeError(
132
+ f"DB SRID '{srid}' and configured SRID '{self.srid}' must identical"
133
+ )
134
+
91
135
  current_version = script.ScriptDirectory.from_config(
92
136
  self.alembic_cfg
93
137
  ).get_heads()
94
138
  # test for tables and revision
95
139
  with self._engine.connect() as conn:
140
+ if self.schema and self.dialect == "postgresql":
141
+ _check_schema_accessibility("USAGE")
96
142
  inspector = inspect(conn)
97
143
  tables = inspector.get_table_names(schema=self.schema)
98
144
  is_coretable = {"coretable", "refs"}.issubset(set(tables))
@@ -106,10 +152,14 @@ class DBRepository(BaseRepository):
106
152
  db_version = conn.execute(stmt).scalars().all()
107
153
  else:
108
154
  db_version = []
155
+ if db_version:
156
+ _check_db_srid()
109
157
  is_current_version = set(db_version) == set(current_version)
110
158
  if is_current_version:
111
159
  logger.info("Database is at current revision")
112
160
  return
161
+ elif self.schema and self.dialect == "postgresql":
162
+ _check_schema_accessibility("CREATE")
113
163
  # handle schema upgrade or table creation
114
164
  if is_coretable and not db_version:
115
165
  e = RuntimeError("Coretable with no revision found in database")
@@ -140,7 +190,7 @@ class DBRepository(BaseRepository):
140
190
  else:
141
191
  # create tables if it's a fresh file-based DB and set it to current revision
142
192
  logger.info("Creating new database schema")
143
- self.create_tables(self.srid)
193
+ self.create_tables()
144
194
  command.stamp(self.alembic_cfg, "head")
145
195
 
146
196
  @property
@@ -151,8 +201,10 @@ class DBRepository(BaseRepository):
151
201
  else self.datasource
152
202
  )
153
203
  connect_args: dict[str, str] = {}
154
- if self.schema and self.dialect == "postgresql":
155
- connect_args["options"] = f"-csearch_path={self.schema},public"
204
+ if self.dialect == "postgresql":
205
+ connect_args["connect_timeout"] = 5
206
+ if self.schema:
207
+ connect_args["options"] = f"-csearch_path={self.schema},public"
156
208
  engine = create_engine(url, connect_args=connect_args)
157
209
  if self.dialect == "geopackage":
158
210
  listen(engine, "connect", load_spatialite_gpkg)
@@ -315,7 +367,7 @@ class DBRepository(BaseRepository):
315
367
  else:
316
368
  raise ValueError(f"no feature found with id {id}")
317
369
 
318
- def create_tables(self, srid: int) -> None:
370
+ def create_tables(self) -> None:
319
371
  """Creates coretable and related/spatial tables in the database.
320
372
 
321
373
  Args:
@@ -326,7 +378,7 @@ class DBRepository(BaseRepository):
326
378
  def pre_creation(_, conn, **kwargs):
327
379
  if self.dialect == "sqlite":
328
380
  conn.execute(text("SELECT InitSpatialMetaData('EMPTY')"))
329
- conn.execute(text("SELECT InsertEpsgSrid(:srid)"), {"srid": srid})
381
+ conn.execute(text("SELECT InsertEpsgSrid(:srid)"), {"srid": self.srid})
330
382
 
331
383
  @listens_for(Base.metadata, "after_create")
332
384
  def post_creation(_, conn, **kwargs):
@@ -362,7 +414,7 @@ class DBRepository(BaseRepository):
362
414
  )
363
415
  )
364
416
 
365
- logger.debug(f"creating tables with srid {srid}")
417
+ logger.debug(f"creating tables with srid {self.srid}")
366
418
  tables = Base.metadata.sorted_tables
367
419
  if not self.dialect == "geopackage":
368
420
  tables.pop(1)
@@ -370,7 +422,7 @@ class DBRepository(BaseRepository):
370
422
  Column(
371
423
  "geometry",
372
424
  Geometry(
373
- srid=srid,
425
+ srid=self.srid,
374
426
  spatial_index=True,
375
427
  ),
376
428
  nullable=True,
@@ -56,6 +56,13 @@ class GMLRepository(BaseRepository):
56
56
  ),
57
57
  ):
58
58
  return "xtrasse"
59
+ elif re.search(
60
+ "http://www[.]xwaermeplan[.]de/[0-9]/[0-9]",
61
+ self.content.get(
62
+ f"{{{xsi}}}schemaLocation", self.content.nsmap.get(None, "")
63
+ ),
64
+ ):
65
+ return "xwp"
59
66
  elif self.content.nsmap.get(
60
67
  "xplan", self.content.nsmap.get(None, "")
61
68
  ) or re.search(
@@ -73,6 +80,11 @@ class GMLRepository(BaseRepository):
73
80
  if self.appschema == "xtrasse":
74
81
  uri = self.content.nsmap.get("xtrasse", self.content.nsmap.get(None, ""))
75
82
  return uri.split("http://www.xtrasse.de/")[1].replace("/", ".")
83
+ elif self.appschema == "xwp":
84
+ uri = self.content.nsmap.get(
85
+ "xwaermeplan", self.content.nsmap.get(None, "")
86
+ )
87
+ return uri.split("http://www.xwaermeplan.de/")[1].replace("/", ".")
76
88
  else:
77
89
  uri = self.content.nsmap.get("xplan", self.content.nsmap.get(None, ""))
78
90
  if "xplan" not in uri:
@@ -149,6 +161,24 @@ class GMLRepository(BaseRepository):
149
161
  },
150
162
  nsmap=nsmap,
151
163
  )
164
+ elif self.appschema == "xwp":
165
+ nsmap = {
166
+ None: f"http://www.xwaermeplan.de/{self.version.replace('.', '/')}",
167
+ "gml": "http://www.opengis.net/gml/3.2",
168
+ "xml": "http://www.w3.org/XML/1998/namespace",
169
+ "xlink": "http://www.w3.org/1999/xlink",
170
+ "xsi": "http://www.w3.org/2001/XMLSchema-instance",
171
+ "sf": "http://www.opengis.net/ogcapi-features-1/1.0/sf",
172
+ }
173
+
174
+ root = etree.Element(
175
+ "{http://www.opengis.net/ogcapi-features-1/1.0/sf}FeatureCollection",
176
+ attrib={
177
+ "{http://www.w3.org/2001/XMLSchema-instance}schemaLocation": f"{nsmap[None]} https://gitlab.opencode.de/xleitstelle/xwaermeplan/spezifikation/-/raw/main/xsd/waermeplan.xsd {nsmap['sf']} http://schemas.opengis.net/ogcapi/features/part1/1.0/xml/core-sf.xsd {nsmap['gml']} https://schemas.opengis.net/gml/3.2.1/gml.xsd",
178
+ "{http://www.opengis.net/gml/3.2}id": f"GML_{uuid4()}",
179
+ },
180
+ nsmap=nsmap,
181
+ )
152
182
 
153
183
  elif self.appschema == "plu":
154
184
  nsmap = {
@@ -173,12 +203,14 @@ class GMLRepository(BaseRepository):
173
203
  nsmap=nsmap,
174
204
  )
175
205
 
176
- if self.appschema != "xtrasse":
206
+ if self.appschema not in ["xtrasse", "xwp"]:
177
207
  bounds = etree.SubElement(
178
208
  root,
179
- "{http://www.opengis.net/gml/3.2}boundedBy"
180
- if self.appschema == "xplan"
181
- else "{http://www.opengis.net/wfs/2.0}boundedBy",
209
+ (
210
+ "{http://www.opengis.net/gml/3.2}boundedBy"
211
+ if self.appschema == "xplan"
212
+ else "{http://www.opengis.net/wfs/2.0}boundedBy"
213
+ ),
182
214
  )
183
215
 
184
216
  geoms = []
@@ -193,11 +225,15 @@ class GMLRepository(BaseRepository):
193
225
  srs = feature.get_geom_srid()
194
226
  etree.SubElement(
195
227
  root,
196
- "{http://www.opengis.net/gml/3.2}featureMember"
197
- if self.appschema == "xplan"
198
- else "{http://www.opengis.net/ogcapi-features-1/1.0/sf}featureMember"
199
- if self.appschema == "xtrasse"
200
- else "{http://www.opengis.net/wfs/2.0}member",
228
+ (
229
+ "{http://www.opengis.net/gml/3.2}featureMember"
230
+ if self.appschema == "xplan"
231
+ else (
232
+ "{http://www.opengis.net/ogcapi-features-1/1.0/sf}featureMember"
233
+ if self.appschema in ["xtrasse", "xwp"]
234
+ else "{http://www.opengis.net/wfs/2.0}member"
235
+ )
236
+ ),
201
237
  ).append(
202
238
  feature.model_dump_gml(feature_srs=kwargs.get("feature_srs", True))
203
239
  )
@@ -210,7 +246,7 @@ class GMLRepository(BaseRepository):
210
246
  else {"srsName": f"EPSG:{srs}"}
211
247
  )
212
248
 
213
- if self.appschema != "xtrasse":
249
+ if self.appschema not in ["xtrasse", "xwp"]:
214
250
  envelope = etree.SubElement(
215
251
  bounds,
216
252
  "{http://www.opengis.net/gml/3.2}Envelope",
@@ -91,6 +91,8 @@ class JsonFGRepository(BaseRepository):
91
91
  raise ValueError("JSON Schema not found in links")
92
92
  if "https://gitlab.opencode.de/xleitstelle/xtrasse/spezifikation" in uri:
93
93
  return "xtrasse"
94
+ elif "https://gitlab.opencode.de/xleitstelle/xwaermeplan/spezifikation/" in uri:
95
+ return "xwp"
94
96
  elif "https://gitlab.opencode.de/xleitstelle/xplanung/schemas/json" in uri:
95
97
  return "xplan"
96
98
  else:
@@ -107,9 +109,18 @@ class JsonFGRepository(BaseRepository):
107
109
  raise ValueError("JSON Schema not found in links")
108
110
  if self.appschema == "xtrasse":
109
111
  return "2.0"
112
+ elif self.appschema == "xwp":
113
+ return "0.9"
110
114
  return re.search(r"(.*\/)(\d.\d)(\/.*)", uri).group(2)
111
115
 
112
116
  def _collection_template(self, srid: int, featuretype: str | None = None):
117
+ match self.appschema:
118
+ case "xtrasse":
119
+ href = "https://gitlab.opencode.de/xleitstelle/xtrasse/spezifikation/-/raw/main/json/featurecollection.json"
120
+ case "xwp":
121
+ href = f"https://gitlab.opencode.de/xleitstelle/xwaermeplan/spezifikation/-/raw/main/json/{self.version}/featurecollection.json"
122
+ case "xplan":
123
+ href = f"https://gitlab.opencode.de/xleitstelle/xplanung/schemas/json/-/raw/main/{self.version}/featurecollection.json"
113
124
  template = {
114
125
  "type": "FeatureCollection",
115
126
  "featureType": featuretype,
@@ -117,9 +128,7 @@ class JsonFGRepository(BaseRepository):
117
128
  "features": [],
118
129
  "links": [
119
130
  {
120
- "href": "https://gitlab.opencode.de/xleitstelle/xtrasse/spezifikation/-/raw/main/json/featurecollection.json"
121
- if self.appschema == "xtrasse"
122
- else f"https://gitlab.opencode.de/xleitstelle/xplanung/schemas/json/-/raw/main/{self.version}/featurecollection.json",
131
+ "href": href,
123
132
  "rel": "describedby",
124
133
  "type": "application/schema+json",
125
134
  "title": "JSON Schema of this document",
xplan_tools/main.py CHANGED
@@ -13,6 +13,7 @@ from rich.logging import RichHandler
13
13
  from rich.traceback import Traceback
14
14
 
15
15
  from xplan_tools.interface import repo_factory
16
+ from xplan_tools.settings.settings import get_settings
16
17
  from xplan_tools.transform import transformer_factory
17
18
  from xplan_tools.util import MigrationPath, _Versions, serialize_style_rules
18
19
  from xplan_tools.util.validate import xplan_validate
@@ -25,6 +26,8 @@ logger = logging.getLogger(__name__)
25
26
  # don't propagate alembic logs when using CLI
26
27
  logging.getLogger("alembic").propagate = False
27
28
 
29
+ settings = get_settings()
30
+
28
31
  app = typer.Typer(help=f"XPlan-Tools {__version__}")
29
32
  db_app = typer.Typer()
30
33
  app.add_typer(
@@ -335,13 +338,16 @@ def create_schema(
335
338
  ] = 25832,
336
339
  views: Annotated[
337
340
  bool, typer.Option(help="Whether to create views for geometry types.")
338
- ] = False,
341
+ ] = True,
339
342
  schema: Annotated[
340
343
  str | None, typer.Option(help="Whether to specify a schema.")
341
344
  ] = None,
342
345
  ):
343
346
  """Create Coretable schema in the given DB."""
344
- repo_factory(connection_string, schema=schema, srid=srid, views=views)
347
+ settings.db_schema = schema
348
+ settings.db_srid = srid
349
+ settings.db_views = views
350
+ repo_factory(connection_string)
345
351
  console.log("Schema created.")
346
352
 
347
353
 
@@ -355,7 +361,8 @@ def drop_schema(
355
361
  ] = None,
356
362
  ):
357
363
  """Drop Coretable schema in the given DB."""
358
- repo_factory(connection_string, schema=schema).delete_tables()
364
+ settings.db_schema = schema
365
+ repo_factory(connection_string).delete_tables()
359
366
  console.log("Schema dropped.")
360
367
 
361
368
 
@@ -27,14 +27,17 @@ Example:
27
27
  from pydoc import locate
28
28
  from typing import TYPE_CHECKING, Literal, Type
29
29
 
30
+ from deprecated import deprecated
31
+
30
32
  if TYPE_CHECKING:
31
33
  from .base import BaseFeature
32
34
 
33
35
 
36
+ @deprecated(reason="use Appschema.model_factory instead")
34
37
  def model_factory(
35
38
  model_name: str,
36
39
  model_version: str | None,
37
- appschema: Literal["xplan", "xtrasse", "plu", "def"] = "xplan",
40
+ appschema: Literal["xplan", "xtrasse", "xwp", "plu", "def"] = "xplan",
38
41
  ) -> Type["BaseFeature"]:
39
42
  """Factory method for retrieving the corresponding pydantic model representation of a feature class.
40
43
 
@@ -75,6 +78,10 @@ def model_factory(
75
78
  model = locate(
76
79
  f"xplan_tools.model.appschema.xtrasse{model_version.replace('.', '')}.{model_name.replace('_', '')}"
77
80
  )
81
+ case "xwp":
82
+ model = locate(
83
+ f"xplan_tools.model.appschema.xwp{model_version.replace('.', '')}.{model_name.replace('_', '')}"
84
+ )
78
85
 
79
86
  if not model:
80
87
  raise ValueError(
@@ -24,9 +24,14 @@ class GMLAdapter:
24
24
 
25
25
  def parse_property(name, value, index: int | None = None):
26
26
  gml_name = f"{{{ns}}}{name}"
27
-
28
27
  if name == "id":
29
28
  feature.set("{http://www.opengis.net/gml/3.2}id", f"GML_{value}")
29
+ if self.get_appschema() != "plu":
30
+ etree.SubElement(
31
+ feature,
32
+ "{http://www.opengis.net/gml/3.2}identifier",
33
+ attrib={"codeSpace": "urn:uuid"},
34
+ ).text = value
30
35
  return
31
36
  # Patch for vertikaleDifferenzierung being optional with a default value of False instead of None
32
37
  if name == "vertikaleDifferenzierung" and value is False:
@@ -59,9 +64,11 @@ class GMLAdapter:
59
64
  options=[
60
65
  "FORMAT=GML32",
61
66
  f"GMLID=GML_{uuid4()}",
62
- "SRSNAME_FORMAT=OGC_URL"
63
- if self.get_appschema() == "plu"
64
- else "GML3_LONGSRS=NO",
67
+ (
68
+ "SRSNAME_FORMAT=OGC_URL"
69
+ if self.get_appschema() == "plu"
70
+ else "GML3_LONGSRS=NO"
71
+ ),
65
72
  "NAMESPACE_DECL=YES",
66
73
  ]
67
74
  )
@@ -1,5 +1,6 @@
1
1
  # generated from JSON Schema
2
2
 
3
+
3
4
  from __future__ import annotations
4
5
 
5
6
  from datetime import date as date_aliased
@@ -42,86 +43,56 @@ class MultiPoint(GeometryBase):
42
43
  wkt: Annotated[str, Field(pattern="^(MULTIPOINT).*$")]
43
44
 
44
45
 
45
- class XPPunktgeometrie(RootModel[Point | MultiPoint]):
46
- root: Point | MultiPoint
47
-
48
-
49
- class XPLiniengeometrie(RootModel[Line | MultiLine]):
50
- root: Line | MultiLine
51
-
52
-
53
- class XPFlaechengeometrie(RootModel[Polygon | MultiPolygon]):
54
- root: Polygon | MultiPolygon
55
-
56
-
57
46
  class Measure(BaseFeature):
58
- """
59
- Basisklasse für Maße
60
- """
47
+ """Basisklasse für Maße"""
61
48
 
62
49
  value: Annotated[float, Field(description="Wert des Maßes")]
63
50
 
64
51
 
65
52
  class Length(Measure):
66
- """
67
- Angabe einer Länge in Metern
68
- """
53
+ """Angabe einer Länge in Metern"""
69
54
 
70
55
  uom: Annotated[str | None, Field(description="Maßeinheit")] = "m"
71
56
 
72
57
 
73
58
  class Area(Measure):
74
- """
75
- Angabe einer Fläche in Quadratmetern
76
- """
59
+ """Angabe einer Fläche in Quadratmetern"""
77
60
 
78
61
  uom: Annotated[str | None, Field(description="Maßeinheit")] = "m2"
79
62
 
80
63
 
81
64
  class Angle(Measure):
82
- """
83
- Angabe eines Winkels in Grad
84
- """
65
+ """Angabe eines Winkels in Grad"""
85
66
 
86
67
  uom: Annotated[str | None, Field(description="Maßeinheit")] = "grad"
87
68
 
88
69
 
89
70
  class Volume(Measure):
90
- """
91
- Angabe eines Volumens in Kubikmetern
92
- """
71
+ """Angabe eines Volumens in Kubikmetern"""
93
72
 
94
73
  uom: Annotated[str | None, Field(description="Maßeinheit")] = "m3"
95
74
 
96
75
 
97
- class Scale(Measure):
98
- """
99
- Angabe einer Skala in Prozent
100
- """
76
+ class Velocity(Measure):
77
+ """Angabe einer Geschwindigkeit in km/h"""
101
78
 
102
- uom: Annotated[str | None, Field(description="Maßeinheit")] = "vH"
79
+ uom: Annotated[str | None, Field(description="Maßeinheit")] = "km/h"
103
80
 
104
81
 
105
- class Velocity(Measure):
106
- """
107
- Angabe einer Geschwindigkeit in Kilometern pro Stunde
108
- """
82
+ class Scale(Measure):
83
+ """Angabe einer Skala in Prozent"""
109
84
 
110
- uom: Annotated[str | None, Field(description="Maßeinheit")] = "km/h"
85
+ uom: Annotated[str | None, Field(description="Maßeinheit")] = "vH"
111
86
 
112
87
 
113
88
  class GenericMeasure(Measure):
114
- """
115
- Nicht näher konkretisiertes Maß
116
- """
89
+ """Nicht näher konkretisiertes Maß"""
117
90
 
118
- uom: Annotated[str | None, Field(description="Maßeinheit")] = "tbd"
91
+ uom: Annotated[str | None, Field(description="Maßeinheit")] = "unknown"
119
92
 
120
93
 
121
94
  class VoidReasonValue(BaseFeature):
122
- """
123
- Reasons for void values.
124
- """
95
+ """Reasons for void values."""
125
96
 
126
97
  nilReason: Annotated[AnyUrl, Field(description="Reason")]
127
98