threedi-modelchecker 2.6.3.dev1__tar.gz → 2.7.1.dev0__tar.gz

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.
Files changed (44) hide show
  1. {threedi-modelchecker-2.6.3.dev1 → threedi-modelchecker-2.7.1.dev0}/CHANGES.rst +8 -1
  2. {threedi-modelchecker-2.6.3.dev1/threedi_modelchecker.egg-info → threedi-modelchecker-2.7.1.dev0}/PKG-INFO +2 -2
  3. {threedi-modelchecker-2.6.3.dev1 → threedi-modelchecker-2.7.1.dev0}/pyproject.toml +1 -1
  4. {threedi-modelchecker-2.6.3.dev1 → threedi-modelchecker-2.7.1.dev0}/threedi_modelchecker/__init__.py +1 -1
  5. {threedi-modelchecker-2.6.3.dev1 → threedi-modelchecker-2.7.1.dev0}/threedi_modelchecker/checks/geo_query.py +1 -1
  6. {threedi-modelchecker-2.6.3.dev1 → threedi-modelchecker-2.7.1.dev0}/threedi_modelchecker/checks/other.py +65 -34
  7. {threedi-modelchecker-2.6.3.dev1 → threedi-modelchecker-2.7.1.dev0}/threedi_modelchecker/checks/raster.py +34 -11
  8. {threedi-modelchecker-2.6.3.dev1 → threedi-modelchecker-2.7.1.dev0}/threedi_modelchecker/config.py +305 -476
  9. {threedi-modelchecker-2.6.3.dev1 → threedi-modelchecker-2.7.1.dev0}/threedi_modelchecker/interfaces/raster_interface.py +4 -0
  10. {threedi-modelchecker-2.6.3.dev1 → threedi-modelchecker-2.7.1.dev0}/threedi_modelchecker/interfaces/raster_interface_gdal.py +7 -0
  11. {threedi-modelchecker-2.6.3.dev1 → threedi-modelchecker-2.7.1.dev0}/threedi_modelchecker/interfaces/raster_interface_rasterio.py +4 -0
  12. {threedi-modelchecker-2.6.3.dev1 → threedi-modelchecker-2.7.1.dev0}/threedi_modelchecker/model_checks.py +1 -0
  13. threedi-modelchecker-2.7.1.dev0/threedi_modelchecker/tests/data/empty.gpkg +0 -0
  14. {threedi-modelchecker-2.6.3.dev1 → threedi-modelchecker-2.7.1.dev0}/threedi_modelchecker/tests/factories.py +31 -34
  15. {threedi-modelchecker-2.6.3.dev1 → threedi-modelchecker-2.7.1.dev0}/threedi_modelchecker/tests/test_checks_base.py +43 -124
  16. {threedi-modelchecker-2.6.3.dev1 → threedi-modelchecker-2.7.1.dev0}/threedi_modelchecker/tests/test_checks_factories.py +1 -1
  17. {threedi-modelchecker-2.6.3.dev1 → threedi-modelchecker-2.7.1.dev0}/threedi_modelchecker/tests/test_checks_other.py +76 -14
  18. {threedi-modelchecker-2.6.3.dev1 → threedi-modelchecker-2.7.1.dev0}/threedi_modelchecker/tests/test_checks_raster.py +56 -54
  19. {threedi-modelchecker-2.6.3.dev1 → threedi-modelchecker-2.7.1.dev0/threedi_modelchecker.egg-info}/PKG-INFO +2 -2
  20. {threedi-modelchecker-2.6.3.dev1 → threedi-modelchecker-2.7.1.dev0}/threedi_modelchecker.egg-info/requires.txt +1 -1
  21. threedi-modelchecker-2.6.3.dev1/threedi_modelchecker/tests/data/empty.gpkg +0 -0
  22. {threedi-modelchecker-2.6.3.dev1 → threedi-modelchecker-2.7.1.dev0}/LICENSE +0 -0
  23. {threedi-modelchecker-2.6.3.dev1 → threedi-modelchecker-2.7.1.dev0}/MANIFEST.in +0 -0
  24. {threedi-modelchecker-2.6.3.dev1 → threedi-modelchecker-2.7.1.dev0}/README.rst +0 -0
  25. {threedi-modelchecker-2.6.3.dev1 → threedi-modelchecker-2.7.1.dev0}/setup.cfg +0 -0
  26. {threedi-modelchecker-2.6.3.dev1 → threedi-modelchecker-2.7.1.dev0}/threedi_modelchecker/checks/__init__.py +0 -0
  27. {threedi-modelchecker-2.6.3.dev1 → threedi-modelchecker-2.7.1.dev0}/threedi_modelchecker/checks/base.py +0 -0
  28. {threedi-modelchecker-2.6.3.dev1 → threedi-modelchecker-2.7.1.dev0}/threedi_modelchecker/checks/cross_section_definitions.py +0 -0
  29. {threedi-modelchecker-2.6.3.dev1 → threedi-modelchecker-2.7.1.dev0}/threedi_modelchecker/checks/factories.py +0 -0
  30. {threedi-modelchecker-2.6.3.dev1 → threedi-modelchecker-2.7.1.dev0}/threedi_modelchecker/checks/timeseries.py +0 -0
  31. {threedi-modelchecker-2.6.3.dev1 → threedi-modelchecker-2.7.1.dev0}/threedi_modelchecker/exporters.py +0 -0
  32. {threedi-modelchecker-2.6.3.dev1 → threedi-modelchecker-2.7.1.dev0}/threedi_modelchecker/interfaces/__init__.py +0 -0
  33. {threedi-modelchecker-2.6.3.dev1 → threedi-modelchecker-2.7.1.dev0}/threedi_modelchecker/scripts.py +0 -0
  34. {threedi-modelchecker-2.6.3.dev1 → threedi-modelchecker-2.7.1.dev0}/threedi_modelchecker/tests/__init__.py +0 -0
  35. {threedi-modelchecker-2.6.3.dev1 → threedi-modelchecker-2.7.1.dev0}/threedi_modelchecker/tests/conftest.py +0 -0
  36. {threedi-modelchecker-2.6.3.dev1 → threedi-modelchecker-2.7.1.dev0}/threedi_modelchecker/tests/data/__init__.py +0 -0
  37. {threedi-modelchecker-2.6.3.dev1 → threedi-modelchecker-2.7.1.dev0}/threedi_modelchecker/tests/test_checks_cross_section_definitions.py +0 -0
  38. {threedi-modelchecker-2.6.3.dev1 → threedi-modelchecker-2.7.1.dev0}/threedi_modelchecker/tests/test_checks_timeseries.py +0 -0
  39. {threedi-modelchecker-2.6.3.dev1 → threedi-modelchecker-2.7.1.dev0}/threedi_modelchecker/tests/test_exporters.py +0 -0
  40. {threedi-modelchecker-2.6.3.dev1 → threedi-modelchecker-2.7.1.dev0}/threedi_modelchecker/tests/test_model_checks.py +0 -0
  41. {threedi-modelchecker-2.6.3.dev1 → threedi-modelchecker-2.7.1.dev0}/threedi_modelchecker.egg-info/SOURCES.txt +0 -0
  42. {threedi-modelchecker-2.6.3.dev1 → threedi-modelchecker-2.7.1.dev0}/threedi_modelchecker.egg-info/dependency_links.txt +0 -0
  43. {threedi-modelchecker-2.6.3.dev1 → threedi-modelchecker-2.7.1.dev0}/threedi_modelchecker.egg-info/entry_points.txt +0 -0
  44. {threedi-modelchecker-2.6.3.dev1 → threedi-modelchecker-2.7.1.dev0}/threedi_modelchecker.egg-info/top_level.txt +0 -0
@@ -2,7 +2,14 @@ Changelog of threedi-modelchecker
2
2
  =================================
3
3
 
4
4
 
5
- 2.6.3 (unreleased)
5
+ 2.7.1 (unreleased)
6
+ ------------------
7
+ - Add info check 1406 to inform the user if a raster is not compressed.
8
+ - Add check 799 to warn if raster friction pixels are < 1 while Chezy friction is selected
9
+ - Change error message for check 1500
10
+
11
+
12
+ 2.7.0 (2024-03-12)
6
13
  ------------------
7
14
  - Support geopackage
8
15
  - Support changes in threedi-schema (0.220) needed for geopackage support
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: threedi-modelchecker
3
- Version: 2.6.3.dev1
3
+ Version: 2.7.1.dev0
4
4
  Summary: Checks validity of a 3Di schematisation
5
5
  Author-email: Nelen & Schuurmans <info@nelen-schuurmans.nl>
6
6
  License: MIT
@@ -15,7 +15,7 @@ License-File: LICENSE
15
15
  Requires-Dist: Click
16
16
  Requires-Dist: GeoAlchemy2!=0.11.*,>=0.9
17
17
  Requires-Dist: SQLAlchemy>=1.4
18
- Requires-Dist: threedi-schema==0.220.*
18
+ Requires-Dist: threedi-schema==0.222.dev0
19
19
  Provides-Extra: rasterio
20
20
  Requires-Dist: rasterio>=1.3; extra == "rasterio"
21
21
  Provides-Extra: test
@@ -19,7 +19,7 @@ dependencies = [
19
19
  "Click",
20
20
  "GeoAlchemy2>=0.9,!=0.11.*",
21
21
  "SQLAlchemy>=1.4",
22
- "threedi-schema==0.220.*"
22
+ "threedi-schema==0.222.dev0"
23
23
  ]
24
24
 
25
25
  [project.optional-dependencies]
@@ -1,5 +1,5 @@
1
1
  from .model_checks import * # NOQA
2
2
 
3
3
  # fmt: off
4
- __version__ = '2.6.3.dev1'
4
+ __version__ = '2.7.1.dev0'
5
5
  # fmt: on
@@ -8,7 +8,7 @@ DEFAULT_EPSG = 28992
8
8
 
9
9
 
10
10
  def epsg_code_query():
11
- epsg_code = Query(models.GlobalSetting.epsg_code).limit(1).scalar_subquery()
11
+ epsg_code = Query(models.ModelSettings.epsg_code).limit(1).scalar_subquery()
12
12
  return func.coalesce(epsg_code, literal(DEFAULT_EPSG)).label("epsg_code")
13
13
 
14
14
 
@@ -10,15 +10,6 @@ from .base import BaseCheck, CheckLevel
10
10
  from .cross_section_definitions import cross_section_configuration
11
11
  from .geo_query import distance, length, transform
12
12
 
13
- # Use these to make checks only work on the first global settings entry:
14
- first_setting = (
15
- Query(models.GlobalSetting.id)
16
- .order_by(models.GlobalSetting.id)
17
- .limit(1)
18
- .scalar_subquery()
19
- )
20
- first_setting_filter = models.GlobalSetting.id == first_setting
21
-
22
13
 
23
14
  class CorrectAggregationSettingsExist(BaseCheck):
24
15
  """Check if aggregation settings are correctly filled with aggregation_method and flow_variable as required"""
@@ -30,30 +21,24 @@ class CorrectAggregationSettingsExist(BaseCheck):
30
21
  *args,
31
22
  **kwargs,
32
23
  ):
33
- super().__init__(column=models.GlobalSetting.id, *args, **kwargs)
24
+ super().__init__(column=models.ModelSettings.id, *args, **kwargs)
34
25
  self.aggregation_method = aggregation_method.value
35
26
  self.flow_variable = flow_variable.value
36
27
 
37
28
  def get_invalid(self, session: Session) -> List[NamedTuple]:
38
- global_settings = self.to_check(session).filter(first_setting_filter)
29
+ global_settings = self.to_check(session)
39
30
  correctly_defined = session.execute(
40
- select(models.AggregationSettings)
41
- .filter(
31
+ select(models.AggregationSettings).filter(
42
32
  models.AggregationSettings.aggregation_method
43
33
  == self.aggregation_method,
44
34
  models.AggregationSettings.flow_variable == self.flow_variable,
45
35
  )
46
- .filter(
47
- models.AggregationSettings.global_settings_id
48
- == global_settings.subquery().c.id
49
- )
50
36
  ).all()
51
-
52
37
  return global_settings.all() if len(correctly_defined) == 0 else []
53
38
 
54
39
  def description(self) -> str:
55
40
  return (
56
- "To use the water balance tool, v2_aggregation_settings should have a row where "
41
+ "To use the water balance tool, aggregation_settings should have a row where "
57
42
  f"aggregation_method is {self.aggregation_method} and flow_variable is {self.flow_variable}."
58
43
  )
59
44
 
@@ -244,12 +229,14 @@ class Use0DFlowCheck(BaseCheck):
244
229
  """
245
230
 
246
231
  def __init__(self, *args, **kwargs):
247
- super().__init__(column=models.GlobalSetting.use_0d_inflow, *args, **kwargs)
232
+ super().__init__(
233
+ column=models.SimulationTemplateSettings.use_0d_inflow, *args, **kwargs
234
+ )
248
235
 
249
236
  def to_check(self, session):
250
237
  """Return a Query object on which this check is applied"""
251
- return session.query(models.GlobalSetting).filter(
252
- models.GlobalSetting.use_0d_inflow != 0
238
+ return session.query(models.SimulationTemplateSettings).filter(
239
+ models.SimulationTemplateSettings.use_0d_inflow != 0
253
240
  )
254
241
 
255
242
  def get_invalid(self, session):
@@ -301,7 +288,7 @@ class ConnectionNodes(BaseCheck):
301
288
  class ConnectionNodesLength(BaseCheck):
302
289
  """Check that the distance between `start_node` and `end_node` is at least
303
290
  `min_distance`. The coords will be transformed into (the first entry) of
304
- GlobalSettings.epsg_code.
291
+ ModelSettings.epsg_code.
305
292
  """
306
293
 
307
294
  def __init__(
@@ -469,7 +456,7 @@ class ChannelManholeLevelCheck(BaseCheck):
469
456
 
470
457
  class OpenChannelsWithNestedNewton(BaseCheck):
471
458
  """Checks whether the model has any closed cross-section in use when the
472
- NumericalSettings.use_of_nested_newton is turned off.
459
+ NumericalSettings.use_nested_newton is turned off.
473
460
 
474
461
  See https://github.com/nens/threeditoolbox/issues/522
475
462
  """
@@ -479,7 +466,7 @@ class OpenChannelsWithNestedNewton(BaseCheck):
479
466
  column=models.CrossSectionDefinition.id,
480
467
  level=level,
481
468
  filters=Query(models.NumericalSettings)
482
- .filter(models.NumericalSettings.use_of_nested_newton == 0)
469
+ .filter(models.NumericalSettings.use_nested_newton == 0)
483
470
  .exists(),
484
471
  *args,
485
472
  **kwargs,
@@ -532,8 +519,8 @@ class OpenChannelsWithNestedNewton(BaseCheck):
532
519
  def description(self) -> str:
533
520
  return (
534
521
  f"{self.column_name} has a closed cross section definition while "
535
- f"NumericalSettings.use_of_nested_newton is switched off. "
536
- f"This gives convergence issues. We recommend setting use_of_nested_newton = 1."
522
+ f"NumericalSettings.use_nested_newton is switched off. "
523
+ f"This gives convergence issues. We recommend setting use_nested_newton = 1."
537
524
  )
538
525
 
539
526
 
@@ -731,7 +718,8 @@ class PumpStorageTimestepCheck(BaseCheck):
731
718
  .filter(
732
719
  (models.ConnectionNode.storage_area != None)
733
720
  & (
734
- ( # calculate how many seconds the pumpstation takes to empty its storage: (storage * height)/pump capacity
721
+ (
722
+ # calculate how many seconds the pumpstation takes to empty its storage: (storage * height)/pump capacity
735
723
  (
736
724
  # Arithmetic operations on None return None, so without this
737
725
  # conditional type cast, no invalid results would be returned
@@ -744,9 +732,7 @@ class PumpStorageTimestepCheck(BaseCheck):
744
732
  )
745
733
  )
746
734
  / (models.Pumpstation.capacity / 1000)
747
- < Query(models.GlobalSetting.sim_time_step)
748
- .filter(first_setting_filter)
749
- .scalar_subquery()
735
+ < Query(models.TimeStepSettings.time_step).scalar_subquery()
750
736
  )
751
737
  )
752
738
  .all()
@@ -819,7 +805,7 @@ class InflowNoFeaturesCheck(BaseCheck):
819
805
  """Check that the surface table in the global use_0d_inflow setting contains at least 1 feature."""
820
806
 
821
807
  def __init__(self, *args, surface_table, condition=True, **kwargs):
822
- super().__init__(*args, column=models.GlobalSetting.id, **kwargs)
808
+ super().__init__(*args, column=models.ModelSettings.id, **kwargs)
823
809
  self.surface_table = surface_table
824
810
  self.condition = condition
825
811
 
@@ -828,13 +814,13 @@ class InflowNoFeaturesCheck(BaseCheck):
828
814
  select(func.count(self.surface_table.id))
829
815
  ).scalar()
830
816
  return (
831
- session.query(models.GlobalSetting)
817
+ session.query(models.ModelSettings)
832
818
  .filter(self.condition, surface_table_length == 0)
833
819
  .all()
834
820
  )
835
821
 
836
822
  def description(self) -> str:
837
- return f"v2_global_settings.use_0d_inflow is set to use {self.surface_table.__tablename__}, but {self.surface_table.__tablename__} does not contain any features."
823
+ return f"model_settings.use_0d_inflow is set to use {self.surface_table.__tablename__}, but {self.surface_table.__tablename__} does not contain any features."
838
824
 
839
825
 
840
826
  class NodeSurfaceConnectionsCheck(BaseCheck):
@@ -1082,3 +1068,48 @@ class AllPresentVariableVegetationParameters(AllPresent):
1082
1068
  )
1083
1069
  )
1084
1070
  )
1071
+
1072
+
1073
+ class UsedSettingsPresentCheck(BaseCheck):
1074
+ def __init__(
1075
+ self,
1076
+ column,
1077
+ settings_table,
1078
+ filters=None,
1079
+ level=CheckLevel.ERROR,
1080
+ error_code=0,
1081
+ ):
1082
+ super().__init__(column, filters, level, error_code)
1083
+ self.settings_table = settings_table
1084
+
1085
+ def get_invalid(self, session: Session) -> List[NamedTuple]:
1086
+ # more than 1 row should be caught by another check
1087
+ all_results = self.to_check(session).filter(self.column == True).all()
1088
+ use_cols = len(all_results) > 0
1089
+ if use_cols and session.query(self.settings_table).count() == 0:
1090
+ return all_results
1091
+ return []
1092
+
1093
+ def description(self) -> str:
1094
+ return f"{self.column_name} in {self.table.name} is set to True but {self.settings_table.__tablename__} is empty"
1095
+
1096
+
1097
+ class MaxOneRecordCheck(BaseCheck):
1098
+ def __init__(self, column, filters=None, level=CheckLevel.ERROR, error_code=0):
1099
+ super().__init__(column, filters, level, error_code)
1100
+ self.observed_length = 0
1101
+
1102
+ def get_invalid(self, session: Session) -> List[NamedTuple]:
1103
+ # return mock list in case the table is empty when it shouldn't be
1104
+ all_results = self.to_check(session).all()
1105
+ self.observed_length = len(all_results)
1106
+ if self.observed_length > 1:
1107
+ return all_results if self.observed_length > 0 else ["foo"]
1108
+ else:
1109
+ return []
1110
+
1111
+ def description(self) -> str:
1112
+ return (
1113
+ f"{self.table.name} has {self.observed_length} rows, "
1114
+ f"but should have at most 1 row."
1115
+ )
@@ -1,7 +1,7 @@
1
1
  from dataclasses import dataclass
2
2
  from math import isclose
3
3
  from pathlib import Path
4
- from typing import Optional, Set, Type
4
+ from typing import Dict, Optional, Type
5
5
 
6
6
  from threedi_schema import models
7
7
 
@@ -9,6 +9,12 @@ from threedi_modelchecker.interfaces import GDALRasterInterface, RasterInterface
9
9
 
10
10
  from .base import BaseCheck
11
11
 
12
+ # interface_cls.is_valid_geotiff returns False if the dataset is empty
13
+ # (such as when the column doesn't reference a file) or the dataset is not a valid geotiff.
14
+ # It's used on every raster check which reads the raster file in any way,
15
+ # to prevent any other code in the check from executing on a bad file.
16
+ # On such datasets, RasterExistsCheck and/or RasterIsValidCheck will be raised.
17
+
12
18
 
13
19
  class Context:
14
20
  pass
@@ -16,7 +22,7 @@ class Context:
16
22
 
17
23
  @dataclass
18
24
  class ServerContext(Context):
19
- available_rasters: Set[str]
25
+ available_rasters: Dict[str, str]
20
26
  raster_interface: Type[RasterInterface] = GDALRasterInterface
21
27
 
22
28
 
@@ -62,7 +68,9 @@ class BaseRasterCheck(BaseCheck):
62
68
  ]
63
69
 
64
70
  def get_path_local(self, record, context: LocalContext) -> Optional[str]:
65
- abs_path = context.base_path / getattr(record, self.column.name)
71
+ abs_path = context.base_path.joinpath(
72
+ "rasters", getattr(record, self.column.name)
73
+ )
66
74
  if abs_path.exists():
67
75
  return str(abs_path)
68
76
 
@@ -153,7 +161,7 @@ class RasterHasMatchingEPSGCheck(BaseRasterCheck):
153
161
  """Check whether a raster's EPSG code matches the EPSG code in the global settings for the SQLite."""
154
162
 
155
163
  def get_invalid(self, session):
156
- epsg_code_query = session.query(models.GlobalSetting.epsg_code).first()
164
+ epsg_code_query = session.query(models.ModelSettings.epsg_code).first()
157
165
  if epsg_code_query is not None:
158
166
  self.epsg_code = epsg_code_query[0]
159
167
  else:
@@ -169,7 +177,7 @@ class RasterHasMatchingEPSGCheck(BaseRasterCheck):
169
177
  return raster.epsg_code == self.epsg_code
170
178
 
171
179
  def description(self):
172
- return f"The file in {self.column_name} has no EPSG code or the EPSG code does not match does not match v2_global_settings.epsg_code"
180
+ return f"The file in {self.column_name} has no EPSG code or the EPSG code does not match does not match model_settings.epsg_code"
173
181
 
174
182
 
175
183
  class RasterSquareCellsCheck(BaseRasterCheck):
@@ -196,9 +204,11 @@ class RasterGridSizeCheck(BaseRasterCheck):
196
204
  """Check whether the global settings' grid size is an even multiple of a raster's cell size (at least 2x)."""
197
205
 
198
206
  def get_invalid(self, session):
199
- grid_space_query = session.query(models.GlobalSetting.grid_space).first()
200
- if grid_space_query is not None:
201
- self.grid_space = grid_space_query[0]
207
+ minimum_cell_size_query = session.query(
208
+ models.ModelSettings.minimum_cell_size
209
+ ).first()
210
+ if minimum_cell_size_query is not None:
211
+ self.minimum_cell_size = minimum_cell_size_query[0]
202
212
  else:
203
213
  return []
204
214
  return super().get_invalid(session)
@@ -211,17 +221,17 @@ class RasterGridSizeCheck(BaseRasterCheck):
211
221
  try:
212
222
  return (
213
223
  isclose(
214
- a=((self.grid_space / raster.pixel_size[0]) % 2),
224
+ a=((self.minimum_cell_size / raster.pixel_size[0]) % 2),
215
225
  b=0,
216
226
  rel_tol=1e-09,
217
227
  )
218
- ) and (self.grid_space >= (2 * raster.pixel_size[0]))
228
+ ) and (self.minimum_cell_size >= (2 * raster.pixel_size[0]))
219
229
  # if one of the fields is a NoneType it will be caught elsewhere
220
230
  except TypeError:
221
231
  return True
222
232
 
223
233
  def description(self):
224
- return "v2_global_settings.grid_space is not a positive even multiple of the raster cell size."
234
+ return "model_settings.minimum_cell_size is not a positive even multiple of the raster cell size."
225
235
 
226
236
 
227
237
  class RasterPixelCountCheck(BaseRasterCheck):
@@ -300,6 +310,19 @@ class RasterRangeCheck(BaseRasterCheck):
300
310
  return f"{self.column_name} has values {' and/or '.join(parts)} or is empty"
301
311
 
302
312
 
313
+ class RasterCompressionUsedCheck(BaseRasterCheck):
314
+ """Checks whether compression was used for the raster"""
315
+
316
+ def is_valid(self, path: str, interface_cls: Type[RasterInterface]):
317
+ with interface_cls(path) as raster:
318
+ if not raster.is_valid_geotiff:
319
+ return True
320
+ return raster.compression != "NONE"
321
+
322
+ def description(self):
323
+ return f"Raster {self.column_name} is not compressed. It is recommended to use DEFLATE compression. This speeds up uploading and downloading and reduces storage space."
324
+
325
+
303
326
  @dataclass
304
327
  class GDALUnavailable:
305
328
  id: int