flood-adapt 0.3.0__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.
Files changed (139) hide show
  1. flood_adapt/__init__.py +22 -0
  2. flood_adapt/adapter/__init__.py +9 -0
  3. flood_adapt/adapter/fiat_adapter.py +1502 -0
  4. flood_adapt/adapter/interface/__init__.py +0 -0
  5. flood_adapt/adapter/interface/hazard_adapter.py +70 -0
  6. flood_adapt/adapter/interface/impact_adapter.py +36 -0
  7. flood_adapt/adapter/interface/model_adapter.py +89 -0
  8. flood_adapt/adapter/interface/offshore.py +19 -0
  9. flood_adapt/adapter/sfincs_adapter.py +1857 -0
  10. flood_adapt/adapter/sfincs_offshore.py +193 -0
  11. flood_adapt/config/__init__.py +0 -0
  12. flood_adapt/config/config.py +245 -0
  13. flood_adapt/config/fiat.py +219 -0
  14. flood_adapt/config/gui.py +224 -0
  15. flood_adapt/config/sfincs.py +336 -0
  16. flood_adapt/config/site.py +124 -0
  17. flood_adapt/database_builder/__init__.py +0 -0
  18. flood_adapt/database_builder/database_builder.py +2175 -0
  19. flood_adapt/database_builder/templates/default_units/imperial.toml +9 -0
  20. flood_adapt/database_builder/templates/default_units/metric.toml +9 -0
  21. flood_adapt/database_builder/templates/green_infra_table/green_infra_lookup_table.csv +10 -0
  22. flood_adapt/database_builder/templates/icons/black_down_48x48.png +0 -0
  23. flood_adapt/database_builder/templates/icons/black_left_48x48.png +0 -0
  24. flood_adapt/database_builder/templates/icons/black_right_48x48.png +0 -0
  25. flood_adapt/database_builder/templates/icons/black_up_48x48.png +0 -0
  26. flood_adapt/database_builder/templates/icons/icons8-triangle-arrow-16_white_down.png +0 -0
  27. flood_adapt/database_builder/templates/icons/icons8-triangle-arrow-16_white_left.png +0 -0
  28. flood_adapt/database_builder/templates/icons/icons8-triangle-arrow-16_white_right.png +0 -0
  29. flood_adapt/database_builder/templates/icons/icons8-triangle-arrow-16_white_up.png +0 -0
  30. flood_adapt/database_builder/templates/icons/icons8-triangle-arrow-24_black_down.png +0 -0
  31. flood_adapt/database_builder/templates/icons/icons8-triangle-arrow-24_black_left.png +0 -0
  32. flood_adapt/database_builder/templates/icons/icons8-triangle-arrow-24_black_right.png +0 -0
  33. flood_adapt/database_builder/templates/icons/icons8-triangle-arrow-24_black_up.png +0 -0
  34. flood_adapt/database_builder/templates/icons/icons8-triangle-arrow-24_white_left.png +0 -0
  35. flood_adapt/database_builder/templates/icons/icons8-triangle-arrow-24_white_right.png +0 -0
  36. flood_adapt/database_builder/templates/icons/white_down_48x48.png +0 -0
  37. flood_adapt/database_builder/templates/icons/white_left_48x48.png +0 -0
  38. flood_adapt/database_builder/templates/icons/white_right_48x48.png +0 -0
  39. flood_adapt/database_builder/templates/icons/white_up_48x48.png +0 -0
  40. flood_adapt/database_builder/templates/infographics/OSM/config_charts.toml +90 -0
  41. flood_adapt/database_builder/templates/infographics/OSM/config_people.toml +57 -0
  42. flood_adapt/database_builder/templates/infographics/OSM/config_risk_charts.toml +121 -0
  43. flood_adapt/database_builder/templates/infographics/OSM/config_roads.toml +65 -0
  44. flood_adapt/database_builder/templates/infographics/OSM/styles.css +45 -0
  45. flood_adapt/database_builder/templates/infographics/US_NSI/config_charts.toml +126 -0
  46. flood_adapt/database_builder/templates/infographics/US_NSI/config_people.toml +60 -0
  47. flood_adapt/database_builder/templates/infographics/US_NSI/config_risk_charts.toml +121 -0
  48. flood_adapt/database_builder/templates/infographics/US_NSI/config_roads.toml +65 -0
  49. flood_adapt/database_builder/templates/infographics/US_NSI/styles.css +45 -0
  50. flood_adapt/database_builder/templates/infographics/images/ambulance.png +0 -0
  51. flood_adapt/database_builder/templates/infographics/images/car.png +0 -0
  52. flood_adapt/database_builder/templates/infographics/images/cart.png +0 -0
  53. flood_adapt/database_builder/templates/infographics/images/firetruck.png +0 -0
  54. flood_adapt/database_builder/templates/infographics/images/hospital.png +0 -0
  55. flood_adapt/database_builder/templates/infographics/images/house.png +0 -0
  56. flood_adapt/database_builder/templates/infographics/images/info.png +0 -0
  57. flood_adapt/database_builder/templates/infographics/images/money.png +0 -0
  58. flood_adapt/database_builder/templates/infographics/images/person.png +0 -0
  59. flood_adapt/database_builder/templates/infographics/images/school.png +0 -0
  60. flood_adapt/database_builder/templates/infographics/images/truck.png +0 -0
  61. flood_adapt/database_builder/templates/infographics/images/walking_person.png +0 -0
  62. flood_adapt/database_builder/templates/infometrics/OSM/metrics_additional_risk_configs.toml +4 -0
  63. flood_adapt/database_builder/templates/infometrics/OSM/with_SVI/infographic_metrics_config.toml +143 -0
  64. flood_adapt/database_builder/templates/infometrics/OSM/with_SVI/infographic_metrics_config_risk.toml +153 -0
  65. flood_adapt/database_builder/templates/infometrics/OSM/without_SVI/infographic_metrics_config.toml +127 -0
  66. flood_adapt/database_builder/templates/infometrics/OSM/without_SVI/infographic_metrics_config_risk.toml +57 -0
  67. flood_adapt/database_builder/templates/infometrics/US_NSI/metrics_additional_risk_configs.toml +4 -0
  68. flood_adapt/database_builder/templates/infometrics/US_NSI/with_SVI/infographic_metrics_config.toml +191 -0
  69. flood_adapt/database_builder/templates/infometrics/US_NSI/with_SVI/infographic_metrics_config_risk.toml +153 -0
  70. flood_adapt/database_builder/templates/infometrics/US_NSI/without_SVI/infographic_metrics_config.toml +178 -0
  71. flood_adapt/database_builder/templates/infometrics/US_NSI/without_SVI/infographic_metrics_config_risk.toml +57 -0
  72. flood_adapt/database_builder/templates/infometrics/mandatory_metrics_config.toml +9 -0
  73. flood_adapt/database_builder/templates/infometrics/mandatory_metrics_config_risk.toml +65 -0
  74. flood_adapt/database_builder/templates/mapbox_layers/bin_colors.toml +5 -0
  75. flood_adapt/database_builder.py +16 -0
  76. flood_adapt/dbs_classes/__init__.py +21 -0
  77. flood_adapt/dbs_classes/database.py +716 -0
  78. flood_adapt/dbs_classes/dbs_benefit.py +97 -0
  79. flood_adapt/dbs_classes/dbs_event.py +91 -0
  80. flood_adapt/dbs_classes/dbs_measure.py +103 -0
  81. flood_adapt/dbs_classes/dbs_projection.py +52 -0
  82. flood_adapt/dbs_classes/dbs_scenario.py +150 -0
  83. flood_adapt/dbs_classes/dbs_static.py +261 -0
  84. flood_adapt/dbs_classes/dbs_strategy.py +147 -0
  85. flood_adapt/dbs_classes/dbs_template.py +302 -0
  86. flood_adapt/dbs_classes/interface/database.py +147 -0
  87. flood_adapt/dbs_classes/interface/element.py +137 -0
  88. flood_adapt/dbs_classes/interface/static.py +47 -0
  89. flood_adapt/flood_adapt.py +1371 -0
  90. flood_adapt/misc/__init__.py +0 -0
  91. flood_adapt/misc/database_user.py +16 -0
  92. flood_adapt/misc/log.py +183 -0
  93. flood_adapt/misc/path_builder.py +54 -0
  94. flood_adapt/misc/utils.py +185 -0
  95. flood_adapt/objects/__init__.py +59 -0
  96. flood_adapt/objects/benefits/__init__.py +0 -0
  97. flood_adapt/objects/benefits/benefits.py +61 -0
  98. flood_adapt/objects/events/__init__.py +0 -0
  99. flood_adapt/objects/events/event_factory.py +135 -0
  100. flood_adapt/objects/events/event_set.py +84 -0
  101. flood_adapt/objects/events/events.py +221 -0
  102. flood_adapt/objects/events/historical.py +55 -0
  103. flood_adapt/objects/events/hurricane.py +64 -0
  104. flood_adapt/objects/events/synthetic.py +48 -0
  105. flood_adapt/objects/forcing/__init__.py +0 -0
  106. flood_adapt/objects/forcing/csv.py +68 -0
  107. flood_adapt/objects/forcing/discharge.py +66 -0
  108. flood_adapt/objects/forcing/forcing.py +142 -0
  109. flood_adapt/objects/forcing/forcing_factory.py +182 -0
  110. flood_adapt/objects/forcing/meteo_handler.py +93 -0
  111. flood_adapt/objects/forcing/netcdf.py +40 -0
  112. flood_adapt/objects/forcing/plotting.py +428 -0
  113. flood_adapt/objects/forcing/rainfall.py +98 -0
  114. flood_adapt/objects/forcing/tide_gauge.py +191 -0
  115. flood_adapt/objects/forcing/time_frame.py +77 -0
  116. flood_adapt/objects/forcing/timeseries.py +552 -0
  117. flood_adapt/objects/forcing/unit_system.py +580 -0
  118. flood_adapt/objects/forcing/waterlevels.py +108 -0
  119. flood_adapt/objects/forcing/wind.py +124 -0
  120. flood_adapt/objects/measures/__init__.py +0 -0
  121. flood_adapt/objects/measures/measure_factory.py +92 -0
  122. flood_adapt/objects/measures/measures.py +506 -0
  123. flood_adapt/objects/object_model.py +68 -0
  124. flood_adapt/objects/projections/__init__.py +0 -0
  125. flood_adapt/objects/projections/projections.py +89 -0
  126. flood_adapt/objects/scenarios/__init__.py +0 -0
  127. flood_adapt/objects/scenarios/scenarios.py +22 -0
  128. flood_adapt/objects/strategies/__init__.py +0 -0
  129. flood_adapt/objects/strategies/strategies.py +68 -0
  130. flood_adapt/workflows/__init__.py +0 -0
  131. flood_adapt/workflows/benefit_runner.py +541 -0
  132. flood_adapt/workflows/floodmap.py +85 -0
  133. flood_adapt/workflows/impacts_integrator.py +82 -0
  134. flood_adapt/workflows/scenario_runner.py +69 -0
  135. flood_adapt-0.3.0.dist-info/LICENSE +21 -0
  136. flood_adapt-0.3.0.dist-info/METADATA +183 -0
  137. flood_adapt-0.3.0.dist-info/RECORD +139 -0
  138. flood_adapt-0.3.0.dist-info/WHEEL +5 -0
  139. flood_adapt-0.3.0.dist-info/top_level.txt +1 -0
File without changes
@@ -0,0 +1,16 @@
1
+ from abc import ABC
2
+
3
+
4
+ class DatabaseUser(ABC):
5
+ """Abstract class for FloodAdapt classes that need to use / interact with the Singleton FloodAdapt database through the lazy-loading self.database property."""
6
+
7
+ _database_instance = None
8
+
9
+ @property
10
+ def database(self):
11
+ """Return the database for the object."""
12
+ if self._database_instance is None:
13
+ from flood_adapt.dbs_classes.database import Database
14
+
15
+ self._database_instance = Database()
16
+ return self._database_instance
@@ -0,0 +1,183 @@
1
+ import logging
2
+ import warnings
3
+ from contextlib import contextmanager
4
+ from pathlib import Path
5
+ from typing import List, Optional
6
+
7
+
8
+ class FloodAdaptLogging:
9
+ _DEFAULT_FORMATTER = logging.Formatter(
10
+ fmt="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
11
+ datefmt="%Y-%m-%d %I:%M:%S %p",
12
+ )
13
+ _root_logger = logging.getLogger("FloodAdapt")
14
+
15
+ def __init__(
16
+ self,
17
+ file_path: Optional[Path] = None,
18
+ loglevel_console: int = logging.WARNING,
19
+ loglevel_root: int = logging.INFO,
20
+ loglevel_files: int = logging.DEBUG,
21
+ formatter: logging.Formatter = _DEFAULT_FORMATTER,
22
+ ignore_warnings: Optional[list[type[Warning]]] = None,
23
+ ) -> None:
24
+ """Initialize the logging system for the FloodAdapt."""
25
+ self._formatter = formatter
26
+
27
+ self._root_logger.setLevel(loglevel_root)
28
+ if self._root_logger.hasHandlers():
29
+ self._root_logger.handlers.clear()
30
+
31
+ # Add file handler if provided
32
+ if file_path is not None:
33
+ self.add_file_handler(file_path, loglevel_files, formatter)
34
+
35
+ # Add console handler
36
+ console_handler = logging.StreamHandler()
37
+ console_handler.setLevel(loglevel_console)
38
+ console_handler.setFormatter(formatter)
39
+ self._root_logger.addHandler(console_handler)
40
+
41
+ if ignore_warnings:
42
+ for warn_type in ignore_warnings:
43
+ self.configure_warnings("ignore", category=warn_type)
44
+
45
+ @classmethod
46
+ def add_file_handler(
47
+ cls,
48
+ file_path: Path,
49
+ loglevel: int = logging.DEBUG,
50
+ formatter: Optional[logging.Formatter] = None,
51
+ ) -> None:
52
+ """Add a file handler to the logger that directs outputs to a the file."""
53
+ if not file_path:
54
+ raise ValueError("file_path must be provided.")
55
+ file_path = Path(file_path)
56
+
57
+ # check if the path is a only a filename
58
+ if not file_path.parents:
59
+ file_path = Path.cwd() / file_path
60
+ file_path.parent.mkdir(parents=True, exist_ok=True)
61
+
62
+ file_handler = logging.FileHandler(filename=file_path, mode="a")
63
+ file_handler.setLevel(loglevel)
64
+
65
+ formatter = formatter or cls._DEFAULT_FORMATTER
66
+ file_handler.setFormatter(formatter)
67
+
68
+ cls.getLogger().addHandler(file_handler)
69
+
70
+ @classmethod
71
+ def remove_file_handler(cls, file_path: Path) -> None:
72
+ """Remove a file handler from the logger, which stops sending logs to that file and closes it."""
73
+ for handler in cls.getLogger().handlers:
74
+ if isinstance(handler, logging.FileHandler) and handler.baseFilename == str(
75
+ file_path.resolve()
76
+ ):
77
+ handler.close()
78
+ cls.getLogger().removeHandler(handler)
79
+
80
+ @classmethod
81
+ def getLogger(
82
+ cls, name: Optional[str] = None, level: int = logging.INFO
83
+ ) -> logging.Logger:
84
+ """Get a logger with the specified name. If no name is provided, return the root logger.
85
+
86
+ If the logger does not exist, it is created with the specified level.
87
+
88
+ Parameters
89
+ ----------
90
+ name : str, optional
91
+ The name of the logger. If not provided, the root logger is returned.
92
+ level : int,
93
+ The level of the logger. The default is logging.INFO.
94
+
95
+ Returns
96
+ -------
97
+ logging.Logger
98
+ The logger with the specified name.
99
+ """
100
+ if name is None:
101
+ logger = cls._root_logger
102
+ else:
103
+ logger = logging.getLogger(f"FloodAdapt.{name}")
104
+ logger.setLevel(level)
105
+ return logger
106
+
107
+ @classmethod
108
+ def shutdown(cls):
109
+ root_logger = cls.getLogger()
110
+ handlers = root_logger.handlers[:]
111
+ for handler in handlers:
112
+ if isinstance(handler, logging.FileHandler):
113
+ handler.close()
114
+ root_logger.removeHandler(handler)
115
+ logging.shutdown()
116
+
117
+ @classmethod
118
+ @contextmanager
119
+ def to_file(
120
+ cls,
121
+ *,
122
+ file_path: Path,
123
+ loglevel: int = logging.DEBUG,
124
+ formatter: logging.Formatter = _DEFAULT_FORMATTER,
125
+ ):
126
+ """Open a file at filepath to write logs to. Does not affect other loggers.
127
+
128
+ When the context manager exits (via regular execution or an exception), the file is closed and the handler is removed.
129
+ """
130
+ if file_path is None:
131
+ raise ValueError(
132
+ "file_path must be provided as a key value pair: 'file_path=<file_path>'."
133
+ )
134
+ cls.add_file_handler(file_path, loglevel, formatter)
135
+ try:
136
+ yield
137
+ finally:
138
+ cls.remove_file_handler(file_path)
139
+
140
+ @classmethod
141
+ def _close_file_handlers(cls, logger: logging.Logger, exclude: List[Path]) -> None:
142
+ """Close and remove file handlers from a logger."""
143
+ for handler in logger.handlers[:]:
144
+ if (
145
+ isinstance(handler, logging.FileHandler)
146
+ and Path(handler.baseFilename) not in exclude
147
+ ):
148
+ handler.close()
149
+ logger.removeHandler(handler)
150
+
151
+ @classmethod
152
+ def close_files(cls, exclude: List[Path] = []) -> None:
153
+ """Close all file handlers except those in the exclude list."""
154
+ cls._close_file_handlers(cls.getLogger(), exclude)
155
+ cls._close_file_handlers(cls.getLogger("hydromt"), exclude)
156
+
157
+ @classmethod
158
+ def deprecation_warning(cls, version: str, reason: str):
159
+ """Log a deprecation warning with reason and the version that will remove it."""
160
+ warnings.warn(
161
+ f"DeprecationWarning: {reason}. This will be removed in version {version}.",
162
+ DeprecationWarning,
163
+ stacklevel=2,
164
+ )
165
+
166
+ @classmethod
167
+ def configure_warnings(
168
+ cls, action: str = "default", category: Optional[type[Warning]] = None
169
+ ):
170
+ """
171
+ Configure the behavior of Python warnings.
172
+
173
+ Parameters
174
+ ----------
175
+ action : str, optional
176
+ The action to take on warnings. Common actions include 'ignore', 'default', 'error', 'always', etc.
177
+ The default is 'default', which shows warnings once per triggering location.
178
+ category : type[Warning], optional
179
+ The category of warnings to configure. If not provided, all warnings are configured.
180
+ categories include DeprecationWarning, UserWarning, RuntimeWarning, etc.
181
+
182
+ """
183
+ warnings.simplefilter(action=action, category=category)
@@ -0,0 +1,54 @@
1
+ from enum import Enum
2
+ from pathlib import Path
3
+ from typing import Optional
4
+
5
+ from flood_adapt.config.config import Settings
6
+
7
+
8
+ class TopLevelDir(str, Enum):
9
+ """Top level directories in the database."""
10
+
11
+ input = "input"
12
+ output = "output"
13
+ static = "static"
14
+ temp = "temp"
15
+
16
+
17
+ class ObjectDir(str, Enum):
18
+ """The names for object directories at the second level of the database."""
19
+
20
+ site = "site"
21
+
22
+ benefit = "benefits"
23
+ event = "events"
24
+ strategy = "strategies"
25
+ measure = "measures"
26
+ projection = "projections"
27
+ scenario = "scenarios"
28
+ config = "config"
29
+
30
+ # buyout = "measures"
31
+ # elevate = "measures"
32
+ # floodproof = "measures"
33
+ # greening = "measures"
34
+ # floodwall = "measures"
35
+ # pump = "measures"
36
+
37
+
38
+ def db_path(
39
+ top_level_dir: TopLevelDir = TopLevelDir.input,
40
+ object_dir: Optional[ObjectDir] = None,
41
+ obj_name: Optional[str] = None,
42
+ ) -> Path:
43
+ """Return an path to a database directory from arguments."""
44
+ rel_path = Path(top_level_dir.value)
45
+ if object_dir is not None:
46
+ if isinstance(object_dir, ObjectDir):
47
+ rel_path = rel_path / object_dir.value
48
+ else:
49
+ rel_path = rel_path / str(object_dir)
50
+
51
+ if obj_name is not None:
52
+ rel_path = rel_path / obj_name
53
+
54
+ return Settings().database_path / rel_path
@@ -0,0 +1,185 @@
1
+ import os
2
+ import shutil
3
+ from contextlib import contextmanager
4
+ from pathlib import Path
5
+ from typing import Union
6
+
7
+ from pydantic import BeforeValidator
8
+
9
+ from flood_adapt.misc.path_builder import (
10
+ ObjectDir,
11
+ db_path,
12
+ )
13
+
14
+
15
+ @contextmanager
16
+ def modified_environ(*remove, **update):
17
+ """
18
+ Temporarily updates the ``os.environ`` dictionary in-place.
19
+
20
+ From https://github.com/laurent-laporte-pro/stackoverflow-q2059482/blob/master/demo/environ_ctx.py
21
+
22
+ The ``os.environ`` dictionary is updated in-place so that the modification
23
+ is sure to work in all situations.
24
+
25
+ :param remove: Environment variables to remove.
26
+ :param update: Dictionary of environment variables and values to add/update.
27
+ """
28
+ env = os.environ
29
+ update = update or {}
30
+ remove = remove or []
31
+
32
+ # List of environment variables being updated or removed.
33
+ stomped = (set(update.keys()) | set(remove)) & set(env.keys())
34
+ # Environment variables and values to restore on exit.
35
+ update_after = {k: env[k] for k in stomped}
36
+ # Environment variables and values to remove on exit.
37
+ remove_after = frozenset(k for k in update if k not in env)
38
+
39
+ try:
40
+ env.update(update)
41
+ [env.pop(k, None) for k in remove]
42
+ yield
43
+ finally:
44
+ env.update(update_after)
45
+ [env.pop(k) for k in remove_after]
46
+
47
+
48
+ @contextmanager
49
+ def cd(newdir: Path):
50
+ prevdir = Path().cwd()
51
+ os.chdir(newdir)
52
+ try:
53
+ yield
54
+ finally:
55
+ os.chdir(prevdir)
56
+
57
+
58
+ def write_finished_file(path: Path):
59
+ if not path.exists():
60
+ path.mkdir(parents=True)
61
+ with open(Path(path) / "finished.txt", "w") as f:
62
+ f.write("run finished")
63
+
64
+
65
+ def finished_file_exists(path: Path):
66
+ return (Path(path) / "finished.txt").exists()
67
+
68
+
69
+ def resolve_filepath(
70
+ object_dir: ObjectDir, obj_name: str, path: Path | str | os.PathLike
71
+ ) -> Path:
72
+ """
73
+ Determine whether a given path is an external file or a file in the database.
74
+
75
+ Users can set the path to a file in the database directly, meaning it will be an absolute path.
76
+ Users can also read the path from loading a toml file, meaning it will be a filename relative to the toml file.
77
+
78
+ Parameters
79
+ ----------
80
+ object_dir : ObjectDir
81
+ The directory name of the object in the database.
82
+ obj_name : str
83
+ The name of the object.
84
+ path : Union[Path, str, os.PathLike]
85
+ The path to the file, which can be an absolute path or a relative path.
86
+
87
+ Returns
88
+ -------
89
+ Path
90
+ The resolved path to the file.
91
+
92
+ Raises
93
+ ------
94
+ FileNotFoundError
95
+ If the file does not exist in either the provided path or the database path.
96
+ """
97
+ _path = Path(path)
98
+ if str(_path) == _path.name:
99
+ # this is a filename, so it is in the database
100
+ src_path = db_path(object_dir=object_dir, obj_name=obj_name) / path
101
+ else:
102
+ # this is a path, so it is an external file
103
+ src_path = Path(path)
104
+ return src_path
105
+
106
+
107
+ def save_file_to_database(
108
+ src_file: Path | str | os.PathLike, dst_dir: Path | str | os.PathLike
109
+ ) -> Path:
110
+ """Save a file to the database.
111
+
112
+ Parameters
113
+ ----------
114
+ src_file : Path | str | os.PathLike
115
+ Path to the file to be copied.
116
+ dst_dir : Path | str | os.PathLike
117
+ Path to the destination directory.
118
+
119
+ Returns
120
+ -------
121
+ Path
122
+ Path to the copied file
123
+
124
+ Raises
125
+ ------
126
+ FileNotFoundError
127
+ If the src_file does not exist at the given path
128
+ """
129
+ src_file = Path(src_file).resolve()
130
+ dst_file = Path(dst_dir).resolve() / src_file.name
131
+
132
+ if not src_file.exists():
133
+ raise FileNotFoundError(
134
+ f"Failed to find {src_file} when saving external file to the database as it does not exist."
135
+ )
136
+ if src_file == dst_file:
137
+ return dst_file
138
+ elif dst_file.exists():
139
+ if dst_file.suffix == ".shp":
140
+ for file in list(dst_file.parent.glob(f"{dst_file.stem}.*")):
141
+ os.remove(file)
142
+ else:
143
+ os.remove(dst_file)
144
+
145
+ dst_file.parent.mkdir(parents=True, exist_ok=True)
146
+ if src_file.suffix == ".shp":
147
+ for file in list(src_file.parent.glob(f"{src_file.stem}.*")):
148
+ shutil.copy2(file, dst_file.parent.joinpath(file.name))
149
+ else:
150
+ shutil.copy2(src_file, dst_file)
151
+
152
+ return dst_file
153
+
154
+
155
+ def copy_file_to_output_dir(file_path: Path, output_dir: Path) -> Path:
156
+ output_dir = output_dir.resolve()
157
+ if file_path == output_dir / file_path.name:
158
+ return file_path
159
+ output_dir.mkdir(parents=True, exist_ok=True)
160
+ shutil.copy2(file_path, output_dir)
161
+ return output_dir / file_path.name
162
+
163
+
164
+ def validate_file_extension(allowed_extensions: list[str]):
165
+ """Validate the extension of the given path has one of the given suffixes.
166
+
167
+ Examples
168
+ --------
169
+ >>> from pydantic import BaseModel
170
+ >>> from pathlib import Path
171
+ >>> from typing import Annotated
172
+
173
+ >>> class MyClass(BaseModel):
174
+ >>> csv_path: Annotated[Path, validate_file_extension([".csv"])]
175
+ """
176
+
177
+ def _validator(value: Union[Path, str, os.PathLike]) -> Path:
178
+ value = Path(value)
179
+ if value.suffix not in allowed_extensions:
180
+ raise ValueError(
181
+ f"Invalid file extension: {value}. Allowed extensions are {', '.join(allowed_extensions)}."
182
+ )
183
+ return value
184
+
185
+ return BeforeValidator(_validator)
@@ -0,0 +1,59 @@
1
+ from .benefits.benefits import Benefit
2
+ from .events.event_factory import EventFactory
3
+ from .events.event_set import EventSet
4
+ from .events.events import Event
5
+ from .events.historical import HistoricalEvent
6
+ from .events.hurricane import HurricaneEvent
7
+ from .events.synthetic import SyntheticEvent
8
+ from .forcing.forcing_factory import ForcingFactory
9
+ from .measures.measure_factory import MeasureFactory
10
+ from .measures.measures import (
11
+ Buyout,
12
+ Elevate,
13
+ FloodProof,
14
+ FloodWall,
15
+ GreenInfrastructure,
16
+ Measure,
17
+ MeasureType,
18
+ Pump,
19
+ SelectionType,
20
+ )
21
+ from .object_model import Object
22
+ from .projections.projections import Projection
23
+ from .scenarios.scenarios import Scenario
24
+ from .strategies.strategies import Strategy
25
+
26
+ __all__ = [
27
+ # Object
28
+ "Object",
29
+ # Measures
30
+ "MeasureFactory",
31
+ "Measure",
32
+ "MeasureType",
33
+ "SelectionType",
34
+ "MeasureType",
35
+ "Buyout",
36
+ "Elevate",
37
+ "FloodProof",
38
+ "FloodWall",
39
+ "GreenInfrastructure",
40
+ "MeasureType",
41
+ "Pump",
42
+ # Events
43
+ "Event",
44
+ "EventFactory",
45
+ "EventSet",
46
+ "SyntheticEvent",
47
+ "HistoricalEvent",
48
+ "HurricaneEvent",
49
+ # Forcing
50
+ "ForcingFactory",
51
+ # Projections
52
+ "Projection",
53
+ # Strategies
54
+ "Strategy",
55
+ # Scenarios
56
+ "Scenario",
57
+ # Benefits
58
+ "Benefit",
59
+ ]
File without changes
@@ -0,0 +1,61 @@
1
+ from typing import Optional
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from flood_adapt.objects.object_model import Object
6
+
7
+
8
+ class CurrentSituationModel(BaseModel):
9
+ """
10
+ The accepted input for a current situation in FloodAdapt.
11
+
12
+ Attributes
13
+ ----------
14
+ projection : str
15
+ The name of the projection. Should be a projection saved in the database.
16
+ year : int
17
+ The year of the current situation.
18
+ """
19
+
20
+ projection: str
21
+ year: int
22
+
23
+
24
+ class Benefit(Object):
25
+ """BaseModel describing the expected variables and data types of a Benefit analysis object.
26
+
27
+ Attributes
28
+ ----------
29
+ name: str
30
+ The name of the benefit analysis.
31
+ description: str, default=""
32
+ The description of the benefit analysis.
33
+ strategy : str
34
+ The name of the strategy. Should be a strategy saved in the database.
35
+ event_set : str
36
+ The name of the event set. Should be an event set saved in the database.
37
+ projection : str
38
+ The name of the projection. Should be a projection saved in the database.
39
+ future_year : int
40
+ The future year for the analysis.
41
+ current_situation : CurrentSituationModel
42
+ The current situation model.
43
+ baseline_strategy : str
44
+ The name of the baseline strategy.
45
+ discount_rate : float
46
+ The discount rate for the analysis.
47
+ implementation_cost : Optional[float], default = None
48
+ The implementation cost of the strategy.
49
+ annual_maint_cost : Optional[float], default = None
50
+ The annual maintenance cost of the strategy.
51
+ """
52
+
53
+ strategy: str
54
+ event_set: str
55
+ projection: str
56
+ future_year: int
57
+ current_situation: CurrentSituationModel
58
+ baseline_strategy: str
59
+ discount_rate: float
60
+ implementation_cost: Optional[float] = None
61
+ annual_maint_cost: Optional[float] = None
File without changes
@@ -0,0 +1,135 @@
1
+ from pathlib import Path
2
+ from typing import Any, List
3
+
4
+ import tomli
5
+
6
+ from flood_adapt.objects.events.event_set import EventSet
7
+ from flood_adapt.objects.events.events import (
8
+ Event,
9
+ Mode,
10
+ Template,
11
+ )
12
+ from flood_adapt.objects.events.historical import HistoricalEvent
13
+ from flood_adapt.objects.events.hurricane import (
14
+ HurricaneEvent,
15
+ TranslationModel,
16
+ )
17
+ from flood_adapt.objects.events.synthetic import SyntheticEvent
18
+
19
+ __all__ = ["TranslationModel", "EventSet"]
20
+
21
+
22
+ class EventFactory:
23
+ """Factory class for creating events.
24
+
25
+ This class is used to create events based on a template.
26
+
27
+ Attributes
28
+ ----------
29
+ _EVENT_TEMPLATES : dict[str, (Event, Event)]
30
+ Dictionary mapping event templates to event classes and models
31
+ """
32
+
33
+ _EVENT_TEMPLATES = {
34
+ Template.Hurricane: HurricaneEvent,
35
+ Template.Historical: HistoricalEvent,
36
+ Template.Synthetic: SyntheticEvent,
37
+ }
38
+
39
+ @staticmethod
40
+ def get_event_from_template(template: Template) -> type[Event]:
41
+ """Get the event class corresponding to the template.
42
+
43
+ Parameters
44
+ ----------
45
+ template : str
46
+ Name of the event template
47
+
48
+ Returns
49
+ -------
50
+ Type[Event]
51
+ Event template
52
+ """
53
+ if template not in EventFactory._EVENT_TEMPLATES:
54
+ raise ValueError(f"Invalid event template: {template}")
55
+ return EventFactory._EVENT_TEMPLATES[template]
56
+
57
+ @staticmethod
58
+ def read_template(filepath: Path) -> Template:
59
+ """Get event template from toml file."""
60
+ if not filepath.exists():
61
+ raise FileNotFoundError(f"File not found: {filepath}")
62
+ with open(filepath, mode="rb") as fp:
63
+ toml = tomli.load(fp)
64
+ if (template := toml.get("template")) is None:
65
+ raise ValueError(f"Event template not found in {filepath}")
66
+
67
+ return Template(template)
68
+
69
+ @staticmethod
70
+ def read_mode(filepath: Path) -> Mode:
71
+ """Get event mode from toml file."""
72
+ if not filepath.exists():
73
+ raise FileNotFoundError(f"File not found: {filepath}")
74
+ with open(filepath, mode="rb") as fp:
75
+ toml = tomli.load(fp)
76
+ if toml.get("mode") is None:
77
+ raise ValueError(f"Event mode not found in {filepath}")
78
+ return Mode(toml.get("mode"))
79
+
80
+ @staticmethod
81
+ def load_file(toml_file: Path) -> Event:
82
+ """Return event object based on toml file.
83
+
84
+ Parameters
85
+ ----------
86
+ toml_file : str
87
+ Template name
88
+
89
+ Returns
90
+ -------
91
+ Event
92
+ Event object
93
+ """
94
+ mode = EventFactory.read_mode(toml_file)
95
+ if mode == Mode.risk:
96
+ event_type = EventSet
97
+ elif mode == Mode.single_event:
98
+ template = Template(EventFactory.read_template(toml_file))
99
+ event_type = EventFactory.get_event_from_template(template)
100
+ else:
101
+ raise ValueError(f"Invalid event mode: {mode}")
102
+ return event_type.load_file(toml_file)
103
+
104
+ @staticmethod
105
+ def load_dict(attrs: dict[str, Any] | Event) -> Event | EventSet:
106
+ """Return event object based on attrs dict.
107
+
108
+ Parameters
109
+ ----------
110
+ attrs : dict[str, Any]
111
+ Event attributes
112
+
113
+ Returns
114
+ -------
115
+ Event
116
+ Event object based on template
117
+ """
118
+ if isinstance(attrs, Event):
119
+ mode = attrs.mode
120
+ template = attrs.template
121
+ else:
122
+ mode = Mode(attrs.get("mode"))
123
+ template = Template(attrs.get("template"))
124
+
125
+ if mode == Mode.risk:
126
+ # TODO Load events
127
+ return EventSet(**attrs)
128
+ elif mode == Mode.single_event:
129
+ return EventFactory.get_event_from_template(template)(**attrs)
130
+ else:
131
+ raise ValueError(f"Invalid event mode: {mode}")
132
+
133
+ @staticmethod
134
+ def get_allowed_forcings(template) -> dict[str, List[str]]:
135
+ return EventFactory.get_event_from_template(template).get_allowed_forcings()