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.
- xplan_tools/interface/__init__.py +1 -4
- xplan_tools/interface/db.py +77 -25
- xplan_tools/interface/gml.py +46 -10
- xplan_tools/interface/jsonfg.py +12 -3
- xplan_tools/main.py +10 -3
- xplan_tools/model/__init__.py +8 -1
- xplan_tools/model/adapters/gml.py +11 -4
- xplan_tools/model/appschema/definitions.py +15 -44
- xplan_tools/model/appschema/xplan41.py +493 -722
- xplan_tools/model/appschema/xplan54.py +521 -734
- xplan_tools/model/appschema/xplan60.py +528 -752
- xplan_tools/model/appschema/xplan61.py +541 -771
- xplan_tools/model/appschema/xtrasse20.py +331 -446
- xplan_tools/model/appschema/xwp09.py +5591 -0
- xplan_tools/model/base.py +191 -1
- xplan_tools/model/migrations/env.py +3 -2
- xplan_tools/model/migrations/versions/3c3445a58565_base_schema.py +8 -6
- xplan_tools/model/migrations/versions/f8b74c08ec07_add_refs_indexes_ensure_polygon_ccw.py +5 -1
- xplan_tools/model/orm.py +12 -18
- xplan_tools/settings/__init__.py +3 -0
- xplan_tools/settings/settings.py +23 -0
- xplan_tools/util/__init__.py +0 -24
- {xplan_tools-1.12.1.dist-info → xplan_tools-1.13.0.dist-info}/METADATA +7 -4
- {xplan_tools-1.12.1.dist-info → xplan_tools-1.13.0.dist-info}/RECORD +27 -24
- {xplan_tools-1.12.1.dist-info → xplan_tools-1.13.0.dist-info}/WHEEL +1 -1
- {xplan_tools-1.12.1.dist-info → xplan_tools-1.13.0.dist-info}/entry_points.txt +0 -0
- {xplan_tools-1.12.1.dist-info → xplan_tools-1.13.0.dist-info}/licenses/LICENSE.md +0 -0
xplan_tools/model/base.py
CHANGED
|
@@ -6,9 +6,13 @@ The classes provide some utility, (de-)serialization and/or validation methods t
|
|
|
6
6
|
import datetime
|
|
7
7
|
import logging
|
|
8
8
|
import re
|
|
9
|
-
from
|
|
9
|
+
from importlib import import_module
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from types import ModuleType, NoneType
|
|
10
12
|
from typing import (
|
|
13
|
+
Annotated,
|
|
11
14
|
Any,
|
|
15
|
+
ClassVar,
|
|
12
16
|
Iterator,
|
|
13
17
|
Literal,
|
|
14
18
|
Optional,
|
|
@@ -26,6 +30,8 @@ from pydantic import (
|
|
|
26
30
|
AnyUrl,
|
|
27
31
|
BaseModel,
|
|
28
32
|
ConfigDict,
|
|
33
|
+
Field,
|
|
34
|
+
RootModel,
|
|
29
35
|
SerializationInfo,
|
|
30
36
|
SerializeAsAny,
|
|
31
37
|
ValidationInfo,
|
|
@@ -34,6 +40,8 @@ from pydantic import (
|
|
|
34
40
|
field_validator,
|
|
35
41
|
model_validator,
|
|
36
42
|
)
|
|
43
|
+
from pydantic_extra_types.semantic_version import SemanticVersion
|
|
44
|
+
from semver import Version
|
|
37
45
|
|
|
38
46
|
from xplan_tools.model import model_factory
|
|
39
47
|
from xplan_tools.model.adapters import CoretableAdapter, GMLAdapter, JsonFGAdapter
|
|
@@ -50,6 +58,183 @@ ogr.UseExceptions()
|
|
|
50
58
|
|
|
51
59
|
logger = logging.getLogger(__name__)
|
|
52
60
|
|
|
61
|
+
APPSCHEMA_MODULE_NAMES: list[str] = [
|
|
62
|
+
file.stem for file in (Path(__file__).parent / "appschema").glob("x*.py")
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Appschema(BaseModel):
|
|
67
|
+
"""Models a supported appschema.
|
|
68
|
+
|
|
69
|
+
Used to access metadata like name, version and description for an appschema.
|
|
70
|
+
|
|
71
|
+
During instantion, validation if an appschema is supported occurs, based on the modules
|
|
72
|
+
in `appschema` subdirectory.
|
|
73
|
+
|
|
74
|
+
The instance stores a reference to the appschema's module, which is used in the
|
|
75
|
+
model_factory method the retrieve appschema classes.
|
|
76
|
+
|
|
77
|
+
Usage example:
|
|
78
|
+
```
|
|
79
|
+
# get a featuretype from appschema
|
|
80
|
+
appschema = Appschema.from_prefix(prefix="xplan", version="6.0")
|
|
81
|
+
plan = appschema.model_factory(name="BP_Plan")
|
|
82
|
+
# show list of supported appschemas
|
|
83
|
+
Appschema.supported_appschemas()
|
|
84
|
+
```
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
88
|
+
|
|
89
|
+
_supported_appschemas: ClassVar[list["Appschema"] | None] = None
|
|
90
|
+
|
|
91
|
+
module: Annotated[ModuleType, Field(repr=False)]
|
|
92
|
+
full_name: Annotated[
|
|
93
|
+
str,
|
|
94
|
+
Field(
|
|
95
|
+
description="The full name of the appschema as defined in the UML model."
|
|
96
|
+
),
|
|
97
|
+
]
|
|
98
|
+
full_version: Annotated[
|
|
99
|
+
SemanticVersion,
|
|
100
|
+
Field(description="The semantic version of the appschema."),
|
|
101
|
+
]
|
|
102
|
+
namespace_uri: Annotated[
|
|
103
|
+
AnyUrl,
|
|
104
|
+
Field(description="The namespace URI of the appschema."),
|
|
105
|
+
]
|
|
106
|
+
prefix: Annotated[
|
|
107
|
+
str,
|
|
108
|
+
Field(description="The (namespace) prefix or shortname of the appschema."),
|
|
109
|
+
]
|
|
110
|
+
description: Annotated[
|
|
111
|
+
str | None,
|
|
112
|
+
Field(
|
|
113
|
+
description="Description of the appschema, if defined.",
|
|
114
|
+
repr=False,
|
|
115
|
+
),
|
|
116
|
+
] = None
|
|
117
|
+
|
|
118
|
+
@field_validator("full_version", mode="before")
|
|
119
|
+
@classmethod
|
|
120
|
+
def _coerce_version(cls, v: Any):
|
|
121
|
+
"""Attempts to convert a version string to a full semantic version."""
|
|
122
|
+
if isinstance(v, str):
|
|
123
|
+
return Version.parse(v, optional_minor_and_patch=True)
|
|
124
|
+
return v
|
|
125
|
+
|
|
126
|
+
def model_factory(self, name: str) -> type["BaseFeature"]:
|
|
127
|
+
"""Factory method for retrieving the corresponding pydantic model representation of a feature class.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
name: name of the feature class
|
|
131
|
+
|
|
132
|
+
Raises:
|
|
133
|
+
ValueError: requested FeatureType not found
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
BaseFeature: The concrete feature class inheriting from BaseFeature.
|
|
137
|
+
"""
|
|
138
|
+
try:
|
|
139
|
+
return getattr(self.module, name.replace("_", ""))
|
|
140
|
+
except AttributeError:
|
|
141
|
+
raise ValueError(f"featuretype {name!r} not found for appschema {self!r}")
|
|
142
|
+
|
|
143
|
+
@classmethod
|
|
144
|
+
def supported_appschemas(cls) -> list["Appschema"]:
|
|
145
|
+
"""Return a list of currently supported appschemas."""
|
|
146
|
+
if not cls._supported_appschemas:
|
|
147
|
+
cls._supported_appschemas = [
|
|
148
|
+
cls.from_module(f"{__package__}.appschema.{module_name}")
|
|
149
|
+
for module_name in APPSCHEMA_MODULE_NAMES
|
|
150
|
+
]
|
|
151
|
+
return cls._supported_appschemas
|
|
152
|
+
|
|
153
|
+
@classmethod
|
|
154
|
+
def from_prefix(cls, prefix: str, version: str) -> "Appschema":
|
|
155
|
+
"""Builds an Appschema instance from the appschema's prefix and version.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
prefix: the namespace prefix of the appschema, e.g. `xplan` or `xtrasse`
|
|
159
|
+
version: the version of the appschema; major and minor are required
|
|
160
|
+
"""
|
|
161
|
+
sem_version = Version.parse(version, optional_minor_and_patch=True)
|
|
162
|
+
versions = []
|
|
163
|
+
candidates = []
|
|
164
|
+
for appschema in filter(
|
|
165
|
+
lambda appschema: appschema.prefix == prefix, cls.supported_appschemas()
|
|
166
|
+
):
|
|
167
|
+
# if exact match, return
|
|
168
|
+
if (
|
|
169
|
+
sem_version.major == appschema.full_version.major
|
|
170
|
+
and sem_version.minor == appschema.full_version.minor
|
|
171
|
+
):
|
|
172
|
+
return appschema
|
|
173
|
+
# if versions are compatible (modules minor version higher or equal), add to candidates
|
|
174
|
+
elif sem_version.is_compatible(appschema.full_version):
|
|
175
|
+
candidates.append(appschema)
|
|
176
|
+
else:
|
|
177
|
+
versions.append(
|
|
178
|
+
f"{appschema.full_version.major}.{appschema.full_version.minor}"
|
|
179
|
+
)
|
|
180
|
+
# return appschema with newer minor version, if found
|
|
181
|
+
# TODO: ensure minor version compatibility without version migration, e.g. via model_validator
|
|
182
|
+
if candidates:
|
|
183
|
+
return sorted(candidates, reverse=True)[0]
|
|
184
|
+
if versions:
|
|
185
|
+
e = NotImplementedError(
|
|
186
|
+
f"version {version!r} not supported for prefix {prefix!r}"
|
|
187
|
+
)
|
|
188
|
+
e.add_note(f"available versions: {', '.join(sorted(versions))}")
|
|
189
|
+
raise e
|
|
190
|
+
else:
|
|
191
|
+
raise NotImplementedError(f"no appschema with prefix {prefix!r}")
|
|
192
|
+
|
|
193
|
+
@classmethod
|
|
194
|
+
def from_namespace(cls, namespace: str) -> "Appschema":
|
|
195
|
+
"""Builds an Appschema instance from the appschema's namespace.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
namespace: the namespace URI of the appschema
|
|
199
|
+
"""
|
|
200
|
+
uri = AnyUrl(namespace)
|
|
201
|
+
for appschema in filter(
|
|
202
|
+
lambda appschema: appschema.namespace_uri == uri, cls.supported_appschemas()
|
|
203
|
+
):
|
|
204
|
+
if appschema.namespace_uri == uri:
|
|
205
|
+
return appschema
|
|
206
|
+
raise NotImplementedError(f"no appschema with namespace {namespace!r}")
|
|
207
|
+
|
|
208
|
+
@classmethod
|
|
209
|
+
def from_module(cls, module_name: str) -> "Appschema":
|
|
210
|
+
"""Builds an Appschema instance from module name.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
module_name: the fully qualified module name
|
|
214
|
+
"""
|
|
215
|
+
try:
|
|
216
|
+
module = import_module(module_name)
|
|
217
|
+
model_cls: type[RootModel] = getattr(module, "Model")
|
|
218
|
+
except (ImportError, AttributeError) as exc:
|
|
219
|
+
raise RuntimeError(
|
|
220
|
+
f"Unable to resolve appschema metadata for module {module_name!r}"
|
|
221
|
+
) from exc
|
|
222
|
+
|
|
223
|
+
if not issubclass(model_cls, RootModel):
|
|
224
|
+
raise ValueError(f"expected RootModel, got {model_cls!r}")
|
|
225
|
+
|
|
226
|
+
metadata = model_cls.model_fields["root"]
|
|
227
|
+
|
|
228
|
+
return cls.model_validate(
|
|
229
|
+
metadata.json_schema_extra
|
|
230
|
+
| {"description": metadata.description, "module": module}
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
@property
|
|
234
|
+
def version(self) -> str:
|
|
235
|
+
"""The version of the appschema in `<major>.<minor>` format."""
|
|
236
|
+
return f"{self.full_version.major}.{self.full_version.minor}"
|
|
237
|
+
|
|
53
238
|
|
|
54
239
|
class BaseCollection(BaseModel):
|
|
55
240
|
"""Container for features that provides validation of references.
|
|
@@ -333,6 +518,11 @@ class BaseFeature(BaseModel, GMLAdapter, CoretableAdapter, JsonFGAdapter):
|
|
|
333
518
|
geom_model = [arg for arg in args if arg is not NoneType]
|
|
334
519
|
return geom_model
|
|
335
520
|
|
|
521
|
+
@classmethod
|
|
522
|
+
def appschema(cls) -> Appschema:
|
|
523
|
+
"""Return metadata about the application schema for the feature class."""
|
|
524
|
+
return Appschema.from_module(cls.__module__)
|
|
525
|
+
|
|
336
526
|
@classmethod
|
|
337
527
|
def get_version(cls) -> str:
|
|
338
528
|
"""Returns the application schema version of the object."""
|
|
@@ -2,12 +2,13 @@ from alembic import context
|
|
|
2
2
|
from sqlalchemy import engine_from_config, pool
|
|
3
3
|
|
|
4
4
|
from xplan_tools.model.orm import Base
|
|
5
|
+
from xplan_tools.settings import get_settings
|
|
5
6
|
|
|
6
7
|
config = context.config
|
|
7
8
|
|
|
8
9
|
target_metadata = Base.metadata
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
settings = get_settings()
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
def run_migrations_offline() -> None:
|
|
@@ -59,7 +60,7 @@ def run_migrations_online() -> None:
|
|
|
59
60
|
context.configure(
|
|
60
61
|
connection=connection,
|
|
61
62
|
target_metadata=target_metadata,
|
|
62
|
-
version_table_schema=
|
|
63
|
+
version_table_schema=settings.db_schema,
|
|
63
64
|
)
|
|
64
65
|
|
|
65
66
|
with context.begin_transaction():
|
|
@@ -12,19 +12,22 @@ from alembic import op
|
|
|
12
12
|
from geoalchemy2.types import Geometry
|
|
13
13
|
from sqlalchemy.dialects import postgresql
|
|
14
14
|
|
|
15
|
+
from xplan_tools.settings import get_settings
|
|
16
|
+
|
|
15
17
|
# revision identifiers, used by Alembic.
|
|
16
18
|
revision: str = "3c3445a58565"
|
|
17
19
|
down_revision: Union[str, Sequence[str], None] = None
|
|
18
20
|
branch_labels: Union[str, Sequence[str], None] = None
|
|
19
21
|
depends_on: Union[str, Sequence[str], None] = None
|
|
20
22
|
|
|
23
|
+
settings = get_settings()
|
|
24
|
+
schema = settings.db_schema
|
|
25
|
+
srid = settings.db_srid
|
|
26
|
+
views = settings.db_views
|
|
27
|
+
|
|
21
28
|
|
|
22
29
|
def upgrade() -> None:
|
|
23
30
|
"""Upgrade schema."""
|
|
24
|
-
config = op.get_context().config
|
|
25
|
-
schema = config.get_main_option("custom_schema")
|
|
26
|
-
srid = int(config.get_main_option("srid", 25832))
|
|
27
|
-
|
|
28
31
|
op.create_table(
|
|
29
32
|
"coretable",
|
|
30
33
|
sa.Column(
|
|
@@ -171,7 +174,7 @@ def upgrade() -> None:
|
|
|
171
174
|
schema=schema,
|
|
172
175
|
)
|
|
173
176
|
|
|
174
|
-
if
|
|
177
|
+
if views:
|
|
175
178
|
view_context = {"schema": schema or "public", "srid": srid}
|
|
176
179
|
op.execute(
|
|
177
180
|
sa.DDL(
|
|
@@ -221,7 +224,6 @@ def upgrade() -> None:
|
|
|
221
224
|
|
|
222
225
|
def downgrade() -> None:
|
|
223
226
|
"""Downgrade schema."""
|
|
224
|
-
schema = op.get_context().config.get_main_option("custom_schema")
|
|
225
227
|
view_context = {"schema": schema or "public"}
|
|
226
228
|
op.execute(
|
|
227
229
|
sa.DDL(
|
|
@@ -10,12 +10,17 @@ from typing import Sequence, Union
|
|
|
10
10
|
import sqlalchemy as sa
|
|
11
11
|
from alembic import op
|
|
12
12
|
|
|
13
|
+
from xplan_tools.settings import get_settings
|
|
14
|
+
|
|
13
15
|
# revision identifiers, used by Alembic.
|
|
14
16
|
revision: str = "f8b74c08ec07"
|
|
15
17
|
down_revision: Union[str, Sequence[str], None] = "3c3445a58565"
|
|
16
18
|
branch_labels: Union[str, Sequence[str], None] = None
|
|
17
19
|
depends_on: Union[str, Sequence[str], None] = None
|
|
18
20
|
|
|
21
|
+
settings = get_settings()
|
|
22
|
+
schema = settings.db_schema
|
|
23
|
+
|
|
19
24
|
|
|
20
25
|
def upgrade() -> None:
|
|
21
26
|
"""Upgrade schema.
|
|
@@ -24,7 +29,6 @@ def upgrade() -> None:
|
|
|
24
29
|
geometries in `coretable.geometry` to counter-clockwise orientation
|
|
25
30
|
using `ST_ForcePolygonCCW` in-place.
|
|
26
31
|
"""
|
|
27
|
-
schema = op.get_context().config.get_main_option("custom_schema")
|
|
28
32
|
op.create_index(
|
|
29
33
|
op.f("ix_refs_base_id"),
|
|
30
34
|
"refs",
|
xplan_tools/model/orm.py
CHANGED
|
@@ -10,7 +10,6 @@ from geoalchemy2.admin.dialects.sqlite import register_sqlite_mapping
|
|
|
10
10
|
from sqlalchemy import (
|
|
11
11
|
JSON,
|
|
12
12
|
BigInteger,
|
|
13
|
-
Column,
|
|
14
13
|
Computed,
|
|
15
14
|
DateTime,
|
|
16
15
|
Dialect,
|
|
@@ -18,12 +17,10 @@ from sqlalchemy import (
|
|
|
18
17
|
Identity,
|
|
19
18
|
Integer,
|
|
20
19
|
String,
|
|
21
|
-
Table,
|
|
22
20
|
Text,
|
|
23
21
|
Uuid,
|
|
24
22
|
case,
|
|
25
23
|
cast,
|
|
26
|
-
event,
|
|
27
24
|
literal,
|
|
28
25
|
select,
|
|
29
26
|
)
|
|
@@ -39,6 +36,7 @@ from sqlalchemy.orm import (
|
|
|
39
36
|
from sqlalchemy.sql import func
|
|
40
37
|
from sqlalchemy.types import TypeDecorator
|
|
41
38
|
|
|
39
|
+
from xplan_tools.settings import get_settings
|
|
42
40
|
from xplan_tools.util import linearize_geom
|
|
43
41
|
|
|
44
42
|
register_sqlite_mapping({"ST_AsEWKT": "AsEWKT"})
|
|
@@ -113,7 +111,9 @@ class PGGeometry(TypeDecorator):
|
|
|
113
111
|
)
|
|
114
112
|
return func.ST_Transform(
|
|
115
113
|
case_clause,
|
|
116
|
-
func.Find_SRID(
|
|
114
|
+
func.Find_SRID(
|
|
115
|
+
get_settings().db_schema or "public", "coretable", "geometry"
|
|
116
|
+
),
|
|
117
117
|
type_=self,
|
|
118
118
|
)
|
|
119
119
|
|
|
@@ -129,17 +129,9 @@ class TextJSON(TypeDecorator):
|
|
|
129
129
|
return json.loads(value)
|
|
130
130
|
|
|
131
131
|
|
|
132
|
-
@event.listens_for(Column, "after_parent_attach")
|
|
133
|
-
def _column_attached(column: Column, table: Table):
|
|
134
|
-
"""Add schema, table and column name to PGGeometry for Find_SRID function."""
|
|
135
|
-
if column.name == "geometry":
|
|
136
|
-
PGGeometry.column_name = column.name
|
|
137
|
-
PGGeometry.schema_name = table.schema or "public"
|
|
138
|
-
PGGeometry.table_name = table.name
|
|
139
|
-
|
|
140
|
-
|
|
141
132
|
class Feature(Base):
|
|
142
133
|
__tablename__ = "coretable"
|
|
134
|
+
__table_args__ = {"schema": get_settings().db_schema}
|
|
143
135
|
pk: Mapped[int] = mapped_column(
|
|
144
136
|
Integer().with_variant(BigInteger, "postgresql"),
|
|
145
137
|
Identity(always=True),
|
|
@@ -182,12 +174,14 @@ class Feature(Base):
|
|
|
182
174
|
cascade="all, delete-orphan",
|
|
183
175
|
primaryjoin="Feature.id==Refs.base_id",
|
|
184
176
|
lazy="selectin",
|
|
177
|
+
passive_deletes=True,
|
|
185
178
|
)
|
|
186
179
|
refs_inv: Mapped[list["Refs"]] = relationship(
|
|
187
180
|
back_populates="feature_inv",
|
|
188
181
|
cascade="all, delete-orphan",
|
|
189
182
|
primaryjoin="Feature.id==Refs.related_id",
|
|
190
183
|
lazy="selectin",
|
|
184
|
+
passive_deletes=True,
|
|
191
185
|
)
|
|
192
186
|
|
|
193
187
|
__mapper_args__ = {"primary_key": [id]}
|
|
@@ -306,6 +300,7 @@ class Feature(Base):
|
|
|
306
300
|
|
|
307
301
|
class Refs(Base):
|
|
308
302
|
__tablename__ = "refs"
|
|
303
|
+
__table_args__ = {"schema": get_settings().db_schema}
|
|
309
304
|
pk: Mapped[int] = mapped_column(
|
|
310
305
|
Integer().with_variant(BigInteger, "postgresql"),
|
|
311
306
|
Identity(always=True),
|
|
@@ -313,13 +308,13 @@ class Refs(Base):
|
|
|
313
308
|
)
|
|
314
309
|
base_id: Mapped[UUID] = mapped_column(
|
|
315
310
|
ForeignKey(
|
|
316
|
-
|
|
311
|
+
Feature.id, ondelete="CASCADE", deferrable=True, initially="DEFERRED"
|
|
317
312
|
),
|
|
318
313
|
index=True,
|
|
319
314
|
)
|
|
320
315
|
related_id: Mapped[UUID] = mapped_column(
|
|
321
316
|
ForeignKey(
|
|
322
|
-
|
|
317
|
+
Feature.id, ondelete="CASCADE", deferrable=True, initially="DEFERRED"
|
|
323
318
|
),
|
|
324
319
|
index=True,
|
|
325
320
|
)
|
|
@@ -327,12 +322,12 @@ class Refs(Base):
|
|
|
327
322
|
rel_inv: Mapped[Optional[str]] = mapped_column(
|
|
328
323
|
String(50).with_variant(Text, "geopackage")
|
|
329
324
|
)
|
|
330
|
-
feature: Mapped[
|
|
325
|
+
feature: Mapped[Feature] = relationship(
|
|
331
326
|
back_populates="refs",
|
|
332
327
|
primaryjoin="Feature.id==Refs.base_id",
|
|
333
328
|
viewonly=True,
|
|
334
329
|
)
|
|
335
|
-
feature_inv: Mapped[
|
|
330
|
+
feature_inv: Mapped[Feature] = relationship(
|
|
336
331
|
back_populates="refs_inv",
|
|
337
332
|
primaryjoin="Feature.id==Refs.related_id",
|
|
338
333
|
viewonly=True,
|
|
@@ -340,7 +335,6 @@ class Refs(Base):
|
|
|
340
335
|
|
|
341
336
|
__mapper_args__ = {
|
|
342
337
|
"primary_key": [base_id, related_id],
|
|
343
|
-
"confirm_deleted_rows": False,
|
|
344
338
|
}
|
|
345
339
|
|
|
346
340
|
def __repr__(self) -> str:
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from functools import lru_cache
|
|
2
|
+
|
|
3
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
4
|
+
|
|
5
|
+
ENV_PREFIX = "xmas_"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Settings(BaseSettings):
|
|
9
|
+
model_config = SettingsConfigDict(
|
|
10
|
+
extra="ignore",
|
|
11
|
+
env_prefix=ENV_PREFIX,
|
|
12
|
+
env_file=".env",
|
|
13
|
+
env_file_encoding="utf-8",
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
db_srid: int = 25832
|
|
17
|
+
db_schema: str | None = None
|
|
18
|
+
db_views: bool = True
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@lru_cache
|
|
22
|
+
def get_settings() -> Settings:
|
|
23
|
+
return Settings()
|
xplan_tools/util/__init__.py
CHANGED
|
@@ -12,8 +12,6 @@ import httpx
|
|
|
12
12
|
import yaml
|
|
13
13
|
from osgeo import gdal, ogr
|
|
14
14
|
from pydantic import AnyUrl, ValidationError
|
|
15
|
-
from sqlalchemy import text
|
|
16
|
-
from sqlalchemy.exc import SQLAlchemyError
|
|
17
15
|
|
|
18
16
|
from xplan_tools.model import model_factory
|
|
19
17
|
from xplan_tools.resources.styles import RULES
|
|
@@ -297,28 +295,6 @@ class RasterReferenceUtil(ExternalReferenceUtil):
|
|
|
297
295
|
return _check_validity(AnyUrl(f"file://{vsimem_path}"))
|
|
298
296
|
|
|
299
297
|
|
|
300
|
-
def check_schema_accessibility(engine, schema: str):
|
|
301
|
-
"""Raises an exception if the schema does not exist or is not accessible to the current user."""
|
|
302
|
-
try:
|
|
303
|
-
with engine.connect() as conn:
|
|
304
|
-
# Check if the schema exists and is accessible
|
|
305
|
-
result = conn.execute(
|
|
306
|
-
text("""
|
|
307
|
-
SELECT schema_name
|
|
308
|
-
FROM information_schema.schemata
|
|
309
|
-
WHERE schema_name = :schema
|
|
310
|
-
"""),
|
|
311
|
-
{"schema": schema},
|
|
312
|
-
)
|
|
313
|
-
if result.scalar() is None:
|
|
314
|
-
raise RuntimeError(
|
|
315
|
-
f"Schema '{schema}' does not exist or is not accessible."
|
|
316
|
-
)
|
|
317
|
-
|
|
318
|
-
except SQLAlchemyError as e:
|
|
319
|
-
raise RuntimeError(f"Error checking schema '{schema}': {e}")
|
|
320
|
-
|
|
321
|
-
|
|
322
298
|
class _Versions(str, Enum):
|
|
323
299
|
_4_1 = "4.1"
|
|
324
300
|
_5_4 = "5.4"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: xplan-tools
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.13.0
|
|
4
4
|
Summary: Manage XPlanung data
|
|
5
5
|
License: EUPL-1.2-or-later
|
|
6
6
|
License-File: LICENSE.md
|
|
@@ -13,6 +13,7 @@ Classifier: License :: OSI Approved :: European Union Public Licence 1.2 (EUPL 1
|
|
|
13
13
|
Classifier: Operating System :: OS Independent
|
|
14
14
|
Classifier: Programming Language :: Python :: 3
|
|
15
15
|
Requires-Dist: alembic (>=1.16.4,<2)
|
|
16
|
+
Requires-Dist: deprecated
|
|
16
17
|
Requires-Dist: gdal (>=3.6.3)
|
|
17
18
|
Requires-Dist: geoalchemy2 (>=0.16)
|
|
18
19
|
Requires-Dist: httpx (>=0.27.2)
|
|
@@ -22,6 +23,8 @@ Requires-Dist: openpyxl (>=3.1)
|
|
|
22
23
|
Requires-Dist: pandas (>=2.2.2)
|
|
23
24
|
Requires-Dist: psycopg[binary] (>=3.2)
|
|
24
25
|
Requires-Dist: pydantic (>=2.10.3)
|
|
26
|
+
Requires-Dist: pydantic-extra-types[semver]
|
|
27
|
+
Requires-Dist: pydantic-settings (>=2.12.0,<3)
|
|
25
28
|
Requires-Dist: python-dateutil (>=2.9,<3)
|
|
26
29
|
Requires-Dist: pytz (>=2024.1)
|
|
27
30
|
Requires-Dist: pyyaml (>=6)
|
|
@@ -42,13 +45,13 @@ Description-Content-Type: text/markdown
|
|
|
42
45
|
**[Documentation](https://xplan-tools-xleitstelle-xplanung-8446a974e8119a851af45bf94e0717.usercontent.opencode.de/)** | **[Repository](https://gitlab.opencode.de/xleitstelle/xplanung/xplan-tools)**
|
|
43
46
|
|
|
44
47
|
|
|
45
|
-
A Python library to provide some useful means when working with XPlanung and
|
|
48
|
+
A Python library to provide some useful means when working with XPlanung, XTrasse and XWaermeplan data, e.g. format conversion, version migration and database setup. Relies on [Pydantic](https://pydantic.dev) for a Python representation of the XPlanung/XTrasse/XWaermeplan model as well as serialization/de-serialization and validation.
|
|
46
49
|
|
|
47
|
-
While it comes with a CLI, its modules are also meant to provide other applications a Python representation as well as an interface for XPlanung/XTrasse data.
|
|
50
|
+
While it comes with a CLI, its modules are also meant to provide other applications a Python representation as well as an interface for XPlanung/XTrasse/XWaermeplan data.
|
|
48
51
|
|
|
49
52
|
## Features
|
|
50
53
|
|
|
51
|
-
* Conversion between GML, JSON-FG and DB encodings of XPlanung/XTrasse data.
|
|
54
|
+
* Conversion between GML, JSON-FG and DB encodings of XPlanung/XTrasse/XWaermeplan data.
|
|
52
55
|
* Migration from older versions of XPlanung to the latest one.
|
|
53
56
|
* Set up a database to store XPlanung data. Supports [PostgreSQL](https://www.postgresql.org)/[PostGIS](https://postgis.net) as well as [GeoPackage](https://www.geopackage.org) and [SpatiaLite](https://www.gaia-gis.it/fossil/libspatialite/index) SQLite databases.
|
|
54
57
|
* Transformation from XPlanung to [INSPIRE PLU](https://inspire-mif.github.io/uml-models/approved/fc/#_P5531) based on the [official mappings](https://xleitstelle.de/xplanung/transformation-inspire/releases).
|
|
@@ -1,32 +1,35 @@
|
|
|
1
|
-
xplan_tools/interface/__init__.py,sha256=
|
|
1
|
+
xplan_tools/interface/__init__.py,sha256=ranXJ28NL2eowWLLnm1jqmUlEwm3R6nvnQcu-KvNZa8,4082
|
|
2
2
|
xplan_tools/interface/base.py,sha256=qZpQsSYa54omfw_tMfvcyQb4_F--_NFTq5Vc035rHIU,2014
|
|
3
|
-
xplan_tools/interface/db.py,sha256=
|
|
4
|
-
xplan_tools/interface/gml.py,sha256=
|
|
5
|
-
xplan_tools/interface/jsonfg.py,sha256=
|
|
3
|
+
xplan_tools/interface/db.py,sha256=pAWoeWsYXROPpkrI_7IqINaNDbEFN3Iw03O5pndcYtY,18737
|
|
4
|
+
xplan_tools/interface/gml.py,sha256=pPdlziRswIyo6YsO77CxniYt_6_O9uCJj4pSb7bX5sY,15871
|
|
5
|
+
xplan_tools/interface/jsonfg.py,sha256=rxG6_54boqaC_EFEUODbRpx6ZxprUkYTyiHi1R3Y_oI,9083
|
|
6
6
|
xplan_tools/interface/shape.py,sha256=1aTJrgklHCv74BQjmOfzUlGXIqKSsuWhqA4bDIt3-gM,11295
|
|
7
|
-
xplan_tools/main.py,sha256=
|
|
8
|
-
xplan_tools/model/__init__.py,sha256=
|
|
7
|
+
xplan_tools/main.py,sha256=fsoV0w5VkRwUyfVGdxm4K4CuP-IR1MlO07Y-nAzzNt0,12903
|
|
8
|
+
xplan_tools/model/__init__.py,sha256=8R-pZb8p8uxOEALGJ6pmeVD4T15-6TufIQ2xOJm3U14,3371
|
|
9
9
|
xplan_tools/model/adapters/__init__.py,sha256=6Q-zr4hr5msjsc5D1RNjxsQip618Kk2jI3p_CJKpw5c,170
|
|
10
10
|
xplan_tools/model/adapters/coretable.py,sha256=M9l-ZC--aDo7viVum8XWu9ArOfgBwB619-r0gOpx-q0,6541
|
|
11
|
-
xplan_tools/model/adapters/gml.py,sha256=
|
|
11
|
+
xplan_tools/model/adapters/gml.py,sha256=3iQRf2RIII8RMtIb_oR3gRXRy0vblaS3PF9BAV_bBUQ,10228
|
|
12
12
|
xplan_tools/model/adapters/jsonfg.py,sha256=MuAYof4skYXApelilhryndh2KjP5tylgpjAfKR5dakk,3459
|
|
13
13
|
xplan_tools/model/appschema/__init__.py,sha256=tY-W6RbLZbZZj6AJ1TwIpAQ3CLZy7F6dq2NhvsqTTpU,29
|
|
14
|
-
xplan_tools/model/appschema/definitions.py,sha256=
|
|
14
|
+
xplan_tools/model/appschema/definitions.py,sha256=8q2gh_tPej448xF6AgfCmUj6kq8ii14e5AnDAguHZyw,2347
|
|
15
15
|
xplan_tools/model/appschema/inspire_base.py,sha256=-PizgUNWTKGZKvSMejAZmraW4nvJrS9M9L7TGlwA-c0,2779
|
|
16
16
|
xplan_tools/model/appschema/inspire_base2.py,sha256=m5NH25fJcAcaT9RXO8BeEZT4qDaXf0bmqKVdjieiEY4,9827
|
|
17
17
|
xplan_tools/model/appschema/inspire_plu40.py,sha256=vgUs3vFj6DAaGuT3G5EGUB90H35S-3zOwG7Uy4T9-1E,33568
|
|
18
|
-
xplan_tools/model/appschema/xplan41.py,sha256=
|
|
19
|
-
xplan_tools/model/appschema/xplan54.py,sha256=
|
|
20
|
-
xplan_tools/model/appschema/xplan60.py,sha256=
|
|
21
|
-
xplan_tools/model/appschema/xplan61.py,sha256=
|
|
22
|
-
xplan_tools/model/appschema/xtrasse20.py,sha256=
|
|
23
|
-
xplan_tools/model/
|
|
24
|
-
xplan_tools/model/
|
|
18
|
+
xplan_tools/model/appschema/xplan41.py,sha256=brr9fZeWiecxe1ZY76GhGBuCiAAr8lyOS_7KpAnWb28,990645
|
|
19
|
+
xplan_tools/model/appschema/xplan54.py,sha256=FtktDeb6Bjvo6Fni3lpk1m9fBXVmL3lpV2eXy6dA9ZI,1011373
|
|
20
|
+
xplan_tools/model/appschema/xplan60.py,sha256=1HCignD1W4bA9DZvZhoEoPiTCSiDQTl6c9JGgFdS70Y,1200398
|
|
21
|
+
xplan_tools/model/appschema/xplan61.py,sha256=st0f3Ih_emUCB1tMcumGAPGNtryBZl16BB-Mqr6o12s,1319024
|
|
22
|
+
xplan_tools/model/appschema/xtrasse20.py,sha256=0Me-y1ydADedhK1fxe8W8fLR1228FMt4RIZU-iYJRro,537913
|
|
23
|
+
xplan_tools/model/appschema/xwp09.py,sha256=Q2oDiAvKjYAbIrHQ2hlwOnbB-XoeaDgi_qJnOVW8k6Y,218599
|
|
24
|
+
xplan_tools/model/base.py,sha256=xbTZyOHuyK3JkNl9DAdzDitKhS9LrJHc3c6cJJDHX5c,27583
|
|
25
|
+
xplan_tools/model/migrations/env.py,sha256=dJw3GH3_pJn2LuiflD1wwJi7Dq-9avm56xdRc50Wzpg,1974
|
|
25
26
|
xplan_tools/model/migrations/script.py.mako,sha256=04kgeBtNMa4cCnG8CfQcKt6P6rnloIfj8wy0u_DBydM,704
|
|
26
|
-
xplan_tools/model/migrations/versions/3c3445a58565_base_schema.py,sha256=
|
|
27
|
-
xplan_tools/model/migrations/versions/f8b74c08ec07_add_refs_indexes_ensure_polygon_ccw.py,sha256=
|
|
28
|
-
xplan_tools/model/orm.py,sha256=
|
|
27
|
+
xplan_tools/model/migrations/versions/3c3445a58565_base_schema.py,sha256=nsgcqTDFkcejdT4YqRfyQeDbWAHMqZJKVyZnUm5BY9A,7857
|
|
28
|
+
xplan_tools/model/migrations/versions/f8b74c08ec07_add_refs_indexes_ensure_polygon_ccw.py,sha256=RCDG-vT6jiHqvg29BCnYoM1YKl9sJw_I5vamdXr62zU,1767
|
|
29
|
+
xplan_tools/model/orm.py,sha256=_OPGbIx3C93RZsJZL-Rx52hWIgwqHlN_yW7Y815Om24,11936
|
|
29
30
|
xplan_tools/resources/styles.py,sha256=h0ezUqZrsXMQz2bTcF0nIyoI-IxMItstHx1j1xYGi3c,45010
|
|
31
|
+
xplan_tools/settings/__init__.py,sha256=yQdL0KFclWVUBE0ZMUTkktKMa9D78RDcPrHkK23Z9m8,89
|
|
32
|
+
xplan_tools/settings/settings.py,sha256=bABcqw7--TU-khpE9l-2XkavlQ-xuVZwmBmJiD0yRVw,462
|
|
30
33
|
xplan_tools/transform/__init__.py,sha256=DBIscGFL0H-oifh_XCeB0T8M5el3JGFwNJo8t-qUwTY,2159
|
|
31
34
|
xplan_tools/transform/mappingtables/XPlanToINSPIRE-SupplementaryRegulation_2_6_2025-10-08.xlsx,sha256=eE0avwlJCBso5zfUXwiY5VNE1x-XgGpWdo5fXKErd3M,85819
|
|
32
35
|
xplan_tools/transform/mappingtables/XPlanToINSPIRE-ZoningElement_2_6_2025-10-08.xlsx,sha256=bP8q5JYO8MZWCtr3hW4I92gyHY1P5q8ywEUlFPadIfw,77838
|
|
@@ -36,11 +39,11 @@ xplan_tools/transform/migrate_54_60.py,sha256=GeVKcfWRaf0vJFuZmvp5jbGJLwB-lX7ebe
|
|
|
36
39
|
xplan_tools/transform/migrate_60_61.py,sha256=S-fdnTjQUf8LGnEvsOY73zZYDvyVgIhVewPqyRR3Rcs,614
|
|
37
40
|
xplan_tools/transform/migrate_6x_plu.py,sha256=3w5UfjdUKFUiLma3Ppc9fAogPVxVshtUzzve0Dv0nWM,216797
|
|
38
41
|
xplan_tools/transform/transformer.py,sha256=2O4Aq_eTo9e2JOUwq9tJnfv6dwOxqNDdTGPtV_SAL1w,26216
|
|
39
|
-
xplan_tools/util/__init__.py,sha256=
|
|
42
|
+
xplan_tools/util/__init__.py,sha256=OqOorl5s8BZANbLeFE9wWlozvA64RINn9wigMkds2u8,11704
|
|
40
43
|
xplan_tools/util/style.py,sha256=6Iyj-fsmnJylkJo-70wCFjfsbbwUGAS5QTN2IMJAVdI,7503
|
|
41
44
|
xplan_tools/util/validate.py,sha256=3inTRLHrVonfQ9FDjtvSNKlAGem5DKtEvLqW7NxwWQs,3327
|
|
42
|
-
xplan_tools-1.
|
|
43
|
-
xplan_tools-1.
|
|
44
|
-
xplan_tools-1.
|
|
45
|
-
xplan_tools-1.
|
|
46
|
-
xplan_tools-1.
|
|
45
|
+
xplan_tools-1.13.0.dist-info/METADATA,sha256=9RF_9DOvPsv7KfMti44bJRpAj64nVLOL9MbwCqjUrxA,4824
|
|
46
|
+
xplan_tools-1.13.0.dist-info/WHEEL,sha256=3ny-bZhpXrU6vSQ1UPG34FoxZBp3lVcvK0LkgUz6VLk,88
|
|
47
|
+
xplan_tools-1.13.0.dist-info/entry_points.txt,sha256=NV98QKVV3vDuWnJtWsKZvcpEH2N7SF1yHXvVMet23Ps,52
|
|
48
|
+
xplan_tools-1.13.0.dist-info/licenses/LICENSE.md,sha256=b8nnCcy_4Nd_v_okJ6mDKCvi64jkexzbSfIag7TR5mU,13827
|
|
49
|
+
xplan_tools-1.13.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|