c2cgeoportal-commons 2.8.1.180__py3-none-any.whl → 2.9rc1__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.

Potentially problematic release.


This version of c2cgeoportal-commons might be problematic. Click here for more details.

Files changed (84) hide show
  1. c2cgeoportal_commons/alembic/env.py +14 -10
  2. c2cgeoportal_commons/alembic/main/028477929d13_add_technical_roles.py +2 -2
  3. c2cgeoportal_commons/alembic/main/04f05bfbb05e_remove_the_old_is_expanded_column.py +3 -1
  4. c2cgeoportal_commons/alembic/main/116b9b79fc4d_internal_and_external_layer_tables_.py +12 -12
  5. c2cgeoportal_commons/alembic/main/1418cb05921b_merge_1_6_and_master_branches.py +3 -1
  6. c2cgeoportal_commons/alembic/main/164ac0819a61_add_image_format_to_wmts_layer.py +2 -2
  7. c2cgeoportal_commons/alembic/main/166ff2dcc48d_create_database.py +2 -2
  8. c2cgeoportal_commons/alembic/main/16e43f8c0330_remove_old_metadata_column.py +2 -2
  9. c2cgeoportal_commons/alembic/main/1d5d4abfebd1_add_restricted_theme.py +2 -2
  10. c2cgeoportal_commons/alembic/main/1de20166b274_remove_v1_artifacts.py +2 -2
  11. c2cgeoportal_commons/alembic/main/20137477bd02_update_icons_url.py +2 -2
  12. c2cgeoportal_commons/alembic/main/21f11066f8ec_trigger_on_role_updates_user_in_static.py +2 -2
  13. c2cgeoportal_commons/alembic/main/22e6dfb556de_add_description_tree_.py +2 -2
  14. c2cgeoportal_commons/alembic/main/29f2a32859ec_merge_1_6_and_master_branches.py +3 -1
  15. c2cgeoportal_commons/alembic/main/2b8ed8c1df94_set_layergroup_treeitem_is_as_a_primary_.py +2 -2
  16. c2cgeoportal_commons/alembic/main/32527659d57b_move_exclude_properties_from_layerv1_to_.py +2 -2
  17. c2cgeoportal_commons/alembic/main/32b21aa1d0ed_merge_e004f76e951a_and_e004f76e951a_.py +3 -1
  18. c2cgeoportal_commons/alembic/main/338b57593823_remove_trigger_on_role_name_change.py +2 -2
  19. c2cgeoportal_commons/alembic/main/415746eb9f6_changes_for_v2.py +2 -2
  20. c2cgeoportal_commons/alembic/main/44c91d82d419_add_table_log.py +8 -6
  21. c2cgeoportal_commons/alembic/main/5109242131ce_add_column_time_widget.py +4 -3
  22. c2cgeoportal_commons/alembic/main/52916d8fde8b_add_sql_fields_to_vector_tiles.py +5 -3
  23. c2cgeoportal_commons/alembic/main/53ba1a68d5fe_add_theme_to_fulltextsearch.py +2 -2
  24. c2cgeoportal_commons/alembic/main/54645a535ad6_add_ordering_in_relation.py +2 -2
  25. c2cgeoportal_commons/alembic/main/56dc90838d90_fix_removing_layerv1.py +2 -2
  26. c2cgeoportal_commons/alembic/main/596ba21e3833_separate_local_internal.py +2 -2
  27. c2cgeoportal_commons/alembic/main/678f88c7ad5e_add_vector_tiles_layers_table.py +2 -2
  28. c2cgeoportal_commons/alembic/main/6a412d9437b1_rename_serverogc_to_ogcserver.py +2 -2
  29. c2cgeoportal_commons/alembic/main/6d87fdad275a_convert_metadata_to_the_right_case.py +2 -2
  30. c2cgeoportal_commons/alembic/main/7530011a66a7_trigger_on_role_updates_user_in_static.py +2 -2
  31. c2cgeoportal_commons/alembic/main/78fd093c8393_add_api_s_intrfaces.py +15 -10
  32. c2cgeoportal_commons/alembic/main/7d271f4527cd_add_layer_column_in_layerv1_table.py +2 -2
  33. c2cgeoportal_commons/alembic/main/809650bd04c3_add_dimension_field.py +2 -2
  34. c2cgeoportal_commons/alembic/main/8117bb9bba16_use_dimension_on_all_the_layers.py +2 -2
  35. c2cgeoportal_commons/alembic/main/87f8330ed64e_add_missing_delete_cascades.py +2 -2
  36. c2cgeoportal_commons/alembic/main/9268a1dffac0_add_trigger_to_be_able_to_correctly_.py +2 -2
  37. c2cgeoportal_commons/alembic/main/94db7e7e5b21_merge_2_2_and_master_branches.py +3 -1
  38. c2cgeoportal_commons/alembic/main/951ff84bd8ec_be_able_to_delete_a_wms_layer_in_sql.py +2 -2
  39. c2cgeoportal_commons/alembic/main/a00109812f89_add_field_layer_wms_valid.py +4 -4
  40. c2cgeoportal_commons/alembic/main/a4558f032d7d_add_support_of_cog_layers.py +74 -0
  41. c2cgeoportal_commons/alembic/main/a4f1aac9bda_merge_1_6_and_master_branches.py +3 -1
  42. c2cgeoportal_commons/alembic/main/b60f2a505f42_remame_uimetadata_to_metadata.py +2 -2
  43. c2cgeoportal_commons/alembic/main/b6b09f414fe8_sync_model_database.py +165 -0
  44. c2cgeoportal_commons/alembic/main/c75124553bf3_remove_deprecated_columns.py +2 -2
  45. c2cgeoportal_commons/alembic/main/d48a63b348f1_change_mapserver_url_for_docker.py +2 -2
  46. c2cgeoportal_commons/alembic/main/d8ef99bc227e_be_able_to_delete_a_linked_functionality.py +2 -2
  47. c2cgeoportal_commons/alembic/main/daf738d5bae4_merge_2_0_and_master_branches.py +3 -1
  48. c2cgeoportal_commons/alembic/main/dba87f2647f9_merge_2_2_on_2_3.py +3 -1
  49. c2cgeoportal_commons/alembic/main/e004f76e951a_add_missing_not_null.py +2 -2
  50. c2cgeoportal_commons/alembic/main/e7e03dedade3_put_the_default_wms_server_in_the_.py +2 -2
  51. c2cgeoportal_commons/alembic/main/e85afd327ab3_cascade_deletes_to_tsearch.py +2 -2
  52. c2cgeoportal_commons/alembic/main/ec82a8906649_add_missing_on_delete_cascade_on_layer_.py +2 -2
  53. c2cgeoportal_commons/alembic/main/ee25d267bf46_main_interface_desktop.py +2 -2
  54. c2cgeoportal_commons/alembic/main/eeb345672454_merge_2_4_and_master_branches.py +3 -1
  55. c2cgeoportal_commons/alembic/static/0c640a58a09a_add_opt_key_column.py +2 -2
  56. c2cgeoportal_commons/alembic/static/107b81f5b9fe_add_missing_delete_cascades.py +2 -2
  57. c2cgeoportal_commons/alembic/static/1857owc78a07_add_last_login_and_expiration_date.py +2 -2
  58. c2cgeoportal_commons/alembic/static/1da396a88908_move_user_table_to_static_schema.py +3 -3
  59. c2cgeoportal_commons/alembic/static/267b4c1bde2e_add_display_name_in_the_user_for_better_.py +62 -0
  60. c2cgeoportal_commons/alembic/static/3f89a7d71a5e_alter_column_url_to_remove_limitation.py +2 -2
  61. c2cgeoportal_commons/alembic/static/44c91d82d419_add_table_log.py +6 -4
  62. c2cgeoportal_commons/alembic/static/53d671b17b20_add_timezone_on_datetime_fields.py +2 -2
  63. c2cgeoportal_commons/alembic/static/5472fbc19f39_add_temp_password_column.py +2 -2
  64. c2cgeoportal_commons/alembic/static/76d72fb3fcb9_add_oauth2_pkce.py +3 -1
  65. c2cgeoportal_commons/alembic/static/7ef947f30f20_add_oauth2_tables.py +3 -1
  66. c2cgeoportal_commons/alembic/static/910b4ca53b68_sync_model_database.py +186 -0
  67. c2cgeoportal_commons/alembic/static/aa41e9613256_wip_add_openid_connect_support.py +64 -0
  68. c2cgeoportal_commons/alembic/static/ae5e88f35669_add_table_user_role.py +2 -2
  69. c2cgeoportal_commons/alembic/static/bd029dbfc11a_fill_tech_data_column.py +2 -2
  70. c2cgeoportal_commons/lib/email_.py +7 -7
  71. c2cgeoportal_commons/lib/literal.py +3 -3
  72. c2cgeoportal_commons/lib/url.py +15 -16
  73. c2cgeoportal_commons/models/__init__.py +15 -8
  74. c2cgeoportal_commons/models/main.py +296 -211
  75. c2cgeoportal_commons/models/sqlalchemy.py +10 -9
  76. c2cgeoportal_commons/models/static.py +125 -76
  77. c2cgeoportal_commons/testing/__init__.py +10 -6
  78. c2cgeoportal_commons/testing/initializedb.py +7 -6
  79. {c2cgeoportal_commons-2.8.1.180.dist-info → c2cgeoportal_commons-2.9rc1.dist-info}/METADATA +3 -9
  80. c2cgeoportal_commons-2.9rc1.dist-info/RECORD +88 -0
  81. {c2cgeoportal_commons-2.8.1.180.dist-info → c2cgeoportal_commons-2.9rc1.dist-info}/WHEEL +1 -1
  82. tests/conftest.py +1 -1
  83. c2cgeoportal_commons-2.8.1.180.dist-info/RECORD +0 -83
  84. {c2cgeoportal_commons-2.8.1.180.dist-info → c2cgeoportal_commons-2.9rc1.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011-2023, Camptocamp SA
1
+ # Copyright (c) 2011-2024, Camptocamp SA
2
2
  # All rights reserved.
3
3
 
4
4
  # Redistribution and use in source and binary forms, with or without
@@ -30,7 +30,8 @@ import enum
30
30
  import logging
31
31
  import os
32
32
  import re
33
- from typing import Any, Dict, List, Optional, Set, Tuple, Union, cast
33
+ from datetime import datetime
34
+ from typing import Any, Literal, Optional, cast, get_args
34
35
 
35
36
  import pyramid.request
36
37
  import sqlalchemy.orm.base
@@ -40,24 +41,28 @@ from geoalchemy2.shape import to_shape
40
41
  from papyrus.geo_interface import GeoInterface
41
42
  from sqlalchemy import Column, ForeignKey, Table, UniqueConstraint, event
42
43
  from sqlalchemy.ext.declarative import AbstractConcreteBase
43
- from sqlalchemy.orm import Session, backref, relationship
44
+ from sqlalchemy.orm import Mapped, Session, backref, mapped_column, relationship
44
45
  from sqlalchemy.schema import Index
45
46
  from sqlalchemy.types import Boolean, DateTime, Enum, Integer, String, Unicode
46
47
 
47
- from c2cgeoportal_commons.lib.literal import Literal
48
+ import c2cgeoportal_commons.lib.literal
48
49
  from c2cgeoportal_commons.lib.url import get_url2
49
50
  from c2cgeoportal_commons.models import Base, _, cache_invalidate_cb
50
51
  from c2cgeoportal_commons.models.sqlalchemy import JSONEncodedDict, TsVector
51
52
 
52
53
  try:
54
+ import colander
53
55
  from c2cgeoform import default_map_settings
54
56
  from c2cgeoform.ext.colander_ext import Geometry as ColanderGeometry
55
57
  from c2cgeoform.ext.deform_ext import MapWidget, RelationSelect2Widget
56
58
  from colander import drop
57
59
  from deform.widget import CheckboxWidget, HiddenWidget, SelectWidget, TextAreaWidget, TextInputWidget
60
+
61
+ colander_null = colander.null
58
62
  except ModuleNotFoundError:
59
- drop = None
63
+ drop = None # pylint: disable=invalid-name
60
64
  default_map_settings = {"srid": 3857, "view": {"projection": "EPSG:3857"}}
65
+ colander_null = None # pylint: disable=invalid-name
61
66
 
62
67
  class GenericClass:
63
68
  """Fallback class implementation."""
@@ -67,11 +72,11 @@ except ModuleNotFoundError:
67
72
 
68
73
  CheckboxWidget = GenericClass
69
74
  HiddenWidget = GenericClass
70
- MapWidget = GenericClass
75
+ MapWidget = GenericClass # type: ignore[misc,assignment]
71
76
  SelectWidget = GenericClass
72
77
  TextAreaWidget = GenericClass
73
- ColanderGeometry = GenericClass
74
- RelationSelect2Widget = GenericClass
78
+ ColanderGeometry = GenericClass # type: ignore[misc,assignment]
79
+ RelationSelect2Widget = GenericClass # type: ignore[misc,assignment]
75
80
  TextInputWidget = GenericClass
76
81
 
77
82
 
@@ -86,14 +91,16 @@ if os.environ.get("DEVELOPMENT", "0") == "1":
86
91
  # information
87
92
  sqlalchemy.orm.base.state_str = state_str
88
93
 
89
- LOG = logging.getLogger(__name__)
94
+ _LOG = logging.getLogger(__name__)
90
95
 
91
96
  _schema: str = config["schema"] or "main"
92
97
  _srid: int = cast(int, config["srid"]) or 3857
93
98
 
94
99
  # Set some default values for the admin interface
95
- _admin_config: Dict[str, Any] = config.get_config().get("admin_interface", {})
96
- _map_config: Dict[str, Any] = {**default_map_settings, **_admin_config.get("map", {})}
100
+ conf = config.get_config()
101
+ assert conf is not None
102
+ _admin_config: dict[str, Any] = conf.get("admin_interface", {})
103
+ _map_config: dict[str, Any] = {**default_map_settings, **_admin_config.get("map", {})}
97
104
  view_srid_match = re.match(r"EPSG:(\d+)", _map_config["view"]["projection"])
98
105
  if "map_srid" not in _admin_config and view_srid_match is not None:
99
106
  _admin_config["map_srid"] = view_srid_match.group(1)
@@ -103,22 +110,30 @@ class FullTextSearch(GeoInterface, Base): # type: ignore
103
110
  """The tsearch table representation."""
104
111
 
105
112
  __tablename__ = "tsearch"
106
- __table_args__ = (Index("tsearch_ts_idx", "ts", postgresql_using="gin"), {"schema": _schema})
113
+ __table_args__ = (
114
+ Index("tsearch_search_index", "ts", "public", "role_id", "interface_id", "lang"),
115
+ Index("tsearch_ts_idx", "ts", postgresql_using="gin"),
116
+ {"schema": _schema},
117
+ )
107
118
 
108
- id = Column(Integer, primary_key=True)
109
- label = Column(Unicode)
110
- layer_name = Column(Unicode)
111
- role_id = Column(Integer, ForeignKey(_schema + ".role.id", ondelete="CASCADE"), nullable=True)
119
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
120
+ label: Mapped[str] = mapped_column(Unicode)
121
+ layer_name: Mapped[str] = mapped_column(Unicode, nullable=True)
122
+ role_id: Mapped[int] = mapped_column(
123
+ Integer, ForeignKey(_schema + ".role.id", ondelete="CASCADE"), nullable=True
124
+ )
112
125
  role = relationship("Role")
113
- interface_id = Column(Integer, ForeignKey(_schema + ".interface.id", ondelete="CASCADE"), nullable=True)
126
+ interface_id: Mapped[int] = mapped_column(
127
+ Integer, ForeignKey(_schema + ".interface.id", ondelete="CASCADE"), nullable=True
128
+ )
114
129
  interface = relationship("Interface")
115
- lang = Column(String(2), nullable=True)
116
- public = Column(Boolean, server_default="true")
117
- ts = Column(TsVector)
118
- the_geom = Column(Geometry("GEOMETRY", srid=_srid))
119
- params = Column(JSONEncodedDict, nullable=True)
120
- actions = Column(JSONEncodedDict, nullable=True)
121
- from_theme = Column(Boolean, server_default="false")
130
+ lang: Mapped[str] = mapped_column(String(2), nullable=True)
131
+ public: Mapped[bool] = mapped_column(Boolean, server_default="true")
132
+ ts = mapped_column(TsVector)
133
+ the_geom = mapped_column(Geometry("GEOMETRY", srid=_srid, spatial_index=False))
134
+ params = mapped_column(JSONEncodedDict, nullable=True)
135
+ actions = mapped_column(JSONEncodedDict, nullable=True)
136
+ from_theme: Mapped[bool] = mapped_column(Boolean, server_default="false")
122
137
 
123
138
  def __str__(self) -> str:
124
139
  return f"{self.label}[{self.id}]"
@@ -133,8 +148,10 @@ class Functionality(Base): # type: ignore
133
148
 
134
149
  __c2cgeoform_config__ = {"duplicate": True}
135
150
 
136
- id = Column(Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}})
137
- name = Column(
151
+ id: Mapped[int] = mapped_column(
152
+ Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}}
153
+ )
154
+ name: Mapped[str] = mapped_column(
138
155
  Unicode,
139
156
  nullable=False,
140
157
  info={
@@ -154,7 +171,7 @@ class Functionality(Base): # type: ignore
154
171
  }
155
172
  },
156
173
  )
157
- description = Column(
174
+ description: Mapped[str | None] = mapped_column(
158
175
  Unicode,
159
176
  info={
160
177
  "colanderalchemy": {
@@ -163,7 +180,7 @@ class Functionality(Base): # type: ignore
163
180
  }
164
181
  },
165
182
  )
166
- value = Column(
183
+ value: Mapped[str] = mapped_column(
167
184
  Unicode,
168
185
  nullable=False,
169
186
  info={
@@ -224,8 +241,10 @@ class Role(Base): # type: ignore
224
241
  __colanderalchemy_config__ = {"title": _("Role"), "plural": _("Roles")}
225
242
  __c2cgeoform_config__ = {"duplicate": True}
226
243
 
227
- id = Column(Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}})
228
- name = Column(
244
+ id: Mapped[int] = mapped_column(
245
+ Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}}
246
+ )
247
+ name: Mapped[str] = mapped_column(
229
248
  Unicode,
230
249
  unique=True,
231
250
  nullable=False,
@@ -236,7 +255,7 @@ class Role(Base): # type: ignore
236
255
  }
237
256
  },
238
257
  )
239
- description = Column(
258
+ description: Mapped[str | None] = mapped_column(
240
259
  Unicode,
241
260
  info={
242
261
  "colanderalchemy": {
@@ -245,8 +264,8 @@ class Role(Base): # type: ignore
245
264
  }
246
265
  },
247
266
  )
248
- extent = Column(
249
- Geometry("POLYGON", srid=_srid),
267
+ extent = mapped_column(
268
+ Geometry("POLYGON", srid=_srid, spatial_index=False),
250
269
  info={
251
270
  "colanderalchemy": {
252
271
  "title": _("Extent"),
@@ -275,8 +294,8 @@ class Role(Base): # type: ignore
275
294
  self,
276
295
  name: str = "",
277
296
  description: str = "",
278
- functionalities: Optional[List[Functionality]] = None,
279
- extent: Geometry = None,
297
+ functionalities: list[Functionality] | None = None,
298
+ extent: Geometry | None = None,
280
299
  ) -> None:
281
300
  if functionalities is None:
282
301
  functionalities = []
@@ -289,10 +308,10 @@ class Role(Base): # type: ignore
289
308
  return f"{self.name}[{self.id}]>"
290
309
 
291
310
  @property
292
- def bounds(self) -> Optional[Tuple[float, float, float, float]]: # TODO
311
+ def bounds(self) -> tuple[float, float, float, float] | None: # TODO
293
312
  if self.extent is None:
294
313
  return None
295
- return cast(Tuple[float, float, float, float], to_shape(self.extent).bounds)
314
+ return cast(tuple[float, float, float, float], to_shape(self.extent).bounds)
296
315
 
297
316
 
298
317
  event.listen(Role.functionalities, "set", cache_invalidate_cb)
@@ -304,15 +323,17 @@ class TreeItem(Base): # type: ignore
304
323
  """The treeitem table representation."""
305
324
 
306
325
  __tablename__ = "treeitem"
307
- __table_args__: Union[Tuple[Any, ...], Dict[str, Any]] = (
326
+ __table_args__: tuple[Any, ...] | dict[str, Any] = (
308
327
  UniqueConstraint("type", "name"),
309
328
  {"schema": _schema},
310
329
  )
311
- item_type = Column("type", String(10), nullable=False, info={"colanderalchemy": {"exclude": True}})
330
+ item_type: Mapped[str] = mapped_column(
331
+ "type", String(10), nullable=False, info={"colanderalchemy": {"exclude": True}}
332
+ )
312
333
  __mapper_args__ = {"polymorphic_on": item_type}
313
334
 
314
- id = Column(Integer, primary_key=True)
315
- name = Column(
335
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
336
+ name: Mapped[str] = mapped_column(
316
337
  Unicode,
317
338
  nullable=False,
318
339
  info={
@@ -327,7 +348,7 @@ class TreeItem(Base): # type: ignore
327
348
  }
328
349
  },
329
350
  )
330
- description = Column(
351
+ description: Mapped[str | None] = mapped_column(
331
352
  Unicode,
332
353
  info={
333
354
  "colanderalchemy": {
@@ -339,7 +360,7 @@ class TreeItem(Base): # type: ignore
339
360
 
340
361
  @property
341
362
  # Better: def parents(self) -> List[TreeGroup]:
342
- def parents(self) -> List["TreeItem"]:
363
+ def parents(self) -> list["TreeItem"]:
343
364
  return [c.treegroup for c in self.parents_relation]
344
365
 
345
366
  def is_in_interface(self, name: str) -> bool:
@@ -352,7 +373,7 @@ class TreeItem(Base): # type: ignore
352
373
 
353
374
  return False
354
375
 
355
- def get_metadata(self, name: str) -> List["Metadata"]:
376
+ def get_metadata(self, name: str) -> list["Metadata"]:
356
377
  return [metadata for metadata in self.metadatas if metadata.name == name]
357
378
 
358
379
  def __init__(self, name: str = "") -> None:
@@ -375,11 +396,13 @@ class LayergroupTreeitem(Base): # type: ignore
375
396
  __table_args__ = {"schema": _schema}
376
397
 
377
398
  # required by formalchemy
378
- id = Column(Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}})
379
- description = Column(Unicode, info={"colanderalchemy": {"exclude": True}})
380
- treegroup_id = Column(
399
+ id: Mapped[int] = mapped_column(
400
+ Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}}
401
+ )
402
+ description: Mapped[str | None] = mapped_column(Unicode, info={"colanderalchemy": {"exclude": True}})
403
+ treegroup_id: Mapped[int] = mapped_column(
381
404
  Integer,
382
- ForeignKey(_schema + ".treegroup.id"),
405
+ ForeignKey(_schema + ".treegroup.id", name="treegroup_id_fkey"),
383
406
  nullable=False,
384
407
  info={"colanderalchemy": {"exclude": True}},
385
408
  )
@@ -394,9 +417,9 @@ class LayergroupTreeitem(Base): # type: ignore
394
417
  primaryjoin="LayergroupTreeitem.treegroup_id==TreeGroup.id",
395
418
  info={"colanderalchemy": {"exclude": True}, "c2cgeoform": {"duplicate": False}},
396
419
  )
397
- treeitem_id = Column(
420
+ treeitem_id: Mapped[int] = mapped_column(
398
421
  Integer,
399
- ForeignKey(_schema + ".treeitem.id"),
422
+ ForeignKey(_schema + ".treeitem.id", ondelete="CASCADE"),
400
423
  nullable=False,
401
424
  info={"colanderalchemy": {"widget": HiddenWidget()}},
402
425
  )
@@ -413,10 +436,10 @@ class LayergroupTreeitem(Base): # type: ignore
413
436
  primaryjoin="LayergroupTreeitem.treeitem_id==TreeItem.id",
414
437
  info={"colanderalchemy": {"exclude": True}, "c2cgeoform": {"duplicate": False}},
415
438
  )
416
- ordering = Column(Integer, info={"colanderalchemy": {"widget": HiddenWidget()}})
439
+ ordering: Mapped[int] = mapped_column(Integer, info={"colanderalchemy": {"widget": HiddenWidget()}})
417
440
 
418
441
  def __init__(
419
- self, group: Optional["TreeGroup"] = None, item: Optional[TreeItem] = None, ordering: int = 0
442
+ self, group: Optional["TreeGroup"] = None, item: TreeItem | None = None, ordering: int = 0
420
443
  ) -> None:
421
444
  self.treegroup = group
422
445
  self.treeitem = item
@@ -436,14 +459,18 @@ class TreeGroup(TreeItem):
436
459
 
437
460
  __tablename__ = "treegroup"
438
461
  __table_args__ = {"schema": _schema}
439
- __mapper_args__ = {"polymorphic_identity": "treegroup"} # needed for _identity_class
462
+ __mapper_args__ = {"polymorphic_identity": "treegroup"} # type: ignore[dict-item] # needed for _identity_class
440
463
 
441
- id = Column(Integer, ForeignKey(_schema + ".treeitem.id"), primary_key=True)
464
+ id: Mapped[int] = mapped_column(
465
+ Integer,
466
+ ForeignKey(_schema + ".treeitem.id", ondelete="CASCADE", name="treegroup_id_fkey"),
467
+ primary_key=True,
468
+ )
442
469
 
443
- def _get_children(self) -> List[TreeItem]:
470
+ def _get_children(self) -> list[TreeItem]:
444
471
  return [c.treeitem for c in self.children_relation]
445
472
 
446
- def _set_children(self, children: List[TreeItem], order: bool = False) -> None:
473
+ def _set_children(self, children: list[TreeItem], order: bool = False) -> None:
447
474
  """
448
475
  Set the current TreeGroup children TreeItem instances.
449
476
 
@@ -489,7 +516,7 @@ class LayerGroup(TreeGroup):
489
516
  __colanderalchemy_config__ = {
490
517
  "title": _("Layers group"),
491
518
  "plural": _("Layers groups"),
492
- "description": Literal(
519
+ "description": c2cgeoportal_commons.lib.literal.Literal(
493
520
  _(
494
521
  """
495
522
  <div class="help-block">
@@ -502,12 +529,12 @@ class LayerGroup(TreeGroup):
502
529
  )
503
530
  ),
504
531
  }
505
- __mapper_args__ = {"polymorphic_identity": "group"}
532
+ __mapper_args__ = {"polymorphic_identity": "group"} # type: ignore[dict-item]
506
533
  __c2cgeoform_config__ = {"duplicate": True}
507
534
 
508
- id = Column(
535
+ id: Mapped[int] = mapped_column(
509
536
  Integer,
510
- ForeignKey(_schema + ".treegroup.id"),
537
+ ForeignKey(_schema + ".treegroup.id", ondelete="CASCADE"),
511
538
  primary_key=True,
512
539
  info={"colanderalchemy": {"missing": drop, "widget": HiddenWidget()}},
513
540
  )
@@ -532,19 +559,19 @@ class Theme(TreeGroup):
532
559
  __tablename__ = "theme"
533
560
  __table_args__ = {"schema": _schema}
534
561
  __colanderalchemy_config__ = {"title": _("Theme"), "plural": _("Themes")}
535
- __mapper_args__ = {"polymorphic_identity": "theme"}
562
+ __mapper_args__ = {"polymorphic_identity": "theme"} # type: ignore[dict-item]
536
563
  __c2cgeoform_config__ = {"duplicate": True}
537
564
 
538
- id = Column(
565
+ id: Mapped[int] = mapped_column(
539
566
  Integer,
540
- ForeignKey(_schema + ".treegroup.id"),
567
+ ForeignKey(_schema + ".treegroup.id", ondelete="CASCADE"),
541
568
  primary_key=True,
542
569
  info={"colanderalchemy": {"missing": drop, "widget": HiddenWidget()}},
543
570
  )
544
- ordering = Column(
571
+ ordering: Mapped[int] = mapped_column(
545
572
  Integer, nullable=False, info={"colanderalchemy": {"title": _("Order"), "widget": HiddenWidget()}}
546
573
  )
547
- public = Column(
574
+ public: Mapped[bool] = mapped_column(
548
575
  Boolean,
549
576
  default=True,
550
577
  nullable=False,
@@ -555,12 +582,14 @@ class Theme(TreeGroup):
555
582
  }
556
583
  },
557
584
  )
558
- icon = Column(
585
+ icon: Mapped[str] = mapped_column(
559
586
  Unicode,
587
+ nullable=True,
560
588
  info={
561
589
  "colanderalchemy": {
562
590
  "title": _("Icon"),
563
591
  "description": _("The icon URL."),
592
+ "missing": "",
564
593
  }
565
594
  },
566
595
  )
@@ -609,15 +638,15 @@ class Layer(TreeItem):
609
638
 
610
639
  __tablename__ = "layer"
611
640
  __table_args__ = {"schema": _schema}
612
- __mapper_args__ = {"polymorphic_identity": "layer"} # needed for _identity_class
641
+ __mapper_args__ = {"polymorphic_identity": "layer"} # type: ignore[dict-item] # needed for _identity_class
613
642
 
614
- id = Column(
643
+ id: Mapped[int] = mapped_column(
615
644
  Integer,
616
- ForeignKey(_schema + ".treeitem.id"),
645
+ ForeignKey(_schema + ".treeitem.id", ondelete="CASCADE"),
617
646
  primary_key=True,
618
647
  info={"colanderalchemy": {"widget": HiddenWidget()}},
619
648
  )
620
- public = Column(
649
+ public: Mapped[bool] = mapped_column(
621
650
  Boolean,
622
651
  default=True,
623
652
  info={
@@ -627,7 +656,7 @@ class Layer(TreeItem):
627
656
  }
628
657
  },
629
658
  )
630
- geo_table = Column(
659
+ geo_table: Mapped[str | None] = mapped_column(
631
660
  Unicode,
632
661
  info={
633
662
  "colanderalchemy": {
@@ -636,8 +665,9 @@ class Layer(TreeItem):
636
665
  }
637
666
  },
638
667
  )
639
- exclude_properties = Column(
668
+ exclude_properties: Mapped[str] = mapped_column(
640
669
  Unicode,
670
+ nullable=True,
641
671
  info={
642
672
  "colanderalchemy": {
643
673
  "title": _("Exclude properties"),
@@ -648,6 +678,7 @@ class Layer(TreeItem):
648
678
  For enumerable attributes (foreign key), the column name should end with '_id'.
649
679
  """
650
680
  ),
681
+ "missing": "",
651
682
  }
652
683
  },
653
684
  )
@@ -660,19 +691,27 @@ class Layer(TreeItem):
660
691
  class DimensionLayer(Layer):
661
692
  """The intermediate class for the leyser with dimension."""
662
693
 
663
- __mapper_args__ = {"polymorphic_identity": "dimensionlayer"} # needed for _identity_class
694
+ __mapper_args__ = {"polymorphic_identity": "dimensionlayer"} # type: ignore[dict-item] # needed for _identity_class
664
695
 
665
696
 
666
- OGCSERVER_TYPE_MAPSERVER = "mapserver"
667
- OGCSERVER_TYPE_QGISSERVER = "qgisserver"
668
- OGCSERVER_TYPE_GEOSERVER = "geoserver"
669
- OGCSERVER_TYPE_ARCGIS = "arcgis"
670
- OGCSERVER_TYPE_OTHER = "other"
697
+ OGCServerType = Literal["mapserver", "qgisserver", "geoserver", "arcgis", "other"]
698
+ OGCSERVER_TYPE_MAPSERVER: OGCServerType = "mapserver"
699
+ OGCSERVER_TYPE_QGISSERVER: OGCServerType = "qgisserver"
700
+ OGCSERVER_TYPE_GEOSERVER: OGCServerType = "geoserver"
701
+ OGCSERVER_TYPE_ARCGIS: OGCServerType = "arcgis"
702
+ OGCSERVER_TYPE_OTHER: OGCServerType = "other"
671
703
 
672
- OGCSERVER_AUTH_NOAUTH = "No auth"
673
- OGCSERVER_AUTH_STANDARD = "Standard auth"
674
- OGCSERVER_AUTH_GEOSERVER = "Geoserver auth"
675
- OGCSERVER_AUTH_PROXY = "Proxy"
704
+
705
+ OGCServerAuth = Literal["No auth", "Standard auth", "Geoserver auth", "Proxy"]
706
+ OGCSERVER_AUTH_NOAUTH: OGCServerAuth = "No auth"
707
+ OGCSERVER_AUTH_STANDARD: OGCServerAuth = "Standard auth"
708
+ OGCSERVER_AUTH_GEOSERVER: OGCServerAuth = "Geoserver auth"
709
+ OGCSERVER_AUTH_PROXY: OGCServerAuth = "Proxy"
710
+
711
+
712
+ ImageType = Literal["image/jpeg", "image/png"]
713
+ TimeMode = Literal["disabled", "value", "range"]
714
+ TimeWidget = Literal["slider", "datepicker"]
676
715
 
677
716
 
678
717
  class OGCServer(Base): # type: ignore
@@ -683,7 +722,7 @@ class OGCServer(Base): # type: ignore
683
722
  __colanderalchemy_config__ = {
684
723
  "title": _("OGC Server"),
685
724
  "plural": _("OGC Servers"),
686
- "description": Literal(
725
+ "description": c2cgeoportal_commons.lib.literal.Literal(
687
726
  _(
688
727
  """
689
728
  <div class="help-block">
@@ -698,8 +737,10 @@ class OGCServer(Base): # type: ignore
698
737
  ),
699
738
  }
700
739
  __c2cgeoform_config__ = {"duplicate": True}
701
- id = Column(Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}})
702
- name = Column(
740
+ id: Mapped[int] = mapped_column(
741
+ Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}}
742
+ )
743
+ name: Mapped[str] = mapped_column(
703
744
  Unicode,
704
745
  nullable=False,
705
746
  unique=True,
@@ -710,7 +751,7 @@ class OGCServer(Base): # type: ignore
710
751
  }
711
752
  },
712
753
  )
713
- description = Column(
754
+ description: Mapped[str | None] = mapped_column(
714
755
  Unicode,
715
756
  info={
716
757
  "colanderalchemy": {
@@ -719,7 +760,7 @@ class OGCServer(Base): # type: ignore
719
760
  }
720
761
  },
721
762
  )
722
- url = Column(
763
+ url: Mapped[str] = mapped_column(
723
764
  Unicode,
724
765
  nullable=False,
725
766
  info={
@@ -729,7 +770,7 @@ class OGCServer(Base): # type: ignore
729
770
  }
730
771
  },
731
772
  )
732
- url_wfs = Column(
773
+ url_wfs: Mapped[str | None] = mapped_column(
733
774
  Unicode,
734
775
  info={
735
776
  "colanderalchemy": {
@@ -738,15 +779,8 @@ class OGCServer(Base): # type: ignore
738
779
  }
739
780
  },
740
781
  )
741
- type = Column(
742
- Enum(
743
- OGCSERVER_TYPE_MAPSERVER,
744
- OGCSERVER_TYPE_QGISSERVER,
745
- OGCSERVER_TYPE_GEOSERVER,
746
- OGCSERVER_TYPE_ARCGIS,
747
- OGCSERVER_TYPE_OTHER,
748
- native_enum=False,
749
- ),
782
+ type: Mapped[OGCServerType] = mapped_column(
783
+ Enum(*get_args(OGCServerType), native_enum=False),
750
784
  nullable=False,
751
785
  info={
752
786
  "colanderalchemy": {
@@ -754,56 +788,35 @@ class OGCServer(Base): # type: ignore
754
788
  "description": _(
755
789
  "The server type which is used to know which custom attribute will be used."
756
790
  ),
757
- "widget": SelectWidget(
758
- values=(
759
- (OGCSERVER_TYPE_MAPSERVER, OGCSERVER_TYPE_MAPSERVER),
760
- (OGCSERVER_TYPE_QGISSERVER, OGCSERVER_TYPE_QGISSERVER),
761
- (OGCSERVER_TYPE_GEOSERVER, OGCSERVER_TYPE_GEOSERVER),
762
- (OGCSERVER_TYPE_ARCGIS, OGCSERVER_TYPE_ARCGIS),
763
- (OGCSERVER_TYPE_OTHER, OGCSERVER_TYPE_OTHER),
764
- )
765
- ),
791
+ "widget": SelectWidget(values=list((e, e) for e in get_args(OGCServerType))),
766
792
  }
767
793
  },
768
794
  )
769
- image_type = Column(
770
- Enum("image/jpeg", "image/png", native_enum=False),
795
+ image_type: Mapped[ImageType] = mapped_column(
796
+ Enum(*get_args(ImageType), native_enum=False),
771
797
  nullable=False,
772
798
  info={
773
799
  "colanderalchemy": {
774
800
  "title": _("Image type"),
775
801
  "description": _("The MIME type of the images (e.g.: ``image/png``)."),
776
- "widget": SelectWidget(values=(("image/jpeg", "image/jpeg"), ("image/png", "image/png"))),
802
+ "widget": SelectWidget(values=list((e, e) for e in get_args(ImageType))),
777
803
  "column": 2,
778
804
  }
779
805
  },
780
806
  )
781
- auth = Column(
782
- Enum(
783
- OGCSERVER_AUTH_NOAUTH,
784
- OGCSERVER_AUTH_STANDARD,
785
- OGCSERVER_AUTH_GEOSERVER,
786
- OGCSERVER_AUTH_PROXY,
787
- native_enum=False,
788
- ),
807
+ auth: Mapped[OGCServerAuth] = mapped_column(
808
+ Enum(*get_args(OGCServerAuth), native_enum=False),
789
809
  nullable=False,
790
810
  info={
791
811
  "colanderalchemy": {
792
812
  "title": _("Authentication type"),
793
813
  "description": "The kind of authentication to use.",
794
- "widget": SelectWidget(
795
- values=(
796
- (OGCSERVER_AUTH_NOAUTH, OGCSERVER_AUTH_NOAUTH),
797
- (OGCSERVER_AUTH_STANDARD, OGCSERVER_AUTH_STANDARD),
798
- (OGCSERVER_AUTH_GEOSERVER, OGCSERVER_AUTH_GEOSERVER),
799
- (OGCSERVER_AUTH_PROXY, OGCSERVER_AUTH_PROXY),
800
- )
801
- ),
814
+ "widget": SelectWidget(values=list((e, e) for e in get_args(OGCServerAuth))),
802
815
  "column": 2,
803
816
  }
804
817
  },
805
818
  )
806
- wfs_support = Column(
819
+ wfs_support: Mapped[bool] = mapped_column(
807
820
  Boolean,
808
821
  info={
809
822
  "colanderalchemy": {
@@ -813,7 +826,7 @@ class OGCServer(Base): # type: ignore
813
826
  }
814
827
  },
815
828
  )
816
- is_single_tile = Column(
829
+ is_single_tile: Mapped[bool] = mapped_column(
817
830
  Boolean,
818
831
  info={
819
832
  "colanderalchemy": {
@@ -827,12 +840,12 @@ class OGCServer(Base): # type: ignore
827
840
  def __init__(
828
841
  self,
829
842
  name: str = "",
830
- description: Optional[str] = None,
843
+ description: str | None = None,
831
844
  url: str = "https://wms.example.com",
832
- url_wfs: Optional[str] = None,
833
- type_: str = "mapserver",
834
- image_type: str = "image/png",
835
- auth: str = "Standard auth",
845
+ url_wfs: str | None = None,
846
+ type_: OGCServerType = OGCSERVER_TYPE_MAPSERVER,
847
+ image_type: ImageType = "image/png",
848
+ auth: OGCServerAuth = OGCSERVER_AUTH_STANDARD,
836
849
  wfs_support: bool = True,
837
850
  is_single_tile: bool = False,
838
851
  ) -> None:
@@ -850,14 +863,14 @@ class OGCServer(Base): # type: ignore
850
863
  return self.name or ""
851
864
 
852
865
  def url_description(self, request: pyramid.request.Request) -> str:
853
- errors: Set[str] = set()
866
+ errors: set[str] = set()
854
867
  url = get_url2(self.name, self.url, request, errors)
855
868
  return url.url() if url else "\n".join(errors)
856
869
 
857
- def url_wfs_description(self, request: pyramid.request.Request) -> Optional[str]:
870
+ def url_wfs_description(self, request: pyramid.request.Request) -> str | None:
858
871
  if not self.url_wfs:
859
872
  return self.url_description(request)
860
- errors: Set[str] = set()
873
+ errors: set[str] = set()
861
874
  url = get_url2(self.name, self.url_wfs, request, errors)
862
875
  return url.url() if url else "\n".join(errors)
863
876
 
@@ -870,7 +883,7 @@ class LayerWMS(DimensionLayer):
870
883
  __colanderalchemy_config__ = {
871
884
  "title": _("WMS Layer"),
872
885
  "plural": _("WMS Layers"),
873
- "description": Literal(
886
+ "description": c2cgeoportal_commons.lib.literal.Literal(
874
887
  _(
875
888
  """
876
889
  <div class="help-block">
@@ -886,15 +899,15 @@ class LayerWMS(DimensionLayer):
886
899
 
887
900
  __c2cgeoform_config__ = {"duplicate": True}
888
901
 
889
- __mapper_args__ = {"polymorphic_identity": "l_wms"}
902
+ __mapper_args__ = {"polymorphic_identity": "l_wms"} # type: ignore[dict-item]
890
903
 
891
- id = Column(
904
+ id: Mapped[int] = mapped_column(
892
905
  Integer,
893
906
  ForeignKey(_schema + ".layer.id", ondelete="CASCADE"),
894
907
  primary_key=True,
895
908
  info={"colanderalchemy": {"missing": None, "widget": HiddenWidget()}},
896
909
  )
897
- ogc_server_id = Column(
910
+ ogc_server_id: Mapped[int] = mapped_column(
898
911
  Integer,
899
912
  ForeignKey(_schema + ".ogc_server.id"),
900
913
  nullable=False,
@@ -908,7 +921,7 @@ class LayerWMS(DimensionLayer):
908
921
  }
909
922
  },
910
923
  )
911
- layer = Column(
924
+ layer: Mapped[str] = mapped_column(
912
925
  Unicode,
913
926
  nullable=False,
914
927
  info={
@@ -926,7 +939,7 @@ class LayerWMS(DimensionLayer):
926
939
  }
927
940
  },
928
941
  )
929
- style = Column(
942
+ style: Mapped[str | None] = mapped_column(
930
943
  Unicode,
931
944
  info={
932
945
  "colanderalchemy": {
@@ -936,30 +949,34 @@ class LayerWMS(DimensionLayer):
936
949
  }
937
950
  },
938
951
  )
939
- valid = Column(
952
+ valid: Mapped[bool] = mapped_column(
940
953
  Boolean,
954
+ nullable=True,
941
955
  info={
942
956
  "colanderalchemy": {
943
957
  "title": _("Valid"),
944
958
  "description": _("The status reported by latest synchronization (readonly)."),
945
959
  "column": 2,
946
960
  "widget": CheckboxWidget(readonly=True),
961
+ "missing": colander_null,
947
962
  }
948
963
  },
949
964
  )
950
- invalid_reason = Column(
965
+ invalid_reason: Mapped[str] = mapped_column(
951
966
  Unicode,
967
+ nullable=True,
952
968
  info={
953
969
  "colanderalchemy": {
954
970
  "title": _("Reason why I am not valid"),
955
971
  "description": _("The reason for status reported by latest synchronization (readonly)."),
956
972
  "column": 2,
957
973
  "widget": TextInputWidget(readonly=True),
974
+ "missing": "",
958
975
  }
959
976
  },
960
977
  )
961
- time_mode = Column(
962
- Enum("disabled", "value", "range", native_enum=False),
978
+ time_mode: Mapped[TimeMode] = mapped_column(
979
+ Enum(*get_args(TimeMode), native_enum=False),
963
980
  default="disabled",
964
981
  nullable=False,
965
982
  info={
@@ -973,8 +990,8 @@ class LayerWMS(DimensionLayer):
973
990
  }
974
991
  },
975
992
  )
976
- time_widget = Column(
977
- Enum("slider", "datepicker", native_enum=False),
993
+ time_widget: Mapped[TimeWidget] = mapped_column(
994
+ Enum(*get_args(TimeWidget), native_enum=False),
978
995
  default="slider",
979
996
  nullable=False,
980
997
  info={
@@ -1005,8 +1022,8 @@ class LayerWMS(DimensionLayer):
1005
1022
  name: str = "",
1006
1023
  layer: str = "",
1007
1024
  public: bool = True,
1008
- time_mode: str = "disabled",
1009
- time_widget: str = "slider",
1025
+ time_mode: TimeMode = "disabled",
1026
+ time_widget: TimeWidget = "slider",
1010
1027
  ) -> None:
1011
1028
  super().__init__(name=name, public=public)
1012
1029
  self.layer = layer
@@ -1014,7 +1031,7 @@ class LayerWMS(DimensionLayer):
1014
1031
  self.time_widget = time_widget
1015
1032
 
1016
1033
  @staticmethod
1017
- def get_default(dbsession: Session) -> Optional[DimensionLayer]:
1034
+ def get_default(dbsession: Session) -> DimensionLayer | None:
1018
1035
  return cast(
1019
1036
  Optional[DimensionLayer],
1020
1037
  dbsession.query(LayerWMS).filter(LayerWMS.name == "wms-defaults").one_or_none(),
@@ -1029,7 +1046,7 @@ class LayerWMTS(DimensionLayer):
1029
1046
  __colanderalchemy_config__ = {
1030
1047
  "title": _("WMTS Layer"),
1031
1048
  "plural": _("WMTS Layers"),
1032
- "description": Literal(
1049
+ "description": c2cgeoportal_commons.lib.literal.Literal(
1033
1050
  _(
1034
1051
  """
1035
1052
  <div class="help-block">
@@ -1077,15 +1094,15 @@ class LayerWMTS(DimensionLayer):
1077
1094
  ),
1078
1095
  }
1079
1096
  __c2cgeoform_config__ = {"duplicate": True}
1080
- __mapper_args__ = {"polymorphic_identity": "l_wmts"}
1097
+ __mapper_args__ = {"polymorphic_identity": "l_wmts"} # type: ignore[dict-item]
1081
1098
 
1082
- id = Column(
1099
+ id: Mapped[int] = mapped_column(
1083
1100
  Integer,
1084
- ForeignKey(_schema + ".layer.id"),
1101
+ ForeignKey(_schema + ".layer.id", ondelete="CASCADE"),
1085
1102
  primary_key=True,
1086
1103
  info={"colanderalchemy": {"missing": None, "widget": HiddenWidget()}},
1087
1104
  )
1088
- url = Column(
1105
+ url: Mapped[str] = mapped_column(
1089
1106
  Unicode,
1090
1107
  nullable=False,
1091
1108
  info={
@@ -1096,7 +1113,7 @@ class LayerWMTS(DimensionLayer):
1096
1113
  }
1097
1114
  },
1098
1115
  )
1099
- layer = Column(
1116
+ layer: Mapped[str] = mapped_column(
1100
1117
  Unicode,
1101
1118
  nullable=False,
1102
1119
  info={
@@ -1107,18 +1124,21 @@ class LayerWMTS(DimensionLayer):
1107
1124
  }
1108
1125
  },
1109
1126
  )
1110
- style = Column(
1127
+ style: Mapped[str] = mapped_column(
1111
1128
  Unicode,
1129
+ nullable=True,
1112
1130
  info={
1113
1131
  "colanderalchemy": {
1114
1132
  "title": _("Style"),
1115
1133
  "description": _("The style to use; if not present, the default style is used."),
1116
1134
  "column": 2,
1135
+ "missing": "",
1117
1136
  }
1118
1137
  },
1119
1138
  )
1120
- matrix_set = Column(
1139
+ matrix_set: Mapped[str] = mapped_column(
1121
1140
  Unicode,
1141
+ nullable=True,
1122
1142
  info={
1123
1143
  "colanderalchemy": {
1124
1144
  "title": _("Matrix set"),
@@ -1127,16 +1147,17 @@ class LayerWMTS(DimensionLayer):
1127
1147
  "left empty."
1128
1148
  ),
1129
1149
  "column": 2,
1150
+ "missing": "",
1130
1151
  }
1131
1152
  },
1132
1153
  )
1133
- image_type = Column(
1134
- Enum("image/jpeg", "image/png", native_enum=False),
1154
+ image_type: Mapped[ImageType] = mapped_column(
1155
+ Enum(*get_args(ImageType), native_enum=False),
1135
1156
  nullable=False,
1136
1157
  info={
1137
1158
  "colanderalchemy": {
1138
1159
  "title": _("Image type"),
1139
- "description": Literal(
1160
+ "description": c2cgeoportal_commons.lib.literal.Literal(
1140
1161
  _(
1141
1162
  """
1142
1163
  The MIME type of the images (e.g.: <code>image/png</code>).
@@ -1144,17 +1165,17 @@ class LayerWMTS(DimensionLayer):
1144
1165
  )
1145
1166
  ),
1146
1167
  "column": 2,
1147
- "widget": SelectWidget(values=(("image/jpeg", "image/jpeg"), ("image/png", "image/png"))),
1168
+ "widget": SelectWidget(values=list((e, e) for e in get_args(ImageType))),
1148
1169
  }
1149
1170
  },
1150
1171
  )
1151
1172
 
1152
- def __init__(self, name: str = "", public: bool = True, image_type: str = "image/png") -> None:
1173
+ def __init__(self, name: str = "", public: bool = True, image_type: ImageType = "image/png") -> None:
1153
1174
  super().__init__(name=name, public=public)
1154
1175
  self.image_type = image_type
1155
1176
 
1156
1177
  @staticmethod
1157
- def get_default(dbsession: Session) -> Optional[DimensionLayer]:
1178
+ def get_default(dbsession: Session) -> DimensionLayer | None:
1158
1179
  return cast(
1159
1180
  Optional[DimensionLayer],
1160
1181
  dbsession.query(LayerWMTS).filter(LayerWMTS.name == "wmts-defaults").one_or_none(),
@@ -1190,6 +1211,58 @@ layer_ra = Table(
1190
1211
  )
1191
1212
 
1192
1213
 
1214
+ class LayerCOG(Layer):
1215
+ """The Cloud Optimized GeoTIFF layer table representation."""
1216
+
1217
+ __tablename__ = "layer_cog"
1218
+ __table_args__ = {"schema": _schema}
1219
+ __colanderalchemy_config__ = {
1220
+ "title": _("COG Layer"),
1221
+ "plural": _("COG Layers"),
1222
+ "description": c2cgeoportal_commons.lib.literal.Literal(
1223
+ _(
1224
+ """
1225
+ <div class="help-block">
1226
+ <p>Definition of a <code>COG Layer</code> (COG for
1227
+ <a href="https://www.cogeo.org/">Cloud Optimized GeoTIFF</a>).</p>
1228
+ <p>Note: The layers named <code>cog-defaults</code> contains the values
1229
+ used when we create a new <code>COG layer</code>.</p>
1230
+ </div>
1231
+ """
1232
+ )
1233
+ ),
1234
+ }
1235
+ __c2cgeoform_config__ = {"duplicate": True}
1236
+ __mapper_args__ = {"polymorphic_identity": "l_cog"} # type: ignore[dict-item]
1237
+
1238
+ id: Mapped[int] = mapped_column(
1239
+ Integer,
1240
+ ForeignKey(_schema + ".layer.id"),
1241
+ primary_key=True,
1242
+ info={"colanderalchemy": {"missing": None, "widget": HiddenWidget()}},
1243
+ )
1244
+ url: Mapped[str] = mapped_column(
1245
+ Unicode,
1246
+ nullable=False,
1247
+ info={
1248
+ "colanderalchemy": {
1249
+ "title": _("URL"),
1250
+ "description": _("URL of the COG file."),
1251
+ "column": 2,
1252
+ }
1253
+ },
1254
+ )
1255
+
1256
+ @staticmethod
1257
+ def get_default(dbsession: Session) -> Layer | None:
1258
+ return dbsession.query(LayerCOG).filter(LayerCOG.name == "cog-defaults").one_or_none()
1259
+
1260
+ def url_description(self, request: pyramid.request.Request) -> str:
1261
+ errors: set[str] = set()
1262
+ url = get_url2(self.name, self.url, request, errors)
1263
+ return url.url() if url else "\n".join(errors)
1264
+
1265
+
1193
1266
  class LayerVectorTiles(DimensionLayer):
1194
1267
  """The layer_vectortiles table representation."""
1195
1268
 
@@ -1198,7 +1271,7 @@ class LayerVectorTiles(DimensionLayer):
1198
1271
  __colanderalchemy_config__ = {
1199
1272
  "title": _("Vector Tiles Layer"),
1200
1273
  "plural": _("Vector Tiles Layers"),
1201
- "description": Literal(
1274
+ "description": c2cgeoportal_commons.lib.literal.Literal(
1202
1275
  _(
1203
1276
  """
1204
1277
  <div class="help-block">
@@ -1232,16 +1305,16 @@ class LayerVectorTiles(DimensionLayer):
1232
1305
 
1233
1306
  __c2cgeoform_config__ = {"duplicate": True}
1234
1307
 
1235
- __mapper_args__ = {"polymorphic_identity": "l_mvt"}
1308
+ __mapper_args__ = {"polymorphic_identity": "l_mvt"} # type: ignore[dict-item]
1236
1309
 
1237
- id = Column(
1310
+ id: Mapped[int] = mapped_column(
1238
1311
  Integer,
1239
1312
  ForeignKey(_schema + ".layer.id"),
1240
1313
  primary_key=True,
1241
1314
  info={"colanderalchemy": {"missing": None, "widget": HiddenWidget()}},
1242
1315
  )
1243
1316
 
1244
- style = Column(
1317
+ style: Mapped[str] = mapped_column(
1245
1318
  Unicode,
1246
1319
  nullable=False,
1247
1320
  info={
@@ -1257,7 +1330,7 @@ class LayerVectorTiles(DimensionLayer):
1257
1330
  },
1258
1331
  )
1259
1332
 
1260
- sql = Column(
1333
+ sql: Mapped[str] = mapped_column(
1261
1334
  Unicode,
1262
1335
  nullable=True,
1263
1336
  info={
@@ -1274,7 +1347,7 @@ class LayerVectorTiles(DimensionLayer):
1274
1347
  },
1275
1348
  )
1276
1349
 
1277
- xyz = Column(
1350
+ xyz: Mapped[str] = mapped_column(
1278
1351
  Unicode,
1279
1352
  nullable=True,
1280
1353
  info={
@@ -1297,7 +1370,7 @@ class LayerVectorTiles(DimensionLayer):
1297
1370
  self.sql = sql
1298
1371
 
1299
1372
  @staticmethod
1300
- def get_default(dbsession: Session) -> Optional[DimensionLayer]:
1373
+ def get_default(dbsession: Session) -> DimensionLayer | None:
1301
1374
  return cast(
1302
1375
  Optional[DimensionLayer],
1303
1376
  dbsession.query(LayerVectorTiles)
@@ -1306,7 +1379,7 @@ class LayerVectorTiles(DimensionLayer):
1306
1379
  )
1307
1380
 
1308
1381
  def style_description(self, request: pyramid.request.Request) -> str:
1309
- errors: Set[str] = set()
1382
+ errors: set[str] = set()
1310
1383
  url = get_url2(self.name, self.style, request, errors)
1311
1384
  return url.url() if url else "\n".join(errors)
1312
1385
 
@@ -1318,9 +1391,11 @@ class RestrictionArea(Base): # type: ignore
1318
1391
  __table_args__ = {"schema": _schema}
1319
1392
  __colanderalchemy_config__ = {"title": _("Restriction area"), "plural": _("Restriction areas")}
1320
1393
  __c2cgeoform_config__ = {"duplicate": True}
1321
- id = Column(Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}})
1394
+ id: Mapped[int] = mapped_column(
1395
+ Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}}
1396
+ )
1322
1397
 
1323
- name = Column(
1398
+ name: Mapped[str] = mapped_column(
1324
1399
  Unicode,
1325
1400
  info={
1326
1401
  "colanderalchemy": {
@@ -1329,7 +1404,7 @@ class RestrictionArea(Base): # type: ignore
1329
1404
  }
1330
1405
  },
1331
1406
  )
1332
- description = Column(
1407
+ description: Mapped[str | None] = mapped_column(
1333
1408
  Unicode,
1334
1409
  info={
1335
1410
  "colanderalchemy": {
@@ -1338,7 +1413,7 @@ class RestrictionArea(Base): # type: ignore
1338
1413
  }
1339
1414
  },
1340
1415
  )
1341
- readwrite = Column(
1416
+ readwrite: Mapped[bool] = mapped_column(
1342
1417
  Boolean,
1343
1418
  default=False,
1344
1419
  info={
@@ -1348,7 +1423,7 @@ class RestrictionArea(Base): # type: ignore
1348
1423
  }
1349
1424
  },
1350
1425
  )
1351
- area = Column(
1426
+ area = mapped_column(
1352
1427
  Geometry("POLYGON", srid=_srid),
1353
1428
  info={
1354
1429
  "colanderalchemy": {
@@ -1393,7 +1468,7 @@ class RestrictionArea(Base): # type: ignore
1393
1468
  "colanderalchemy": {
1394
1469
  "title": _("Layers"),
1395
1470
  "exclude": True,
1396
- "description": Literal(
1471
+ "description": c2cgeoportal_commons.lib.literal.Literal(
1397
1472
  _(
1398
1473
  """
1399
1474
  <div class="help-block">
@@ -1422,9 +1497,9 @@ class RestrictionArea(Base): # type: ignore
1422
1497
  self,
1423
1498
  name: str = "",
1424
1499
  description: str = "",
1425
- layers: Optional[List[Layer]] = None,
1426
- roles: Optional[List[Role]] = None,
1427
- area: Geometry = None,
1500
+ layers: list[Layer] | None = None,
1501
+ roles: list[Role] | None = None,
1502
+ area: Geometry | None = None,
1428
1503
  readwrite: bool = False,
1429
1504
  ) -> None:
1430
1505
  if layers is None:
@@ -1478,8 +1553,10 @@ class Interface(Base): # type: ignore
1478
1553
  __c2cgeoform_config__ = {"duplicate": True}
1479
1554
  __colanderalchemy_config__ = {"title": _("Interface"), "plural": _("Interfaces")}
1480
1555
 
1481
- id = Column(Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}})
1482
- name = Column(
1556
+ id: Mapped[int] = mapped_column(
1557
+ Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}}
1558
+ )
1559
+ name: Mapped[str] = mapped_column(
1483
1560
  Unicode,
1484
1561
  info={
1485
1562
  "colanderalchemy": {
@@ -1488,7 +1565,7 @@ class Interface(Base): # type: ignore
1488
1565
  }
1489
1566
  },
1490
1567
  )
1491
- description = Column(
1568
+ description: Mapped[str | None] = mapped_column(
1492
1569
  Unicode,
1493
1570
  info={
1494
1571
  "colanderalchemy": {
@@ -1550,18 +1627,23 @@ class Metadata(Base): # type: ignore
1550
1627
  "plural": _("Metadatas"),
1551
1628
  }
1552
1629
 
1553
- id = Column(Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}})
1554
- name = Column(
1630
+ id: Mapped[int] = mapped_column(
1631
+ Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}}
1632
+ )
1633
+ name: Mapped[str] = mapped_column(
1555
1634
  Unicode,
1556
1635
  info={
1557
1636
  "colanderalchemy": {
1558
1637
  "title": _("Name"),
1559
- "description": Literal(_("The type of <code>Metadata</code> we want to set.")),
1638
+ "description": c2cgeoportal_commons.lib.literal.Literal(
1639
+ _("The type of <code>Metadata</code> we want to set.")
1640
+ ),
1560
1641
  }
1561
1642
  },
1562
1643
  )
1563
- value = Column(
1644
+ value: Mapped[str] = mapped_column(
1564
1645
  Unicode,
1646
+ nullable=True,
1565
1647
  info={
1566
1648
  "colanderalchemy": {
1567
1649
  "title": _("Value"),
@@ -1570,7 +1652,7 @@ class Metadata(Base): # type: ignore
1570
1652
  }
1571
1653
  },
1572
1654
  )
1573
- description = Column(
1655
+ description: Mapped[str | None] = mapped_column(
1574
1656
  Unicode,
1575
1657
  info={
1576
1658
  "colanderalchemy": {
@@ -1581,10 +1663,10 @@ class Metadata(Base): # type: ignore
1581
1663
  },
1582
1664
  )
1583
1665
 
1584
- item_id = Column(
1666
+ item_id: Mapped[int] = mapped_column(
1585
1667
  "item_id",
1586
1668
  Integer,
1587
- ForeignKey(_schema + ".treeitem.id"),
1669
+ ForeignKey(_schema + ".treeitem.id", ondelete="CASCADE"),
1588
1670
  nullable=False,
1589
1671
  info={"colanderalchemy": {"exclude": True}, "c2cgeoform": {"duplicate": False}},
1590
1672
  )
@@ -1598,7 +1680,7 @@ class Metadata(Base): # type: ignore
1598
1680
  info={
1599
1681
  "colanderalchemy": {
1600
1682
  "title": _("Metadatas"),
1601
- "description": Literal(
1683
+ "description": c2cgeoportal_commons.lib.literal.Literal(
1602
1684
  _(
1603
1685
  """
1604
1686
  <div class="help-block">
@@ -1631,7 +1713,7 @@ class Metadata(Base): # type: ignore
1631
1713
  ),
1632
1714
  )
1633
1715
 
1634
- def __init__(self, name: str = "", value: str = "", description: Optional[str] = None) -> None:
1716
+ def __init__(self, name: str = "", value: str = "", description: str | None = None) -> None:
1635
1717
  self.name = name
1636
1718
  self.value = value
1637
1719
  self.description = description
@@ -1655,8 +1737,10 @@ class Dimension(Base): # type: ignore
1655
1737
  "plural": _("Dimensions"),
1656
1738
  }
1657
1739
 
1658
- id = Column(Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}})
1659
- name = Column(
1740
+ id: Mapped[int] = mapped_column(
1741
+ Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}}
1742
+ )
1743
+ name: Mapped[str] = mapped_column(
1660
1744
  Unicode,
1661
1745
  info={
1662
1746
  "colanderalchemy": {
@@ -1665,8 +1749,9 @@ class Dimension(Base): # type: ignore
1665
1749
  }
1666
1750
  },
1667
1751
  )
1668
- value = Column(
1752
+ value: Mapped[str] = mapped_column(
1669
1753
  Unicode,
1754
+ nullable=True,
1670
1755
  info={
1671
1756
  "colanderalchemy": {
1672
1757
  "title": _("Value"),
@@ -1674,7 +1759,7 @@ class Dimension(Base): # type: ignore
1674
1759
  }
1675
1760
  },
1676
1761
  )
1677
- field = Column(
1762
+ field: Mapped[str | None] = mapped_column(
1678
1763
  Unicode,
1679
1764
  info={
1680
1765
  "colanderalchemy": {
@@ -1685,7 +1770,7 @@ class Dimension(Base): # type: ignore
1685
1770
  }
1686
1771
  },
1687
1772
  )
1688
- description = Column(
1773
+ description: Mapped[str | None] = mapped_column(
1689
1774
  Unicode,
1690
1775
  info={
1691
1776
  "colanderalchemy": {
@@ -1696,7 +1781,7 @@ class Dimension(Base): # type: ignore
1696
1781
  },
1697
1782
  )
1698
1783
 
1699
- layer_id = Column(
1784
+ layer_id: Mapped[int] = mapped_column(
1700
1785
  "layer_id",
1701
1786
  Integer,
1702
1787
  ForeignKey(_schema + ".layer.id"),
@@ -1713,7 +1798,7 @@ class Dimension(Base): # type: ignore
1713
1798
  "colanderalchemy": {
1714
1799
  "title": _("Dimensions"),
1715
1800
  "exclude": True,
1716
- "description": Literal(
1801
+ "description": c2cgeoportal_commons.lib.literal.Literal(
1717
1802
  _(
1718
1803
  """
1719
1804
  <div class="help-block">
@@ -1732,9 +1817,9 @@ class Dimension(Base): # type: ignore
1732
1817
  self,
1733
1818
  name: str = "",
1734
1819
  value: str = "",
1735
- layer: Optional[str] = None,
1736
- field: Optional[str] = None,
1737
- description: Optional[str] = None,
1820
+ layer: str | None = None,
1821
+ field: str | None = None,
1822
+ description: str | None = None,
1738
1823
  ) -> None:
1739
1824
  self.name = name
1740
1825
  self.value = value
@@ -1767,8 +1852,8 @@ class AbstractLog(AbstractConcreteBase, Base): # type: ignore
1767
1852
  "plural": _("Logs"),
1768
1853
  }
1769
1854
 
1770
- id = Column(Integer, primary_key=True, info={"colanderalchemy": {}})
1771
- date = Column(
1855
+ id: Mapped[int] = mapped_column(Integer, primary_key=True, info={"colanderalchemy": {}})
1856
+ date: Mapped[datetime] = mapped_column(
1772
1857
  DateTime(timezone=True),
1773
1858
  nullable=False,
1774
1859
  info={
@@ -1777,7 +1862,7 @@ class AbstractLog(AbstractConcreteBase, Base): # type: ignore
1777
1862
  }
1778
1863
  },
1779
1864
  )
1780
- action = Column(
1865
+ action: Mapped[LogAction] = mapped_column(
1781
1866
  Enum(LogAction, native_enum=False),
1782
1867
  nullable=False,
1783
1868
  info={
@@ -1786,7 +1871,7 @@ class AbstractLog(AbstractConcreteBase, Base): # type: ignore
1786
1871
  }
1787
1872
  },
1788
1873
  )
1789
- element_type = Column(
1874
+ element_type: Mapped[str] = mapped_column(
1790
1875
  String(50),
1791
1876
  nullable=False,
1792
1877
  info={
@@ -1795,7 +1880,7 @@ class AbstractLog(AbstractConcreteBase, Base): # type: ignore
1795
1880
  }
1796
1881
  },
1797
1882
  )
1798
- element_id = Column(
1883
+ element_id: Mapped[int] = mapped_column(
1799
1884
  Integer,
1800
1885
  nullable=False,
1801
1886
  info={
@@ -1804,7 +1889,7 @@ class AbstractLog(AbstractConcreteBase, Base): # type: ignore
1804
1889
  }
1805
1890
  },
1806
1891
  )
1807
- element_name = Column(
1892
+ element_name: Mapped[str] = mapped_column(
1808
1893
  Unicode,
1809
1894
  nullable=False,
1810
1895
  info={
@@ -1813,7 +1898,7 @@ class AbstractLog(AbstractConcreteBase, Base): # type: ignore
1813
1898
  }
1814
1899
  },
1815
1900
  )
1816
- element_url_table = Column(
1901
+ element_url_table: Mapped[str] = mapped_column(
1817
1902
  Unicode,
1818
1903
  nullable=False,
1819
1904
  info={
@@ -1822,7 +1907,7 @@ class AbstractLog(AbstractConcreteBase, Base): # type: ignore
1822
1907
  }
1823
1908
  },
1824
1909
  )
1825
- username = Column(
1910
+ username: Mapped[str] = mapped_column(
1826
1911
  Unicode,
1827
1912
  nullable=False,
1828
1913
  info={