Coverage for gws-app/gws/plugin/alkis/data/exporter.py: 0%
92 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-16 22:59 +0200
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-16 22:59 +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. (added in 8.2)"""
26 title: Optional[str]
27 """Title to display in the ui. (added in 8.2)"""
28 model: Optional[gws.ext.config.model]
29 """Export model. (added in 8.2)"""
32class Args(gws.Data):
33 """Arguments for the export operation."""
35 fsList: Iterable[dt.Flurstueck]
36 """Iterable of Flurstuecke to export."""
37 user: gws.User
38 """User who requested the export."""
39 progress: Optional[ProgressIndicator]
40 """Progress indicator to update during export."""
41 path: str
42 """Path to save the export."""
45_DEFAULT_FIELDS = [
46 gws.Config(type='text', name='fs_flurstueckskennzeichen', title='Flurstückskennzeichen'),
47 gws.Config(type='text', name='fs_recs_gemeinde_text', title='Gemeinde'),
48 gws.Config(type='text', name='fs_recs_gemarkung_code', title='Gemarkungsnummer'),
49 gws.Config(type='text', name='fs_recs_gemarkung_text', title='Gemarkung'),
50 gws.Config(type='text', name='fs_recs_flurnummer', title='Flurnummer'),
51 gws.Config(type='text', name='fs_recs_zaehler', title='Zähler'),
52 gws.Config(type='text', name='fs_recs_nenner', title='Nenner'),
53 gws.Config(type='text', name='fs_recs_flurstuecksfolge', title='Folge'),
54 gws.Config(type='float', name='fs_recs_amtlicheFlaeche', title='Fläche'),
55 gws.Config(type='float', name='fs_recs_x', title='X'),
56 gws.Config(type='float', name='fs_recs_y', title='Y'),
57]
60class Model(gws.base.model.Object):
61 def configure(self):
62 self.configure_model()
65class Object(gws.Node):
66 model: Model
67 title: str
68 type: str
69 mimeType: str
70 usedKeys: set[str]
71 withEigentuemer: bool
72 withBuchung: bool
74 def configure(self):
75 self.type = self.cfg('type') or 'csv'
76 if self.type == 'csv':
77 self.mimeType = gws.lib.mime.CSV
78 elif self.type == 'geojson':
79 self.mimeType = gws.lib.mime.JSON
80 else:
81 raise gws.ConfigurationError(f'Unsupported export type: {self.type}')
83 self.title = self.cfg('title') or self.type
85 p = self.cfg('model') or gws.Config(fields=_DEFAULT_FIELDS)
86 self.model = cast(
87 Model,
88 self.create_child(
89 Model,
90 p,
91 # NB need write permissions for `feature.to_record`
92 permissions=gws.Config(read='allow all', write='allow all'),
93 ),
94 )
96 self.withEigentuemer = any('namensnummer' in fld.name for fld in self.model.fields)
97 self.withBuchung = any('buchung' in fld.name for fld in self.model.fields)
99 def run(self, args: Args):
100 """Export a Flurstueck list to a file."""
102 if self.type == 'csv':
103 return self._export_csv(args)
104 if self.type == 'geojson':
105 return self._export_geojson(args)
106 raise gws.NotFoundError(f'Unsupported export format')
108 def _export_csv(self, args: Args):
109 csv_helper = cast(gws.plugin.csv_helper.Object, self.root.app.helper('csv'))
111 with open(args.path, 'wb') as fp:
112 writer = csv_helper.writer(gws.lib.intl.locale('de_DE'), stream_to=fp)
113 for row in self._iter_rows(args):
114 writer.write_dict(row)
116 def _export_geojson(self, args: Args):
117 with open(args.path, 'wb') as fp:
118 fp.write(b'{"type": "FeatureCollection", "features": [')
119 comma = b'\n '
120 for row in self._iter_rows(args, with_geometry=True):
121 shape = row.pop('fs_shape', None)
122 d = dict(
123 type='Feature',
124 properties=row,
125 geometry=cast(gws.Shape, shape).to_geojson() if shape else None,
126 )
127 fp.write(comma + gws.lib.jsonx.to_string(d, ensure_ascii=False).encode('utf8'))
128 comma = b',\n '
130 fp.write(b'\n]}\n')
132 def _iter_rows(self, args: Args, with_geometry=False):
133 """Iterate over a Flurstueck list and yield flat rows (dicts).
135 The Flurstueck structure, as created by our indexer, is deeply nested.
136 We flatten it, creating a dict 'nested_key->value'. For list values, we repeat the dict
137 for each item in the list, thus creating a product of all lists, e.g.
139 record:
140 a:x, b:[1,2], c:[3,4]
142 flat list:
143 a:x, b:1, c:3
144 a:x, b:1, c:4
145 a:x, b:2, c:3
146 a:x, b:2, c:4
148 @TODO: with certain combinations of keys this can explode very quickly
149 """
151 all_keys = set(fld.name for fld in self.model.fields)
152 mc = gws.ModelContext(op=gws.ModelOperation.read, target=gws.ModelReadTarget.searchResults, user=args.user)
153 row_hashes = set()
155 for fs in args.fsList:
156 if args.progress:
157 args.progress.update(1)
159 for atts in index.flatten_fs(fs, all_keys):
160 # create a 'raw' feature from attributes and convert it to a record
161 # so that dynamic fields can be computed
163 feature = gws.base.feature.new(model=self.model, attributes=atts)
164 for fld in self.model.fields:
165 fld.to_record(feature, mc)
167 # fmt: off
168 row = {
169 fld.title: feature.record.attributes.get(fld.name, '')
170 for fld in self.model.fields
171 if not fld.isHidden
172 }
173 # fmt: on
175 h = gws.u.sha256(row)
176 if h in row_hashes:
177 continue
179 row_hashes.add(h)
180 if with_geometry:
181 row['fs_shape'] = fs.shape
182 yield row