Coverage for gws-app/gws/plugin/alkis/data/exporter.py: 0%
139 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-24 12:46 +0200
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-24 12:46 +0200
1"""ALKIS exporter.
3Export Flurstuecke to CSV or GeoJSON.
4"""
6from typing import Iterable, Optional, cast
8import gws
9import gws.base.feature
10import gws.base.model
11import gws.lib.intl
12import gws.lib.mime
13import gws.lib.jsonx
14import gws.plugin.csv_helper
16from gws.lib.cli import ProgressIndicator
17from . import types as dt
18from . import index
21class Config(gws.ConfigWithAccess):
22 """Export configuration"""
24 type: str
25 """Export type."""
26 title: Optional[str]
27 """Title to display in the ui."""
28 model: Optional[gws.ext.config.model]
29 """Export model."""
30 models: Optional[list[gws.ext.config.model]]
31 """Export models."""
34class Args(gws.Data):
35 """Arguments for the export operation."""
37 fsList: Iterable[dt.Flurstueck]
38 """Iterable of Flurstuecke to export."""
39 user: gws.User
40 """User who requested the export."""
41 progress: Optional[ProgressIndicator]
42 """Progress indicator to update during export."""
43 path: str
44 """Path to save the export."""
45 models: list['Model']
46 """List of models to export."""
49_DEFAULT_FIELDS = [
50 gws.Config(type='text', name='fs_flurstueckskennzeichen', title='Flurstückskennzeichen'),
51 gws.Config(type='text', name='fs_recs_gemeinde_text', title='Gemeinde'),
52 gws.Config(type='text', name='fs_recs_gemarkung_code', title='Gemarkungsnummer'),
53 gws.Config(type='text', name='fs_recs_gemarkung_text', title='Gemarkung'),
54 gws.Config(type='text', name='fs_recs_flurnummer', title='Flurnummer'),
55 gws.Config(type='text', name='fs_recs_zaehler', title='Zähler'),
56 gws.Config(type='text', name='fs_recs_nenner', title='Nenner'),
57 gws.Config(type='text', name='fs_recs_flurstuecksfolge', title='Folge'),
58 gws.Config(type='float', name='fs_recs_amtlicheFlaeche', title='Fläche'),
59 gws.Config(type='float', name='fs_recs_x', title='X'),
60 gws.Config(type='float', name='fs_recs_y', title='Y'),
61]
64class Model(gws.base.model.Object):
65 withEigentuemer: bool
66 withBuchung: bool
68 def configure(self):
69 self.configure_model()
72class ModelProps(gws.Props):
73 title: str
76class Props(gws.Props):
77 title: str
78 models: list[ModelProps]
81_READ_WRITE_PERMISSIONS = gws.Config(read='allow all', write='allow all')
84class Object(gws.Node):
85 models: list[Model]
86 title: str
87 type: str
88 mimeType: str
89 usedKeys: set[str]
91 def configure(self):
92 self.type = self.cfg('type') or 'csv'
93 if self.type == 'csv':
94 self.mimeType = gws.lib.mime.CSV
95 elif self.type == 'geojson':
96 self.mimeType = gws.lib.mime.JSON
97 else:
98 raise gws.ConfigurationError(f'Unsupported export type: {self.type}')
100 self.title = self.cfg('title') or self.type
102 p = self.cfg('models')
103 if not p:
104 p = [self.cfg('model') or gws.Config(fields=_DEFAULT_FIELDS)]
106 self.models = []
107 for m in p:
108 mod = cast(
109 Model,
110 self.create_child(
111 Model,
112 m,
113 # NB need write permissions for `feature.to_record`
114 permissions=_READ_WRITE_PERMISSIONS,
115 ),
116 )
117 mod.withEigentuemer = any('eigentuemer' in fld.name for fld in mod.fields)
118 mod.withBuchung = any('buchung' in fld.name for fld in mod.fields)
119 self.models.append(mod)
121 def props_with_flags(self, user: gws.User, withEigentuemer: bool, withBuchung: bool) -> Optional[Props]:
122 if not user.can_use(self):
123 return
125 models = []
126 for mod in self.get_models(user):
127 if not user.can_use(mod):
128 continue
129 if mod.withEigentuemer and not withEigentuemer:
130 continue
131 if mod.withBuchung and not withBuchung:
132 continue
133 models.append(ModelProps(uid=mod.uid, title=mod.title))
134 if not models:
135 return
137 return Props(uid=self.uid, title=self.title, models=models)
139 def get_models(self, user: gws.User, uids: Optional[list[str]] = None) -> list[Model]:
140 if not uids:
141 ms = self.models
142 else:
143 s = set(uids)
144 ms = []
145 for m in self.models:
146 if m.uid in s:
147 ms.append(m)
148 s.remove(m.uid)
149 if s:
150 gws.log.warning(f'Unknown model uids: {s}')
152 return [m for m in ms if user.can_use(m)]
154 def run(self, args: Args):
155 """Export a Flurstueck list to a file."""
157 if self.type == 'csv':
158 return self._export_csv(args)
159 if self.type == 'geojson':
160 return self._export_geojson(args)
161 raise gws.NotFoundError(f'Unsupported export format')
163 def _export_csv(self, args: Args):
164 csv_helper = cast(gws.plugin.csv_helper.Object, self.root.app.helper('csv'))
166 with open(args.path, 'wb') as fp:
167 writer = csv_helper.writer(gws.lib.intl.locale('de_DE'), stream_to=fp)
168 for row in self._iter_rows(args):
169 writer.write_dict(row)
171 def _export_geojson(self, args: Args):
172 with open(args.path, 'wb') as fp:
173 fp.write(b'{"type": "FeatureCollection", "features": [')
174 comma = b'\n '
175 for row in self._iter_rows(args, with_geometry=True):
176 shape = row.pop('fs_shape', None)
177 d = dict(
178 type='Feature',
179 properties=row,
180 geometry=cast(gws.Shape, shape).to_geojson() if shape else None,
181 )
182 fp.write(comma + gws.lib.jsonx.to_string(d, ensure_ascii=False).encode('utf8'))
183 comma = b',\n '
185 fp.write(b'\n]}\n')
187 def _iter_rows(self, args: Args, with_geometry=False):
188 """Iterate over a Flurstueck list and yield flat rows (dicts).
190 The Flurstueck structure, as created by our indexer, is deeply nested.
191 We flatten it, creating a dict 'nested_key->value'. For list values, we repeat the dict
192 for each item in the list, thus creating a product of all lists, e.g.
194 record:
195 a:x, b:[1,2], c:[3,4]
197 flat list:
198 a:x, b:1, c:3
199 a:x, b:1, c:4
200 a:x, b:2, c:3
201 a:x, b:2, c:4
203 @TODO: with certain combinations of keys this can explode very quickly
204 """
206 if len(args.models) == 1:
207 export_model = args.models[0]
208 else:
209 export_model = cast(
210 Model,
211 self.root.create_temporary(
212 Model,
213 gws.Config(fields=[]),
214 permissions=_READ_WRITE_PERMISSIONS,
215 ),
216 )
217 for mod in args.models:
218 export_model.fields.extend(mod.fields)
220 all_keys = set(fld.name for fld in export_model.fields)
221 mc = gws.ModelContext(
222 op=gws.ModelOperation.read,
223 target=gws.ModelReadTarget.searchResults,
224 user=args.user,
225 )
226 row_hashes = set()
228 for fs in args.fsList:
229 if args.progress:
230 args.progress.update(1)
232 for atts in index.flatten_fs(fs, all_keys):
233 # create a 'raw' feature from attributes and convert it to a record
234 # so that dynamic fields can be computed
236 feature = gws.base.feature.new(model=export_model, attributes=atts)
237 for fld in export_model.fields:
238 fld.to_record(feature, mc)
240 # fmt: off
241 row = {
242 fld.title: feature.record.attributes.get(fld.name, '')
243 for fld in export_model.fields
244 if not fld.isHidden
245 }
246 # fmt: on
248 h = gws.u.sha256(row)
249 if h in row_hashes:
250 continue
252 row_hashes.add(h)
253 if with_geometry:
254 row['fs_shape'] = fs.shape
255 yield row