xplan-tools 1.11.1__py3-none-any.whl → 1.12.1__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/db.py +152 -213
- xplan_tools/interface/gml.py +1 -3
- xplan_tools/main.py +2 -0
- xplan_tools/model/adapters/coretable.py +20 -14
- xplan_tools/model/adapters/gml.py +3 -5
- xplan_tools/model/base.py +37 -12
- xplan_tools/model/migrations/env.py +11 -3
- xplan_tools/model/migrations/versions/3c3445a58565_base_schema.py +269 -4
- xplan_tools/model/migrations/versions/f8b74c08ec07_add_refs_indexes_ensure_polygon_ccw.py +61 -0
- xplan_tools/model/orm.py +170 -29
- {xplan_tools-1.11.1.dist-info → xplan_tools-1.12.1.dist-info}/METADATA +3 -2
- {xplan_tools-1.11.1.dist-info → xplan_tools-1.12.1.dist-info}/RECORD +15 -14
- {xplan_tools-1.11.1.dist-info → xplan_tools-1.12.1.dist-info}/WHEEL +1 -1
- {xplan_tools-1.11.1.dist-info → xplan_tools-1.12.1.dist-info}/entry_points.txt +0 -0
- {xplan_tools-1.11.1.dist-info → xplan_tools-1.12.1.dist-info/licenses}/LICENSE.md +0 -0
xplan_tools/interface/db.py
CHANGED
|
@@ -6,10 +6,20 @@ from pathlib import Path
|
|
|
6
6
|
from typing import Iterable
|
|
7
7
|
|
|
8
8
|
from alembic import command, config, script
|
|
9
|
-
from alembic.runtime import migration
|
|
10
9
|
from geoalchemy2 import load_spatialite_gpkg
|
|
11
10
|
from geoalchemy2.admin.dialects.sqlite import load_spatialite_driver
|
|
12
|
-
from sqlalchemy import
|
|
11
|
+
from sqlalchemy import (
|
|
12
|
+
Column,
|
|
13
|
+
Engine,
|
|
14
|
+
MetaData,
|
|
15
|
+
Table,
|
|
16
|
+
create_engine,
|
|
17
|
+
delete,
|
|
18
|
+
insert,
|
|
19
|
+
inspect,
|
|
20
|
+
select,
|
|
21
|
+
text,
|
|
22
|
+
)
|
|
13
23
|
from sqlalchemy.engine import URL, make_url
|
|
14
24
|
|
|
15
25
|
# from sqlalchemy.dialects.sqlite.base import SQLiteCompiler
|
|
@@ -21,7 +31,7 @@ from sqlalchemy.orm import sessionmaker
|
|
|
21
31
|
# from sqlalchemy.sql.expression import BindParameter
|
|
22
32
|
from xplan_tools.model import model_factory
|
|
23
33
|
from xplan_tools.model.base import BaseCollection, BaseFeature
|
|
24
|
-
from xplan_tools.model.orm import Base, Feature, Geometry
|
|
34
|
+
from xplan_tools.model.orm import Base, Feature, Geometry, Refs
|
|
25
35
|
from xplan_tools.util import check_schema_accessibility
|
|
26
36
|
|
|
27
37
|
# from xplan_tools.util import linearize_geom
|
|
@@ -62,147 +72,124 @@ class DBRepository(BaseRepository):
|
|
|
62
72
|
self.srid = srid
|
|
63
73
|
self.with_views = with_views
|
|
64
74
|
|
|
65
|
-
alembic_cfg = config.Config()
|
|
66
|
-
alembic_cfg.set_main_option(
|
|
67
|
-
|
|
75
|
+
self.alembic_cfg = config.Config()
|
|
76
|
+
self.alembic_cfg.set_main_option(
|
|
77
|
+
"script_location", "xplan_tools:model:migrations"
|
|
78
|
+
)
|
|
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
|
+
self.alembic_cfg.set_main_option(
|
|
68
83
|
"sqlalchemy.url",
|
|
69
84
|
datasource.replace("gpkg:", "sqlite:").replace(
|
|
70
85
|
"postgresql:", "postgresql+psycopg:"
|
|
71
86
|
),
|
|
72
87
|
)
|
|
73
|
-
alembic_url = make_url(alembic_cfg.get_main_option("sqlalchemy.url"))
|
|
74
88
|
if self.schema and self.dialect == "postgresql":
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
89
|
+
check_schema_accessibility(self._engine, self.schema)
|
|
90
|
+
self.alembic_cfg.set_main_option("custom_schema", self.schema)
|
|
91
|
+
current_version = script.ScriptDirectory.from_config(
|
|
92
|
+
self.alembic_cfg
|
|
93
|
+
).get_heads()
|
|
78
94
|
# test for tables and revision
|
|
79
|
-
with
|
|
80
|
-
context = migration.MigrationContext.configure(
|
|
81
|
-
conn,
|
|
82
|
-
)
|
|
83
|
-
db_version = context.get_current_heads()
|
|
95
|
+
with self._engine.connect() as conn:
|
|
84
96
|
inspector = inspect(conn)
|
|
85
97
|
tables = inspector.get_table_names(schema=self.schema)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
else:
|
|
96
|
-
e = RuntimeError(
|
|
97
|
-
f"Incompatible database revision and automatic migration not implemented for {self.dialect}"
|
|
98
|
-
)
|
|
99
|
-
e.add_note(
|
|
100
|
-
"please set up a new database with the current version of this library"
|
|
101
|
-
)
|
|
102
|
-
raise e
|
|
98
|
+
is_coretable = {"coretable", "refs"}.issubset(set(tables))
|
|
99
|
+
if "alembic_version" in tables:
|
|
100
|
+
alembic_table = Table(
|
|
101
|
+
"alembic_version",
|
|
102
|
+
MetaData(schema=self.schema),
|
|
103
|
+
Column("version_num"),
|
|
104
|
+
)
|
|
105
|
+
stmt = select(alembic_table.c.version_num)
|
|
106
|
+
db_version = conn.execute(stmt).scalars().all()
|
|
103
107
|
else:
|
|
108
|
+
db_version = []
|
|
109
|
+
is_current_version = set(db_version) == set(current_version)
|
|
110
|
+
if is_current_version:
|
|
104
111
|
logger.info("Database is at current revision")
|
|
112
|
+
return
|
|
113
|
+
# handle schema upgrade or table creation
|
|
114
|
+
if is_coretable and not db_version:
|
|
115
|
+
e = RuntimeError("Coretable with no revision found in database")
|
|
116
|
+
e.add_note(
|
|
117
|
+
"it is likely that the database was set up with an older version of this library which didn't use revisions yet"
|
|
118
|
+
)
|
|
119
|
+
e.add_note(
|
|
120
|
+
"please set up a new database or add a revision corresponding to the current model manually"
|
|
121
|
+
)
|
|
122
|
+
raise e
|
|
123
|
+
# if postgresql, run alembic and return
|
|
124
|
+
elif self.dialect == "postgresql":
|
|
125
|
+
logger.info(
|
|
126
|
+
"Running database migrations"
|
|
127
|
+
if db_version
|
|
128
|
+
else "Creating new database schema"
|
|
129
|
+
)
|
|
130
|
+
command.upgrade(self.alembic_cfg, "head")
|
|
131
|
+
return
|
|
132
|
+
elif db_version:
|
|
133
|
+
e = NotImplementedError(
|
|
134
|
+
f"Incompatible database revision and automatic migration not implemented for {self.dialect}"
|
|
135
|
+
)
|
|
136
|
+
e.add_note(
|
|
137
|
+
"please set up a new database with the current version of this library"
|
|
138
|
+
)
|
|
139
|
+
raise e
|
|
105
140
|
else:
|
|
106
|
-
if
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
)
|
|
111
|
-
e.add_note(
|
|
112
|
-
"please set up a new database or add a revision corresponding to the current model manually"
|
|
113
|
-
)
|
|
114
|
-
raise e
|
|
115
|
-
else:
|
|
116
|
-
# create tables if it's a fresh DB and set it to current revision
|
|
117
|
-
logger.info("Creating new database schema")
|
|
118
|
-
self.create_tables(self.srid, self.with_views)
|
|
119
|
-
command.stamp(alembic_cfg, "head")
|
|
141
|
+
# create tables if it's a fresh file-based DB and set it to current revision
|
|
142
|
+
logger.info("Creating new database schema")
|
|
143
|
+
self.create_tables(self.srid)
|
|
144
|
+
command.stamp(self.alembic_cfg, "head")
|
|
120
145
|
|
|
121
146
|
@property
|
|
122
147
|
def _engine(self) -> Engine:
|
|
148
|
+
url = (
|
|
149
|
+
self.datasource.set(drivername="postgresql+psycopg")
|
|
150
|
+
if self.dialect == "postgresql"
|
|
151
|
+
else self.datasource
|
|
152
|
+
)
|
|
153
|
+
connect_args: dict[str, str] = {}
|
|
154
|
+
if self.schema and self.dialect == "postgresql":
|
|
155
|
+
connect_args["options"] = f"-csearch_path={self.schema},public"
|
|
156
|
+
engine = create_engine(url, connect_args=connect_args)
|
|
123
157
|
if self.dialect == "geopackage":
|
|
124
|
-
engine = create_engine(self.datasource)
|
|
125
158
|
listen(engine, "connect", load_spatialite_gpkg)
|
|
126
|
-
return engine
|
|
127
159
|
elif self.dialect == "sqlite":
|
|
128
|
-
engine = create_engine(self.datasource)
|
|
129
160
|
listen(
|
|
130
161
|
engine,
|
|
131
162
|
"connect",
|
|
132
163
|
load_spatialite_driver,
|
|
133
164
|
)
|
|
134
|
-
|
|
135
|
-
else:
|
|
136
|
-
engine = create_engine(
|
|
137
|
-
self.datasource.set(drivername="postgresql+psycopg"), echo=False
|
|
138
|
-
)
|
|
139
|
-
if self.schema:
|
|
140
|
-
check_schema_accessibility(engine, self.schema)
|
|
141
|
-
|
|
142
|
-
return engine
|
|
143
|
-
|
|
144
|
-
# see https://docs.sqlalchemy.org/en/20/faq/sqlexpressions.html#rendering-bound-parameters-inline
|
|
145
|
-
# @compiles(BindParameter)
|
|
146
|
-
# def _render_literal_bindparam(
|
|
147
|
-
# element: BindParameter, compiler, dump_to_file=False, **kw
|
|
148
|
-
# ):
|
|
149
|
-
# if not dump_to_file:
|
|
150
|
-
# return compiler.visit_bindparam(element, **kw)
|
|
151
|
-
# if (
|
|
152
|
-
# isinstance(compiler, SQLiteCompiler)
|
|
153
|
-
# and "geometry" in str(element.type)
|
|
154
|
-
# and element.value is not None
|
|
155
|
-
# ):
|
|
156
|
-
# return repr(linearize_geom(element.value))
|
|
157
|
-
# elif isinstance(element.value, dict):
|
|
158
|
-
# return repr(json.dumps(element.value))
|
|
159
|
-
# else:
|
|
160
|
-
# return repr(str(element.value))
|
|
165
|
+
return engine
|
|
161
166
|
|
|
162
167
|
def get_plan_by_id(self, id: str) -> BaseCollection:
|
|
163
168
|
logger.debug(f"retrieving plan with id {id}")
|
|
164
169
|
with self.Session() as session:
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
if self.dialect == "geopackage"
|
|
168
|
-
else "SELECT srid FROM geometry_columns WHERE f_table_name='coretable'"
|
|
169
|
-
)
|
|
170
|
-
srid = session.execute(stmt).scalar_one()
|
|
171
|
-
|
|
172
|
-
feature = session.get(Feature, id)
|
|
173
|
-
if not feature:
|
|
170
|
+
plan_feature = session.get(Feature, id)
|
|
171
|
+
if not plan_feature:
|
|
174
172
|
raise ValueError(f"no feature found with id {id}")
|
|
175
|
-
elif "Plan" not in
|
|
176
|
-
raise ValueError(f"{
|
|
173
|
+
elif "Plan" not in plan_feature.featuretype:
|
|
174
|
+
raise ValueError(f"{plan_feature.featuretype} is not a plan object")
|
|
177
175
|
else:
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
176
|
+
plan_model = model_factory(
|
|
177
|
+
plan_feature.featuretype,
|
|
178
|
+
plan_feature.version,
|
|
179
|
+
plan_feature.appschema,
|
|
180
|
+
).model_validate(plan_feature)
|
|
181
|
+
collection = {id: plan_model}
|
|
182
|
+
srid = plan_model.get_geom_srid()
|
|
183
|
+
# iterate related features with depth=2: plan -> section -> features
|
|
184
|
+
for feature in plan_feature.related_features(session, depth=2):
|
|
185
|
+
collection[str(feature.id)] = model_factory(
|
|
186
|
+
feature.featuretype, feature.version, feature.appschema
|
|
184
187
|
).model_validate(feature)
|
|
185
|
-
}
|
|
186
|
-
for ref in feature.refs:
|
|
187
|
-
collection[ref.feature_inv.id] = model_factory(
|
|
188
|
-
ref.feature_inv.featuretype, self.version, feature.appschema
|
|
189
|
-
).model_validate(ref.feature_inv)
|
|
190
|
-
if ref.rel == "bereich":
|
|
191
|
-
for obj in ref.feature_inv.refs:
|
|
192
|
-
collection[obj.feature_inv.id] = model_factory(
|
|
193
|
-
obj.feature_inv.featuretype,
|
|
194
|
-
self.version,
|
|
195
|
-
feature.appschema,
|
|
196
|
-
).model_validate(obj.feature_inv)
|
|
197
|
-
for ref_inv in feature.refs_inv:
|
|
198
|
-
collection[ref_inv.feature.id] = model_factory(
|
|
199
|
-
ref_inv.feature.featuretype, self.version, feature.appschema
|
|
200
|
-
).model_validate(ref_inv.feature)
|
|
201
188
|
return BaseCollection(
|
|
202
189
|
features=collection,
|
|
203
190
|
srid=srid,
|
|
204
|
-
version=
|
|
205
|
-
appschema=
|
|
191
|
+
version=plan_feature.version,
|
|
192
|
+
appschema=plan_feature.appschema,
|
|
206
193
|
)
|
|
207
194
|
|
|
208
195
|
def get(self, id: str) -> BaseFeature:
|
|
@@ -228,24 +215,25 @@ class DBRepository(BaseRepository):
|
|
|
228
215
|
def delete_plan_by_id(self, id: str) -> BaseFeature:
|
|
229
216
|
logger.debug(f"deleting plan with id {id}")
|
|
230
217
|
with self.Session() as session:
|
|
231
|
-
|
|
232
|
-
if not
|
|
218
|
+
plan_feature = session.get(Feature, id)
|
|
219
|
+
if not plan_feature:
|
|
233
220
|
raise ValueError(f"no feature found with id {id}")
|
|
234
|
-
elif "Plan" not in
|
|
235
|
-
raise ValueError(f"{
|
|
221
|
+
elif "Plan" not in plan_feature.featuretype:
|
|
222
|
+
raise ValueError(f"{plan_feature.featuretype} is not a plan object")
|
|
236
223
|
else:
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
224
|
+
plan_model = model_factory(
|
|
225
|
+
plan_feature.featuretype,
|
|
226
|
+
plan_feature.version,
|
|
227
|
+
plan_feature.appschema,
|
|
228
|
+
).model_validate(plan_feature)
|
|
229
|
+
ids = [plan_feature.id]
|
|
230
|
+
ids += [
|
|
231
|
+
feature.id for feature in plan_feature.related_features(session)
|
|
232
|
+
]
|
|
233
|
+
stmt = delete(Feature).where(Feature.id.in_(ids))
|
|
234
|
+
session.execute(stmt)
|
|
245
235
|
session.commit()
|
|
246
|
-
return
|
|
247
|
-
feature.featuretype, feature.version, feature.appschema
|
|
248
|
-
).model_validate(feature)
|
|
236
|
+
return plan_model
|
|
249
237
|
|
|
250
238
|
def delete(self, id: str) -> BaseFeature:
|
|
251
239
|
logger.debug(f"deleting feature with id {id}")
|
|
@@ -265,15 +253,20 @@ class DBRepository(BaseRepository):
|
|
|
265
253
|
) -> None:
|
|
266
254
|
logger.debug("saving collection")
|
|
267
255
|
with self.Session() as session:
|
|
256
|
+
feature_list = []
|
|
257
|
+
refs_list = []
|
|
268
258
|
for feature in (
|
|
269
259
|
features.get_features()
|
|
270
260
|
if isinstance(features, BaseCollection)
|
|
271
261
|
else features
|
|
272
262
|
):
|
|
273
|
-
feature = feature.
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
263
|
+
feature, refs = feature.model_dump_coretable_bulk()
|
|
264
|
+
feature_list.append(feature)
|
|
265
|
+
refs_list.extend([ref for ref in refs if ref not in refs_list])
|
|
266
|
+
if feature_list:
|
|
267
|
+
session.execute(insert(Feature), feature_list)
|
|
268
|
+
if refs_list:
|
|
269
|
+
session.execute(insert(Refs), refs_list)
|
|
277
270
|
session.commit()
|
|
278
271
|
|
|
279
272
|
def update_all(
|
|
@@ -322,32 +315,21 @@ class DBRepository(BaseRepository):
|
|
|
322
315
|
else:
|
|
323
316
|
raise ValueError(f"no feature found with id {id}")
|
|
324
317
|
|
|
325
|
-
def create_tables(self, srid: int
|
|
318
|
+
def create_tables(self, srid: int) -> None:
|
|
326
319
|
"""Creates coretable and related/spatial tables in the database.
|
|
327
320
|
|
|
328
321
|
Args:
|
|
329
322
|
srid: the EPSG code for spatial data
|
|
330
|
-
with_views: whether to create geometrytype-specific views (postgres only)
|
|
331
323
|
"""
|
|
332
324
|
|
|
333
325
|
@listens_for(Base.metadata, "before_create")
|
|
334
|
-
def pre_creation(
|
|
326
|
+
def pre_creation(_, conn, **kwargs):
|
|
335
327
|
if self.dialect == "sqlite":
|
|
336
328
|
conn.execute(text("SELECT InitSpatialMetaData('EMPTY')"))
|
|
337
329
|
conn.execute(text("SELECT InsertEpsgSrid(:srid)"), {"srid": srid})
|
|
338
330
|
|
|
339
331
|
@listens_for(Base.metadata, "after_create")
|
|
340
|
-
def post_creation(
|
|
341
|
-
coretable = self.schema + ".coretable" if self.schema else "coretable"
|
|
342
|
-
stmt = (
|
|
343
|
-
DDL(
|
|
344
|
-
"CREATE INDEX IF NOT EXISTS idx_coretable_geometry ON %(coretable)s USING GIST (geometry)",
|
|
345
|
-
{"coretable": coretable},
|
|
346
|
-
)
|
|
347
|
-
if self.dialect == "postgresql"
|
|
348
|
-
else text("SELECT CreateSpatialIndex(:coretable, 'geometry')")
|
|
349
|
-
)
|
|
350
|
-
conn.execute(stmt, {"coretable": coretable})
|
|
332
|
+
def post_creation(_, conn, **kwargs):
|
|
351
333
|
if self.dialect == "geopackage":
|
|
352
334
|
conn.execute(
|
|
353
335
|
text(
|
|
@@ -362,83 +344,40 @@ class DBRepository(BaseRepository):
|
|
|
362
344
|
)
|
|
363
345
|
)
|
|
364
346
|
conn.execute(
|
|
365
|
-
text(
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
347
|
+
text(
|
|
348
|
+
"""
|
|
349
|
+
INSERT INTO gpkgext_relations (base_table_name, base_primary_column, related_table_name, related_primary_column, relation_name, mapping_table_name)
|
|
350
|
+
VALUES
|
|
351
|
+
('coretable', 'id', 'coretable', 'id', 'features', 'refs')
|
|
352
|
+
"""
|
|
353
|
+
)
|
|
370
354
|
)
|
|
371
355
|
conn.execute(
|
|
372
|
-
text(
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
if with_views:
|
|
379
|
-
if self.dialect != "postgresql":
|
|
380
|
-
logger.warning(
|
|
381
|
-
f"Creating views not yet supported for {self.dialect}, skipping"
|
|
382
|
-
)
|
|
383
|
-
else:
|
|
384
|
-
conn.execute(
|
|
385
|
-
DDL(
|
|
386
|
-
"""
|
|
387
|
-
create or replace view %(schema)s.coretable_points as
|
|
388
|
-
select pk, id, featuretype, properties, ST_Multi(geometry)::geometry(MultiPoint, %(srid)s) as geometry, appschema, version
|
|
389
|
-
from %(schema)s.coretable
|
|
390
|
-
where geometry_type = 'point'
|
|
391
|
-
""",
|
|
392
|
-
{"srid": srid, "schema": self.schema or "public"},
|
|
393
|
-
)
|
|
394
|
-
)
|
|
395
|
-
conn.execute(
|
|
396
|
-
DDL(
|
|
397
|
-
"""
|
|
398
|
-
create or replace view %(schema)s.coretable_lines as
|
|
399
|
-
select pk, id, featuretype, properties, ST_Multi(ST_ForceCurve(geometry))::geometry(MultiCurve, %(srid)s) as geometry, appschema, version
|
|
400
|
-
from %(schema)s.coretable
|
|
401
|
-
where geometry_type = 'line'
|
|
402
|
-
""",
|
|
403
|
-
{"srid": srid, "schema": self.schema or "public"},
|
|
404
|
-
)
|
|
405
|
-
)
|
|
406
|
-
conn.execute(
|
|
407
|
-
DDL(
|
|
408
|
-
"""
|
|
409
|
-
create or replace view %(schema)s.coretable_polygons as
|
|
410
|
-
select pk, id, featuretype, properties, ST_Multi(ST_ForceCurve(geometry))::geometry(MultiSurface, %(srid)s) as geometry, appschema, version
|
|
411
|
-
from %(schema)s.coretable
|
|
412
|
-
where geometry_type = 'polygon'
|
|
413
|
-
""",
|
|
414
|
-
{"srid": srid, "schema": self.schema or "public"},
|
|
415
|
-
)
|
|
416
|
-
)
|
|
417
|
-
conn.execute(
|
|
418
|
-
DDL(
|
|
419
|
-
"""
|
|
420
|
-
create or replace view %(schema)s.coretable_nogeoms as
|
|
421
|
-
select pk, id, featuretype, properties, geometry, appschema, version
|
|
422
|
-
from %(schema)s.coretable
|
|
423
|
-
where geometry_type = 'nogeom'
|
|
424
|
-
""",
|
|
425
|
-
{"schema": self.schema or "public"},
|
|
426
|
-
),
|
|
356
|
+
text(
|
|
357
|
+
"""
|
|
358
|
+
INSERT INTO gpkg_data_columns (table_name, column_name, mime_type)
|
|
359
|
+
VALUES
|
|
360
|
+
('coretable', 'properties', 'application/json')
|
|
361
|
+
"""
|
|
427
362
|
)
|
|
363
|
+
)
|
|
428
364
|
|
|
429
365
|
logger.debug(f"creating tables with srid {srid}")
|
|
430
366
|
tables = Base.metadata.sorted_tables
|
|
431
367
|
if not self.dialect == "geopackage":
|
|
432
368
|
tables.pop(1)
|
|
433
369
|
tables[0].append_column(
|
|
434
|
-
Column(
|
|
370
|
+
Column(
|
|
371
|
+
"geometry",
|
|
372
|
+
Geometry(
|
|
373
|
+
srid=srid,
|
|
374
|
+
spatial_index=True,
|
|
375
|
+
),
|
|
376
|
+
nullable=True,
|
|
377
|
+
),
|
|
435
378
|
replace_existing=True,
|
|
436
379
|
)
|
|
437
380
|
|
|
438
|
-
if self.schema and self.dialect.startswith("postgresql"):
|
|
439
|
-
for table in Base.metadata.tables.values():
|
|
440
|
-
table.schema = self.schema
|
|
441
|
-
|
|
442
381
|
try:
|
|
443
382
|
Base.metadata.create_all(self._engine, tables)
|
|
444
383
|
remove(Base.metadata, "before_create", pre_creation)
|
|
@@ -453,7 +392,7 @@ class DBRepository(BaseRepository):
|
|
|
453
392
|
def delete_tables(self) -> None:
|
|
454
393
|
"""Deletes coretable and related/spatial tables from the database."""
|
|
455
394
|
logger.debug("deleting tables")
|
|
456
|
-
if self.
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
395
|
+
if self.dialect == "postgresql":
|
|
396
|
+
command.downgrade(self.alembic_cfg, "base")
|
|
397
|
+
else:
|
|
398
|
+
Base.metadata.drop_all(self._engine)
|
xplan_tools/interface/gml.py
CHANGED
|
@@ -199,9 +199,7 @@ class GMLRepository(BaseRepository):
|
|
|
199
199
|
if self.appschema == "xtrasse"
|
|
200
200
|
else "{http://www.opengis.net/wfs/2.0}member",
|
|
201
201
|
).append(
|
|
202
|
-
feature.model_dump_gml(
|
|
203
|
-
self.appschema, feature_srs=kwargs.get("feature_srs", True)
|
|
204
|
-
)
|
|
202
|
+
feature.model_dump_gml(feature_srs=kwargs.get("feature_srs", True))
|
|
205
203
|
)
|
|
206
204
|
bbox = get_envelope(geoms)
|
|
207
205
|
attrib = (
|
xplan_tools/main.py
CHANGED
|
@@ -22,6 +22,8 @@ __version__ = metadata.version("xplan_tools")
|
|
|
22
22
|
console = Console()
|
|
23
23
|
error_console = Console(stderr=True, style="bold red")
|
|
24
24
|
logger = logging.getLogger(__name__)
|
|
25
|
+
# don't propagate alembic logs when using CLI
|
|
26
|
+
logging.getLogger("alembic").propagate = False
|
|
25
27
|
|
|
26
28
|
app = typer.Typer(help=f"XPlan-Tools {__version__}")
|
|
27
29
|
db_app = typer.Typer()
|
|
@@ -8,7 +8,9 @@ from xplan_tools.model.orm import Feature, Refs
|
|
|
8
8
|
class CoretableAdapter:
|
|
9
9
|
"""Class to add ORM model - i.e. coretable - transformation methods to XPlan pydantic model via inheritance."""
|
|
10
10
|
|
|
11
|
-
def _to_coretable(
|
|
11
|
+
def _to_coretable(
|
|
12
|
+
self, bulk_mode: bool = False
|
|
13
|
+
) -> Feature | tuple[dict, list[dict]]:
|
|
12
14
|
"""Converts a BaseFeature to a Coretable Feature object."""
|
|
13
15
|
properties = self.model_dump(mode="json", exclude_none=True)
|
|
14
16
|
id = properties.pop("id")
|
|
@@ -95,19 +97,23 @@ class CoretableAdapter:
|
|
|
95
97
|
gener_att[f"wert_{item.get_name()}"] = gener_att.pop("wert")
|
|
96
98
|
gener_att["datatype"] = item.get_name()
|
|
97
99
|
properties["hatGenerAttribut"].append(gener_att)
|
|
98
|
-
feature =
|
|
99
|
-
id
|
|
100
|
-
featuretype
|
|
101
|
-
properties
|
|
102
|
-
geometry
|
|
103
|
-
appschema
|
|
104
|
-
version
|
|
105
|
-
|
|
106
|
-
if
|
|
107
|
-
feature
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
100
|
+
feature = {
|
|
101
|
+
"id": id,
|
|
102
|
+
"featuretype": self.get_name(),
|
|
103
|
+
"properties": properties,
|
|
104
|
+
"geometry": geometry,
|
|
105
|
+
"appschema": self.get_appschema(),
|
|
106
|
+
"version": self.get_version(),
|
|
107
|
+
}
|
|
108
|
+
if bulk_mode:
|
|
109
|
+
return feature, [*refs, *refs_inv]
|
|
110
|
+
else:
|
|
111
|
+
orm_feature = Feature(**feature)
|
|
112
|
+
if refs:
|
|
113
|
+
orm_feature.refs = [Refs(**ref) for ref in refs]
|
|
114
|
+
if refs_inv:
|
|
115
|
+
orm_feature.refs_inv = [Refs(**ref) for ref in refs_inv]
|
|
116
|
+
return orm_feature
|
|
111
117
|
|
|
112
118
|
@classmethod
|
|
113
119
|
def _from_coretable(cls, feature: Feature) -> dict:
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""Module containing the GMLAdapter for reading from and writing to gml."""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
from typing import Literal
|
|
5
4
|
from uuid import uuid4
|
|
6
5
|
|
|
7
6
|
from lxml import etree
|
|
@@ -19,7 +18,6 @@ class GMLAdapter:
|
|
|
19
18
|
|
|
20
19
|
def _to_etree(
|
|
21
20
|
self,
|
|
22
|
-
appschema: Literal["xplan", "xtrasse", "plu"] = "xplan",
|
|
23
21
|
**kwargs,
|
|
24
22
|
) -> etree._Element:
|
|
25
23
|
"""Converts XPlan and INSPIRE PLU object to lxml etree Element."""
|
|
@@ -62,7 +60,7 @@ class GMLAdapter:
|
|
|
62
60
|
"FORMAT=GML32",
|
|
63
61
|
f"GMLID=GML_{uuid4()}",
|
|
64
62
|
"SRSNAME_FORMAT=OGC_URL"
|
|
65
|
-
if
|
|
63
|
+
if self.get_appschema() == "plu"
|
|
66
64
|
else "GML3_LONGSRS=NO",
|
|
67
65
|
"NAMESPACE_DECL=YES",
|
|
68
66
|
]
|
|
@@ -125,11 +123,11 @@ class GMLAdapter:
|
|
|
125
123
|
if isinstance(model_value, list):
|
|
126
124
|
value_item = model_value[index]
|
|
127
125
|
etree.SubElement(feature, gml_name).append(
|
|
128
|
-
value_item._to_etree(
|
|
126
|
+
value_item._to_etree()
|
|
129
127
|
)
|
|
130
128
|
else:
|
|
131
129
|
etree.SubElement(feature, gml_name).append(
|
|
132
|
-
model_value._to_etree(
|
|
130
|
+
model_value._to_etree()
|
|
133
131
|
)
|
|
134
132
|
|
|
135
133
|
ns = self.namespace_uri.replace("base/4.0", "base/3.3")
|