From 8db099c5c728a1f399b00f490c2eff3ae92fad00 Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Mon, 31 Aug 2026 11:56:01 +0200 Subject: [PATCH 1/6] Moving points in network: selected node is now unselected after the end of moving, so clicking anywhere in the graph doesnt change his position anymore without refreshing the window --- src/View/Network/GraphWidget.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/View/Network/GraphWidget.py b/src/View/Network/GraphWidget.py index 6d6daa4c..46a1ff97 100644 --- a/src/View/Network/GraphWidget.py +++ b/src/View/Network/GraphWidget.py @@ -954,9 +954,10 @@ class GraphWidget(QGraphicsView): self.set_current_edge(edge) if not locked: if items and type(items[0]) is NodeItem: - self._mouse_origin_x = pos.x() - self._mouse_origin_y = pos.y() self._current_moved_node = items[0] + node_pos = self._current_moved_node.pos() + self._mouse_origin_x = node_pos.x() + self._mouse_origin_y = node_pos.y() if not locked: # Add nodes and edges @@ -1011,6 +1012,8 @@ class GraphWidget(QGraphicsView): self._mouse_origin_y) ) ) + # reset the current moved node + self._current_moved_node = None self.update() super(GraphWidget, self).mouseReleaseEvent(event) From e9aa980c886485af55a4714f3c86803a483e4432 Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Mon, 31 Aug 2026 14:08:09 +0200 Subject: [PATCH 2/6] Export geometry: popup displayed before (and not after anymore) the file browser to choose if we export only geometry points, or if we want interpolated ones too --- src/View/Geometry/Window.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/View/Geometry/Window.py b/src/View/Geometry/Window.py index 13ab9140..a870cee5 100644 --- a/src/View/Geometry/Window.py +++ b/src/View/Geometry/Window.py @@ -805,6 +805,10 @@ class GeometryWindow(PamhyrWindow): # self.update_plot_rkc() def export_to_file(self): + export_interpolated = self._ask_export_options() + if export_interpolated is None: + return + settings = QSettings( QSettings.IniFormat, QSettings.UserScope, 'MyOrg' @@ -834,11 +838,9 @@ class GeometryWindow(PamhyrWindow): suffix = Path(filename).suffix if suffix != "": if suffix == ".st" or suffix == ".ST": - export_interpolated = self._ask_export_options() - if export_interpolated is not None: - self._export_to_file_st( - filename[:-3], export_interpolated - ) + self._export_to_file_st( + filename[:-3], export_interpolated + ) else: # Warning popup when the format is not exportable win = QtWidgets.QMessageBox() @@ -849,9 +851,7 @@ class GeometryWindow(PamhyrWindow): pass else: - export_interpolated = self._ask_export_options() - if export_interpolated is not None: - self._export_to_file_st(filename, export_interpolated) + self._export_to_file_st(filename, export_interpolated) def _ask_export_options(self): win = QtWidgets.QMessageBox(self) From ef34fecc31be1e8da175f86a33126c549cb4747b Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Mon, 31 Aug 2026 14:21:53 +0200 Subject: [PATCH 3/6] Export geometry: doesnt export disabled profiles --- src/View/Geometry/Window.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/View/Geometry/Window.py b/src/View/Geometry/Window.py index a870cee5..93d46af7 100644 --- a/src/View/Geometry/Window.py +++ b/src/View/Geometry/Window.py @@ -888,8 +888,9 @@ class GeometryWindow(PamhyrWindow): if is_interpolated and not export_interpolated: continue - self._export_to_file_st_profile(wfile, profile, pid) - pid += 1 + if profile.enabled: + self._export_to_file_st_profile(wfile, profile, pid) + pid += 1 def _export_to_file_st_profile(self, wfile, profile, pid): num = f"{pid:>6}" From b7aab9f2636a909f7432ebb66c5954722dacdd03 Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Mon, 31 Aug 2026 14:42:45 +0200 Subject: [PATCH 4/6] Import geometry: debug case when we cannot open profiles after importing a new geometry --- src/View/Geometry/Profile/Window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/View/Geometry/Profile/Window.py b/src/View/Geometry/Profile/Window.py index 4ada0795..41f20bbe 100644 --- a/src/View/Geometry/Profile/Window.py +++ b/src/View/Geometry/Profile/Window.py @@ -171,7 +171,7 @@ class ProfileWindow(PamhyrWindow): def profiles(self): """Return profiles from the geometry model owning this profile.""" - return self._profile.reach.reach.profiles + return self._profile.reach.profiles def navigate_profile(self, offset): profiles = self.profiles() From 6586a7b1913d6ded61c571a7a30bf090e1a73762 Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Mon, 31 Aug 2026 16:14:28 +0200 Subject: [PATCH 5/6] Date/time parameters: fixed the link between the start date in the study settings and the display in the tables --- src/View/BoundaryCondition/Edit/Table.py | 8 +++++++- src/View/BoundaryCondition/Edit/Window.py | 3 ++- src/View/BoundaryConditionsAdisTS/Edit/Table.py | 8 +++++++- src/View/BoundaryConditionsAdisTS/Edit/Window.py | 3 ++- .../BoundaryConditionsTemperature/Edit/Table.py | 8 +++++++- .../BoundaryConditionsTemperature/Edit/Window.py | 3 ++- src/View/LateralContribution/Edit/Table.py | 8 +++++++- src/View/LateralContribution/Edit/Window.py | 3 ++- src/View/LateralContributionsAdisTS/Edit/Table.py | 8 +++++++- src/View/LateralContributionsAdisTS/Edit/Window.py | 3 ++- src/View/Tools/PamhyrTable.py | 14 +++++++++++++- src/View/WeatherParameters/Edit/Table.py | 8 +++++++- src/View/WeatherParameters/Edit/Window.py | 3 ++- 13 files changed, 67 insertions(+), 13 deletions(-) diff --git a/src/View/BoundaryCondition/Edit/Table.py b/src/View/BoundaryCondition/Edit/Table.py index 363aa19f..d36f56f5 100644 --- a/src/View/BoundaryCondition/Edit/Table.py +++ b/src/View/BoundaryCondition/Edit/Table.py @@ -97,7 +97,9 @@ class TableModel(PamhyrTableModel): if self._opt_data == "time": value = timestamp_to_old_pamhyr_date(int(v)) else: - value = str(datetime.fromtimestamp(v)) + value = str( + self._start_date + timedelta(seconds=float(v)) + ) else: value = f"{v}" @@ -114,6 +116,10 @@ class TableModel(PamhyrTableModel): column = index.column() try: + if (self._data.header[column] == "time" + and self._opt_data == "date"): + value = self.date_to_elapsed_seconds(value) + row = self.get_true_data_row(row) self._undo.push( diff --git a/src/View/BoundaryCondition/Edit/Window.py b/src/View/BoundaryCondition/Edit/Window.py index 29281d6f..788b1203 100644 --- a/src/View/BoundaryCondition/Edit/Window.py +++ b/src/View/BoundaryCondition/Edit/Window.py @@ -173,7 +173,8 @@ class EditBoundaryConditionWindow(PamhyrWindow): }, data=self._data, undo=self._undo_stack, - opt_data=self._study.time_system + opt_data=self._study.time_system, + start_date=self._study.date ) table.setModel(self._table) diff --git a/src/View/BoundaryConditionsAdisTS/Edit/Table.py b/src/View/BoundaryConditionsAdisTS/Edit/Table.py index eeba2689..01179d92 100644 --- a/src/View/BoundaryConditionsAdisTS/Edit/Table.py +++ b/src/View/BoundaryConditionsAdisTS/Edit/Table.py @@ -87,7 +87,9 @@ class TableModel(PamhyrTableModel): if self._opt_data == "time": value = timestamp_to_old_pamhyr_date(int(v)) else: - value = str(datetime.fromtimestamp(v)) + value = str( + self._start_date + timedelta(seconds=float(v)) + ) else: value = f"{v}" @@ -104,6 +106,10 @@ class TableModel(PamhyrTableModel): column = index.column() try: + if (self._data.header[column] == "time" + and self._opt_data == "date"): + value = self.date_to_elapsed_seconds(value) + row = self.get_true_data_row(row) self._undo.push( diff --git a/src/View/BoundaryConditionsAdisTS/Edit/Window.py b/src/View/BoundaryConditionsAdisTS/Edit/Window.py index 9ae31212..09670489 100644 --- a/src/View/BoundaryConditionsAdisTS/Edit/Window.py +++ b/src/View/BoundaryConditionsAdisTS/Edit/Window.py @@ -120,7 +120,8 @@ class EditBoundaryConditionWindow(PamhyrWindow): }, data=self._data, undo=self._undo_stack, - opt_data=self._study.time_system + opt_data=self._study.time_system, + start_date=self._study.date ) table.setModel(self._table) diff --git a/src/View/BoundaryConditionsTemperature/Edit/Table.py b/src/View/BoundaryConditionsTemperature/Edit/Table.py index 812cc5ec..a29937b3 100644 --- a/src/View/BoundaryConditionsTemperature/Edit/Table.py +++ b/src/View/BoundaryConditionsTemperature/Edit/Table.py @@ -87,7 +87,9 @@ class TableModel(PamhyrTableModel): if self._opt_data == "time": value = timestamp_to_old_pamhyr_date(int(v)) else: - value = str(datetime.fromtimestamp(v)) + value = str( + self._start_date + timedelta(seconds=float(v)) + ) else: value = f"{v}" @@ -104,6 +106,10 @@ class TableModel(PamhyrTableModel): column = index.column() try: + if (self._data.header[column] == "time" + and self._opt_data == "date"): + value = self.date_to_elapsed_seconds(value) + row = self.get_true_data_row(row) self._undo.push( diff --git a/src/View/BoundaryConditionsTemperature/Edit/Window.py b/src/View/BoundaryConditionsTemperature/Edit/Window.py index 5d875d3e..0c7306a4 100644 --- a/src/View/BoundaryConditionsTemperature/Edit/Window.py +++ b/src/View/BoundaryConditionsTemperature/Edit/Window.py @@ -117,7 +117,8 @@ class EditBoundaryConditionWindow(PamhyrWindow): }, data=self._data, undo=self._undo_stack, - opt_data=self._study.time_system + opt_data=self._study.time_system, + start_date=self._study.date ) table.setModel(self._table) diff --git a/src/View/LateralContribution/Edit/Table.py b/src/View/LateralContribution/Edit/Table.py index 2c1e87ce..330d2092 100644 --- a/src/View/LateralContribution/Edit/Table.py +++ b/src/View/LateralContribution/Edit/Table.py @@ -76,7 +76,9 @@ class TableModel(PamhyrTableModel): if self._opt_data == "time": value = timestamp_to_old_pamhyr_date(int(v)) else: - value = str(datetime.fromtimestamp(v)) + value = str( + self._start_date + timedelta(seconds=float(v)) + ) else: value = f"{v}" @@ -93,6 +95,10 @@ class TableModel(PamhyrTableModel): column = index.column() try: + if (self._data.header[column] == "time" + and self._opt_data == "date"): + value = self.date_to_elapsed_seconds(value) + self._undo.push( SetDataCommand( self._data, row, column, value diff --git a/src/View/LateralContribution/Edit/Window.py b/src/View/LateralContribution/Edit/Window.py index 6b01d249..62329aef 100644 --- a/src/View/LateralContribution/Edit/Window.py +++ b/src/View/LateralContribution/Edit/Window.py @@ -109,7 +109,8 @@ class EditLateralContributionWindow(PamhyrWindow): data=self._data, undo=self._undo_stack, trad=self._trad, - opt_data=self._study.time_system + opt_data=self._study.time_system, + start_date=self._study.date ) table.setModel(self._table) diff --git a/src/View/LateralContributionsAdisTS/Edit/Table.py b/src/View/LateralContributionsAdisTS/Edit/Table.py index 5975c875..51d2540b 100644 --- a/src/View/LateralContributionsAdisTS/Edit/Table.py +++ b/src/View/LateralContributionsAdisTS/Edit/Table.py @@ -90,7 +90,9 @@ class TableModel(PamhyrTableModel): if self._opt_data == "time": value = timestamp_to_old_pamhyr_date_adists(int(v)) else: - value = str(datetime.fromtimestamp(v)) + value = str( + self._start_date + timedelta(seconds=float(v)) + ) else: value = f"{v}" @@ -107,6 +109,10 @@ class TableModel(PamhyrTableModel): column = index.column() try: + if (self._data.header[column] == "time" + and self._opt_data == "date"): + value = self.date_to_elapsed_seconds(value) + row = self.get_true_data_row(row) self._undo.push( diff --git a/src/View/LateralContributionsAdisTS/Edit/Window.py b/src/View/LateralContributionsAdisTS/Edit/Window.py index 174cc644..f828dc8c 100644 --- a/src/View/LateralContributionsAdisTS/Edit/Window.py +++ b/src/View/LateralContributionsAdisTS/Edit/Window.py @@ -124,7 +124,8 @@ class EditLateralContributionAdisTSWindow(PamhyrWindow): data=self._data, undo=self._undo_stack, trad=self._trad, - opt_data=self._study.time_system + opt_data=self._study.time_system, + start_date=self._study.date ) table.setModel(self._table) diff --git a/src/View/Tools/PamhyrTable.py b/src/View/Tools/PamhyrTable.py index dc8920fb..9748a13d 100644 --- a/src/View/Tools/PamhyrTable.py +++ b/src/View/Tools/PamhyrTable.py @@ -19,6 +19,8 @@ import logging import traceback +from datetime import datetime + from tools import trace, timer from Model.Except import NotImplementedMethodeError @@ -59,6 +61,14 @@ class PamhyrTextDelegate(QStyledItemDelegate): class PamhyrTableModel(QAbstractTableModel): + def date_to_elapsed_seconds(self, value): + """Convert an entered date to seconds since the study start date.""" + entered_date = value + if not isinstance(entered_date, datetime): + entered_date = datetime.fromisoformat(str(value).strip()) + + return (entered_date - self._start_date).total_seconds() + def _setup_delegates(self): if self._table_view is None: return @@ -85,7 +95,8 @@ class PamhyrTableModel(QAbstractTableModel): undo=None, opt_data=None, options=["rows_selection"], - parent=None): + parent=None, + start_date=None): super(PamhyrTableModel, self).__init__() self._table_view = table_view @@ -102,6 +113,7 @@ class PamhyrTableModel(QAbstractTableModel): self._options = options self._undo = undo self._lst = [] + self._start_date = start_date self._setup_delegates() self._setup_lst() diff --git a/src/View/WeatherParameters/Edit/Table.py b/src/View/WeatherParameters/Edit/Table.py index 996ef4b7..59191551 100644 --- a/src/View/WeatherParameters/Edit/Table.py +++ b/src/View/WeatherParameters/Edit/Table.py @@ -77,7 +77,9 @@ class TableModel(PamhyrTableModel): if self._opt_data == "time": value = timestamp_to_old_pamhyr_date(int(v)) else: - value = str(datetime.fromtimestamp(v)) + value = str( + self._start_date + timedelta(seconds=float(v)) + ) else: value = f"{v}" @@ -94,6 +96,10 @@ class TableModel(PamhyrTableModel): column = index.column() try: + if (self._data.header[column] == "time" + and self._opt_data == "date"): + value = self.date_to_elapsed_seconds(value) + self._undo.push( SetDataCommand( self._data, row, column, value diff --git a/src/View/WeatherParameters/Edit/Window.py b/src/View/WeatherParameters/Edit/Window.py index 2e2bb878..d20beaab 100644 --- a/src/View/WeatherParameters/Edit/Window.py +++ b/src/View/WeatherParameters/Edit/Window.py @@ -121,7 +121,8 @@ class EditWeatherParametersWindow(PamhyrWindow): data=self._data, undo=self._undo_stack, trad=self._trad, - opt_data=self._study.time_system + opt_data=self._study.time_system, + start_date=self._study.date ) table.setModel(self._table) From 0f1d1b07973e94e893b51af38845317a757b60c9 Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Mon, 31 Aug 2026 16:58:36 +0200 Subject: [PATCH 6/6] Time/Date: accept different formats for date, YYYY-MM-DD [HH:MM[:SS]], DD-MM-YYYY [HH:MM[:SS]] and DD/MM/YYYY [HH:MM[:SS]], convert it to classic one used by pamhyr, and prevent user when he's using a wrong one --- src/View/BoundaryCondition/Edit/Table.py | 4 +- .../BoundaryConditionsAdisTS/Edit/Table.py | 4 +- .../Edit/Table.py | 4 +- src/View/LateralContribution/Edit/Table.py | 4 +- .../LateralContributionsAdisTS/Edit/Table.py | 4 +- src/View/Tools/PamhyrTable.py | 26 ++++++--- src/View/WeatherParameters/Edit/Table.py | 4 +- src/tools.py | 53 ++++++++++++++++++- 8 files changed, 84 insertions(+), 19 deletions(-) diff --git a/src/View/BoundaryCondition/Edit/Table.py b/src/View/BoundaryCondition/Edit/Table.py index d36f56f5..d4e981df 100644 --- a/src/View/BoundaryCondition/Edit/Table.py +++ b/src/View/BoundaryCondition/Edit/Table.py @@ -24,7 +24,7 @@ from datetime import date, time, datetime, timedelta from tools import ( trace, timer, timestamp_to_old_pamhyr_date, - old_pamhyr_date_to_timestamp + old_pamhyr_date_to_timestamp, format_datetime ) from View.Tools.PamhyrTable import PamhyrTableModel @@ -97,7 +97,7 @@ class TableModel(PamhyrTableModel): if self._opt_data == "time": value = timestamp_to_old_pamhyr_date(int(v)) else: - value = str( + value = format_datetime( self._start_date + timedelta(seconds=float(v)) ) else: diff --git a/src/View/BoundaryConditionsAdisTS/Edit/Table.py b/src/View/BoundaryConditionsAdisTS/Edit/Table.py index 01179d92..e6c714d8 100644 --- a/src/View/BoundaryConditionsAdisTS/Edit/Table.py +++ b/src/View/BoundaryConditionsAdisTS/Edit/Table.py @@ -24,7 +24,7 @@ from datetime import date, time, datetime, timedelta from tools import ( trace, timer, timestamp_to_old_pamhyr_date, - old_pamhyr_date_to_timestamp + old_pamhyr_date_to_timestamp, format_datetime ) from View.Tools.PamhyrTable import PamhyrTableModel @@ -87,7 +87,7 @@ class TableModel(PamhyrTableModel): if self._opt_data == "time": value = timestamp_to_old_pamhyr_date(int(v)) else: - value = str( + value = format_datetime( self._start_date + timedelta(seconds=float(v)) ) else: diff --git a/src/View/BoundaryConditionsTemperature/Edit/Table.py b/src/View/BoundaryConditionsTemperature/Edit/Table.py index a29937b3..196d68d7 100644 --- a/src/View/BoundaryConditionsTemperature/Edit/Table.py +++ b/src/View/BoundaryConditionsTemperature/Edit/Table.py @@ -24,7 +24,7 @@ from datetime import date, time, datetime, timedelta from tools import ( trace, timer, timestamp_to_old_pamhyr_date, - old_pamhyr_date_to_timestamp + old_pamhyr_date_to_timestamp, format_datetime ) from View.Tools.PamhyrTable import PamhyrTableModel @@ -87,7 +87,7 @@ class TableModel(PamhyrTableModel): if self._opt_data == "time": value = timestamp_to_old_pamhyr_date(int(v)) else: - value = str( + value = format_datetime( self._start_date + timedelta(seconds=float(v)) ) else: diff --git a/src/View/LateralContribution/Edit/Table.py b/src/View/LateralContribution/Edit/Table.py index 330d2092..289b0265 100644 --- a/src/View/LateralContribution/Edit/Table.py +++ b/src/View/LateralContribution/Edit/Table.py @@ -23,7 +23,7 @@ from datetime import date, time, datetime, timedelta from tools import ( trace, timer, timestamp_to_old_pamhyr_date, - old_pamhyr_date_to_timestamp + old_pamhyr_date_to_timestamp, format_datetime ) from View.Tools.PamhyrTable import PamhyrTableModel @@ -76,7 +76,7 @@ class TableModel(PamhyrTableModel): if self._opt_data == "time": value = timestamp_to_old_pamhyr_date(int(v)) else: - value = str( + value = format_datetime( self._start_date + timedelta(seconds=float(v)) ) else: diff --git a/src/View/LateralContributionsAdisTS/Edit/Table.py b/src/View/LateralContributionsAdisTS/Edit/Table.py index 51d2540b..eacd21b1 100644 --- a/src/View/LateralContributionsAdisTS/Edit/Table.py +++ b/src/View/LateralContributionsAdisTS/Edit/Table.py @@ -24,7 +24,7 @@ from tools import ( trace, timer, timestamp_to_old_pamhyr_date, old_pamhyr_date_to_timestamp, - timestamp_to_old_pamhyr_date_adists + timestamp_to_old_pamhyr_date_adists, format_datetime ) from View.Tools.PamhyrTable import PamhyrTableModel @@ -90,7 +90,7 @@ class TableModel(PamhyrTableModel): if self._opt_data == "time": value = timestamp_to_old_pamhyr_date_adists(int(v)) else: - value = str( + value = format_datetime( self._start_date + timedelta(seconds=float(v)) ) else: diff --git a/src/View/Tools/PamhyrTable.py b/src/View/Tools/PamhyrTable.py index 9748a13d..a3196f97 100644 --- a/src/View/Tools/PamhyrTable.py +++ b/src/View/Tools/PamhyrTable.py @@ -19,9 +19,7 @@ import logging import traceback -from datetime import datetime - -from tools import trace, timer +from tools import trace, timer, parse_datetime from Model.Except import NotImplementedMethodeError @@ -36,8 +34,11 @@ from PyQt5.QtWidgets import ( QFileDialog, QTableView, QAbstractItemView, QUndoStack, QShortcut, QAction, QItemDelegate, QComboBox, QStyledItemDelegate, QHeaderView, + QMessageBox, ) +_translate = QCoreApplication.translate + logger = logging.getLogger() @@ -63,9 +64,22 @@ class PamhyrTextDelegate(QStyledItemDelegate): class PamhyrTableModel(QAbstractTableModel): def date_to_elapsed_seconds(self, value): """Convert an entered date to seconds since the study start date.""" - entered_date = value - if not isinstance(entered_date, datetime): - entered_date = datetime.fromisoformat(str(value).strip()) + try: + entered_date = parse_datetime(value) + except ValueError as error: + QMessageBox.warning( + self._table_view, + _translate("PamhyrTableModel", "Invalid date/time"), + _translate( + "PamhyrTableModel", + "The entered date/time is invalid.\n" + "Expected formats:\n" + "YYYY-MM-DD [HH:MM[:SS]],\n" + "DD-MM-YYYY [HH:MM[:SS]] or\n" + "DD/MM/YYYY [HH:MM[:SS]]." + ) + ) + raise error return (entered_date - self._start_date).total_seconds() diff --git a/src/View/WeatherParameters/Edit/Table.py b/src/View/WeatherParameters/Edit/Table.py index 59191551..163dcfe4 100644 --- a/src/View/WeatherParameters/Edit/Table.py +++ b/src/View/WeatherParameters/Edit/Table.py @@ -23,7 +23,7 @@ from datetime import date, time, datetime, timedelta from tools import ( trace, timer, timestamp_to_old_pamhyr_date, - old_pamhyr_date_to_timestamp + old_pamhyr_date_to_timestamp, format_datetime ) from View.Tools.PamhyrTable import PamhyrTableModel @@ -77,7 +77,7 @@ class TableModel(PamhyrTableModel): if self._opt_data == "time": value = timestamp_to_old_pamhyr_date(int(v)) else: - value = str( + value = format_datetime( self._start_date + timedelta(seconds=float(v)) ) else: diff --git a/src/tools.py b/src/tools.py index 35f806fd..6422f808 100644 --- a/src/tools.py +++ b/src/tools.py @@ -22,7 +22,7 @@ import logging import sqlite3 import traceback -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from pathlib import Path from colorama import Fore @@ -239,6 +239,57 @@ def timestamp(dt: datetime): return dt.timestamp() +SUPPORTED_DATETIME_FORMATS = ( + "%d-%m-%Y %H:%M:%S.%f", + "%d-%m-%Y %H:%M:%S", + "%d-%m-%Y %H:%M", + "%d-%m-%Y", + "%d/%m/%Y %H:%M:%S.%f", + "%d/%m/%Y %H:%M:%S", + "%d/%m/%Y %H:%M", + "%d/%m/%Y", + "%d-%m-%y %H:%M:%S", + "%d/%m/%y %H:%M:%S", +) + + +def parse_datetime(value): + """Parse a supported date representation into a naive UTC datetime.""" + if isinstance(value, datetime): + parsed = value + else: + text = str(value).strip() + if text.endswith(("Z", "z")): + text = text[:-1] + "+00:00" + + try: + parsed = datetime.fromisoformat(text) + except ValueError as iso_error: + for date_format in SUPPORTED_DATETIME_FORMATS: + try: + parsed = datetime.strptime(text, date_format) + break + except ValueError: + continue + else: + raise ValueError( + f"Unsupported date format: {value!r}. Expected an ISO " + "date/time or one of: " + + ", ".join(SUPPORTED_DATETIME_FORMATS) + ) from iso_error + + # If the parsed datetime has timezone information, convert it to UTC and remove the timezone info + if parsed.tzinfo is not None: + parsed = parsed.astimezone(timezone.utc).replace(tzinfo=None) + + return parsed + + +def format_datetime(value): + """Return a datetime using the canonical application format.""" + return parse_datetime(value).strftime("%Y-%m-%d %H:%M:%S") + + def date_iso_to_timestamp(date: str): if type(date) is str: return timestamp(datetime.fromisoformat(date))