From 4a1fd09308734a3b7d7b71160f1757b128e36ecd Mon Sep 17 00:00:00 2001
From: Andrej Prsa <aprsa09@gmail.com>
Date: Thu, 7 Nov 2024 11:37:51 -0500
Subject: [PATCH 1/3] Improves handling of passband history FITS file format
 provides a HISTORY keyword that was not used correctly in previous versions.
 This commit fixes that and improves the overall code. WIP!

---
 phoebe/atmospheres/passbands.py | 132 +++++++++++++++++++++++---------
 1 file changed, 97 insertions(+), 35 deletions(-)

diff --git a/phoebe/atmospheres/passbands.py b/phoebe/atmospheres/passbands.py
index eac4faef6..4a870b073 100644
--- a/phoebe/atmospheres/passbands.py
+++ b/phoebe/atmospheres/passbands.py
@@ -2,7 +2,6 @@
 from phoebe import conf, mpi
 from phoebe.utils import _bytes
 from tqdm import tqdm
-from itertools import product
 
 import ndpolator
 
@@ -17,14 +16,13 @@
 import numpy as np
 from scipy import interpolate, integrate
 from scipy.optimize import curve_fit as cfit
-from datetime import datetime
+from datetime import datetime as dt
 import libphoebe
 import os
 import sys
 import glob
 import shutil
 import json
-import time
 import re
 
 # NOTE: python3 only
@@ -38,7 +36,7 @@
 
 # define the URL to query for online passbands.  See tables.phoebe-project.org
 # repo for the source-code of the server
-_url_tables_server = 'http://tables.phoebe-project.org'
+_url_tables_server = 'https://tables.phoebe-project.org'
 # comment out the following line if testing tables.phoebe-project.org server locally:
 # _url_tables_server = 'http://localhost:5555'
 
@@ -305,11 +303,22 @@ def log(self):
 
     def add_to_history(self, history, max_length=46):
         """
-        Adds a history entry to the passband file header.
+        Adds a history entry to the Passband instance.
 
         Parameters
         ----------
-        * `comment` (string, required): comment to be added to the passband header.
+        * `history` (string, required): comment to be added to the passband header.
+        * `max_length` (int, optional, default=46): maximum length of the history entry.
+        
+        Raises
+        ------
+        * ValueError: if the history entry is not a string.
+        * ValueError: if the history entry exceeds the maximum length.
+        
+        Examples
+        --------
+        >>> pb = phoebe.get_passband('Johnson:V')
+        >>> pb.add_to_history('passband updated.')
         """
 
         if not isinstance(history, str):
@@ -317,21 +326,34 @@ def add_to_history(self, history, max_length=46):
         if len(history) > max_length:
             raise ValueError(f'comment length should not exceed {max_length} characters.')
 
-        self.history.append(f'{time.ctime()}: {history}')
+        self.history.append(f'{_generate_timestamp()}: {history}')
 
-    def add_comment(self, comment):
+    def add_comment(self, comment, max_length=46):
         """
-        Adds a comment to the passband file header.
+        Adds a comment entry to the Passbands instance.
 
         Parameters
         ----------
         * `comment` (string, required): comment to be added to the passband header.
+        * `max_length` (int, optional, default=46): maximum length of the comment entry.
+        
+        Raises
+        ------
+        * ValueError: if the comment is not a string.
+        * ValueError: if the comment exceeds the maximum length.
+        
+        Examples
+        --------
+        >>> pb = phoebe.get_passband('Johnson:V')
+        >>> pb.add_comment('this is a comment.')
         """
 
         if not isinstance(comment, str):
             raise ValueError('passband header comments must be strings.')
+        if len(comment) > max_length:
+            raise ValueError(f'comment length should not exceed {max_length} characters.')
 
-        self.comments.append(comment)
+        self.comments.append(comment)                
 
     def on_updated_ptf(self, ptf, wlunits=u.AA, oversampling=1, ptf_order=3):
         """
@@ -380,7 +402,7 @@ def save(self, archive, overwrite=True, update_timestamp=True, export_inorm_tabl
         """
 
         # Timestamp is used for passband versioning.
-        timestamp = time.ctime() if update_timestamp else self.timestamp
+        timestamp = _generate_timestamp() if update_timestamp else self.timestamp
 
         header = fits.Header()
         header['PHOEBEVN'] = phoebe_version
@@ -1094,13 +1116,12 @@ def export_legacy_ldcoeffs(self, models, atm='ck2004', filename=None, intens_wei
         grid = self.ndp[atm].table['ld@photon'][1] if intens_weighting == 'photon' else self.ndp[atm].table['ld@energy'][1]
 
         if filename is not None:
-            import time
             f = open(filename, 'w')
-            f.write('# PASS_SET  %s\n' % self.pbset)
-            f.write('# PASSBAND  %s\n' % self.pbname)
+            f.write(f'# PASS_SET  {self.pbset}\n')
+            f.write(f'# PASSBAND  {self.pbname}\n')
             f.write('# VERSION   1.0\n\n')
-            f.write('# Exported from PHOEBE-2 passband on %s\n' % (time.ctime()))
-            f.write('# The coefficients are computed for the %s-weighted regime from %s atmospheres.\n\n' % (intens_weighting, atm))
+            f.write(f'# Exported from PHOEBE-2 passband on {_generate_timestamp()}\n')
+            f.write(f'# The coefficients are computed for the {intens_weighting}-weighted regime from {atm} atmospheres.\n\n')
 
         mods = np.loadtxt(models)
         for mod in mods:
@@ -1885,12 +1906,51 @@ def bindex(self, teffs=5772., loggs=4.43, abuns=0.0, mus=1.0, atm='ck2004', inte
             raise ValueError('Atmosphere parameters out of bounds: Teff=%s, logg=%s, abun=%s' % (Teff[nanmask], logg[nanmask], abun[nanmask]))
         return retval
 
+
+def _generate_timestamp():
+    """
+    Generates a timestamp string in the format 'YYYY-MM-DD HH:MM:SS'.
+
+    Returns
+    ----------
+    * (string) timestamp string
+    """
+
+    return dt.now().strftime("%Y-%m-%d %H:%M:%S")
+
+
 def _timestamp_to_dt(timestamp):
+    """
+    Converts a timestamp string to a datetime object.
+
+    Arguments
+    ----------
+    * `timestamp` (string): timestamp string in the format 'YYYY-MM-DD HH:MM:SS' or 'Mon Jan 01 00:00:00 2000'
+
+    Raises
+    ----------
+    * TypeError: if timestamp is not of type string
+    * ValueError: if timestamp is not in the correct format
+
+    Returns
+    ----------
+    * (datetime) datetime object
+    """
+
     if timestamp is None:
         return None
     elif not isinstance(timestamp, str):
         raise TypeError("timestamp not of type string")
-    return datetime.strptime(timestamp, "%a %b %d %H:%M:%S %Y")
+    try:
+        # if timestamp is in the new format, 'YYYY-MM-DD HH:MM:SS':
+        ts = dt.strptime(timestamp, "%Y-%m-%d %H:%M:%S")
+    except ValueError:
+        # if timestamp is in the old format, 'Mon Jan 01 00:00:00 2000':
+        ts = dt.strptime(timestamp, "%a %b %d %H:%M:%S %Y")
+    else:
+        raise ValueError("timestamp not in the correct format")
+    
+    return ts
 
 def _init_passband(fullpath, check_for_update=True):
     """
@@ -2177,34 +2237,36 @@ def list_passband_online_history(passband, since_installed=True):
     ----------
     * (dict): dictionary with timestamps as keys and messages and values.
     """
+
     if passband not in list_online_passbands(repeat_errors=False):
-        raise ValueError("'{}' passband not available online".format(passband))
+        raise ValueError(f'{passband} passband not available online')
 
-    url = '{}/pbs/history/{}?phoebe_version={}'.format(_url_tables_server, passband, phoebe_version)
+    url = f'{_url_tables_server}/pbs/history/{passband}?phoebe_version={phoebe_version}'
 
     try:
         resp = urlopen(url, timeout=3)
     except Exception as err:
-        msg = "connection to online passbands at {} could not be established.  Check your internet connection or try again later.  If the problem persists and you're using a Mac, you may need to update openssl (see http://phoebe-project.org/help/faq).".format(_url_tables_server)
-        msg += " Original error from urlopen: {} {}".format(err.__class__.__name__, str(err))
+        msg = f"connection to online passbands at {_url_tables_server} could not be established.  Check your internet connection or try again later.  If the problem persists and you're using a Mac, you may need to update openssl (see http://phoebe-project.org/help/faq)."
+        msg += f" Original error from urlopen: {err.__class__.__name__} {str(err)}"
 
         logger.warning(msg)
-        return {str(time.ctime()): "could not retrieve history entries"}
-    else:
-        try:
-            all_history = json.loads(resp.read().decode('utf-8'), object_pairs_hook=parse_json).get('passband_history', {}).get(passband, {})
-        except Exception as err:
-            msg = "Parsing response from online passbands at {} failed.".format(_url_tables_server)
-            msg += " Original error from json.loads: {} {}".format(err.__class__.__name__, str(err))
+        return {_generate_timestamp(): "could not retrieve history entries"}
+
+    try:
+        print(resp.read().decode('utf-8'))
+        all_history = json.loads(resp.read().decode('utf-8'), object_pairs_hook=parse_json).get('passband_history', {}).get(passband, {})
+    except Exception as err:
+        msg = f"Parsing response from online passbands at {_url_tables_server} failed."
+        msg += f" Original error from json.loads: {err.__class__.__name__} {str(err)}"
 
-            logger.warning(msg)
-            return {str(time.ctime()): "could not parse history entries"}
+        logger.warning(msg)
+        return {_generate_timestamp(): "could not parse history entries"}
 
-        if since_installed:
-            installed_timestamp = _timestamp_to_dt(_pbtable.get(passband, {}).get('timestamp', None))
-            return {k:v for k,v in all_history.items() if installed_timestamp < _timestamp_to_dt(k)} if installed_timestamp is not None else all_history
-        else:
-            return all_history
+    if since_installed:
+        installed_timestamp = _timestamp_to_dt(_pbtable.get(passband, {}).get('timestamp', None))
+        return {k:v for k,v in all_history.items() if installed_timestamp < _timestamp_to_dt(k)} if installed_timestamp is not None else all_history
+    else:
+        return all_history
 
 def update_passband_available(passband, history_dict=False):
     """

From b87c88c3b267d359151d49871d14ee4f6db083fa Mon Sep 17 00:00:00 2001
From: Andrej Prsa <aprsa09@gmail.com>
Date: Thu, 7 Nov 2024 13:41:02 -0500
Subject: [PATCH 2/3] Adds support for both old (ctime-style) and new
 (iso-style, locale-independent) timestamps.

---
 phoebe/atmospheres/passbands.py | 43 +++++++++++++++++++++++++++------
 1 file changed, 35 insertions(+), 8 deletions(-)

diff --git a/phoebe/atmospheres/passbands.py b/phoebe/atmospheres/passbands.py
index 4a870b073..9ae18b45a 100644
--- a/phoebe/atmospheres/passbands.py
+++ b/phoebe/atmospheres/passbands.py
@@ -36,7 +36,7 @@
 
 # define the URL to query for online passbands.  See tables.phoebe-project.org
 # repo for the source-code of the server
-_url_tables_server = 'https://tables.phoebe-project.org'
+_url_tables_server = 'https://staging.phoebe-project.org'
 # comment out the following line if testing tables.phoebe-project.org server locally:
 # _url_tables_server = 'http://localhost:5555'
 
@@ -1939,18 +1939,17 @@ def _timestamp_to_dt(timestamp):
 
     if timestamp is None:
         return None
-    elif not isinstance(timestamp, str):
+    if not isinstance(timestamp, str):
         raise TypeError("timestamp not of type string")
+
     try:
         # if timestamp is in the new format, 'YYYY-MM-DD HH:MM:SS':
-        ts = dt.strptime(timestamp, "%Y-%m-%d %H:%M:%S")
+        return dt.strptime(timestamp, "%Y-%m-%d %H:%M:%S")
     except ValueError:
         # if timestamp is in the old format, 'Mon Jan 01 00:00:00 2000':
-        ts = dt.strptime(timestamp, "%a %b %d %H:%M:%S %Y")
+        return dt.strptime(timestamp, "%a %b %d %H:%M:%S %Y")
     else:
-        raise ValueError("timestamp not in the correct format")
-    
-    return ts
+        raise ValueError(f'timestamp "{timestamp}" not in the correct format')
 
 def _init_passband(fullpath, check_for_update=True):
     """
@@ -2252,9 +2251,37 @@ def list_passband_online_history(passband, since_installed=True):
         logger.warning(msg)
         return {_generate_timestamp(): "could not retrieve history entries"}
 
+    def _parse(entry):
+        # parses the history entry into a timestamp and message
+        parts = entry.split(':')
+
+        # try the new format first: 'YYYY-MM-DD HH:MM:SS'
+        for i in range(len(parts)-1, 0, -1):
+            try:
+                timestamp = ':'.join(parts[:i])
+                dt.strptime(timestamp, "%Y-%m-%d %H:%M:%S")
+                message = ':'.join(parts[i:]).strip()
+                return timestamp, message
+            except ValueError:
+                continue
+
+        # try the old format: 'Mon Jan 01 00:00:00 2000'
+        for i in range(len(parts)-1, 0, -1):
+            try:
+                timestamp = ':'.join(parts[:i])
+                ts = dt.strptime(timestamp, "%a %b %d %H:%M:%S %Y")
+                # if successful, convert to the new format:
+                timestamp = ts.strftime('%Y-%m-%d %H:%M:%S')
+                message = ':'.join(parts[i:]).strip()
+                return timestamp, message
+            except ValueError:
+                continue
+
     try:
-        print(resp.read().decode('utf-8'))
         all_history = json.loads(resp.read().decode('utf-8'), object_pairs_hook=parse_json).get('passband_history', {}).get(passband, {})
+        # convert all_history to dict with timestamps as keys and messages as values:
+        all_history = dict([_parse(entry) for entry in all_history])
+
     except Exception as err:
         msg = f"Parsing response from online passbands at {_url_tables_server} failed."
         msg += f" Original error from json.loads: {err.__class__.__name__} {str(err)}"

From d45c588ef3e799002b8753e00c8c38e0ab7b1402 Mon Sep 17 00:00:00 2001
From: Andrej Prsa <aprsa09@gmail.com>
Date: Thu, 7 Nov 2024 15:34:32 -0500
Subject: [PATCH 3/3] Removes trailing whitespace

Co-authored-by: Kyle Conroy <kyleconroy@gmail.com>
---
 phoebe/atmospheres/passbands.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/phoebe/atmospheres/passbands.py b/phoebe/atmospheres/passbands.py
index 9ae18b45a..d92d65e1f 100644
--- a/phoebe/atmospheres/passbands.py
+++ b/phoebe/atmospheres/passbands.py
@@ -353,7 +353,7 @@ def add_comment(self, comment, max_length=46):
         if len(comment) > max_length:
             raise ValueError(f'comment length should not exceed {max_length} characters.')
 
-        self.comments.append(comment)                
+        self.comments.append(comment)
 
     def on_updated_ptf(self, ptf, wlunits=u.AA, oversampling=1, ptf_order=3):
         """