Source code for koa_middleware.utils
from datetime import datetime, timezone, timedelta
import re
import os
import hashlib
_uuid_regex = re.compile(
r'^[a-f0-9]{8}-'
r'[a-f0-9]{4}-'
r'4[a-f0-9]{3}-'
r'[89ab][a-f0-9]{3}-'
r'[a-f0-9]{12}\Z',
re.I
)
[docs]
def is_valid_uuid(value: str) -> bool:
"""
Checks if a given string is a valid UUID v4.
Parameters
----------
value : str
The string to check.
Returns
-------
bool
True if the string is a valid UUID v4, False otherwise.
"""
return bool(_uuid_regex.match(value))
[docs]
def get_env_var_bool(name : str, default : bool | None = None) -> bool | None:
"""
Return the boolean value of an environment variable.
Parameters
----------
name : str
The name of the environment variable.
default : bool | None, optional
The default value to return if the environment variable is not set. Default is None.
Returns
-------
bool | None
The boolean value of the environment variable, or the default if not set.
"""
val = os.environ.get(name)
if val is None:
return default
return val.lower() in {"1", "true", "yes", "on"}
[docs]
def generate_md5_file(filepath: str) -> str:
"""
Generate the MD5 checksum of a FITS file.
Parameters
----------
filepath : str
The path to the file for which to compute the MD5 checksum.
Returns
-------
str
The MD5 checksum of the file.
"""
chunk_size = 1024 * 1024
h = hashlib.md5()
with open(filepath, "rb") as f:
for chunk in iter(lambda: f.read(chunk_size), b""):
h.update(chunk)
return h.hexdigest()
[docs]
def get_koa_id_timestamp_from_datetime(dt : str):
"""
Get the KOA ID from a datetime string.
Parameters
----------
dt : str
The datetime string in ISO format.
Returns
-------
koa_id : str
The KOA ID timestamp in the format 'YYYYMMDD.SSSSS.ss'.
"""
utc = datetime.strptime(dt, '%Y-%m-%dT%H:%M:%S.%f')
total_seconds = utc.hour * 3600 + utc.minute * 60 + utc.second + utc.microsecond / 1e6
seconds = f"{total_seconds:08.2f}"
date = utc.strftime('%Y%m%d')
return f"{date}.{seconds}"
[docs]
def generate_koa_filehandle(
instrument_name : str,
datetime_obs : str,
koa_id : str
) -> str:
"""
Generate a KOA filehandle.
Format:
``/{instrument_name}/YYYY/YYMMDD/{koa_id}``
where:
- ``instrument_name`` is the instrument name.
- ``YYYY`` is the 4-digit year of the observation.
- ``YYMMDD`` is the date of the observation in year-month-day format.
- ``koa_id`` is the KOA ID (same as the filename for HISPEC and PARVI).
Parameters
----------
instrument_name : str
The instrument name.
datetime_obs : str
The observation date in ISO format.
koa_id : str
The KOA ID.
Returns
-------
str
The KOA filehandle.
"""
year = datetime_obs[:4]
ymd = datetime_obs[:10].replace('-', '')
koa_filehandle = f"/{instrument_name}/{year}/{ymd}/{koa_id}"
return koa_filehandle
[docs]
def postgres_http_date_to_iso(date_str: str) -> str:
"""
Return datetime as:
YYYY-MM-DDTHH:MM:SS.SSS
Parameters
----------
date_str : str
The input date string, which can be in one of the following formats:
- ISO 8601 strings
- Postgres HTTP-date strings like:
'Thu, 12 Feb 2026 00:00:00 GMT'
Returns
-------
str
The datetime string in ISO format.
"""
# Try ISO first
try:
dt = datetime.fromisoformat(date_str.replace("Z", "+00:00"))
except ValueError:
# Try Postgres HTTP-date
try:
dt = datetime.strptime(
date_str,
"%a, %d %b %Y %H:%M:%S GMT"
).replace(tzinfo=timezone.utc)
except ValueError:
raise ValueError(f"Invalid datetime string: {date_str}")
# Convert to UTC if tz-aware
if dt.tzinfo is not None:
dt = dt.astimezone(timezone.utc)
# Return exactly millisecond precision, no timezone
return dt.strftime("%Y-%m-%dT%H:%M:%S.") + f"{dt.microsecond // 1000:03d}"
_MJD_UNIX_OFFSET = 40587.0
_SECONDS_PER_DAY = 86400.0
[docs]
def isot_to_mjd(isot : str) -> float:
"""
Convert an ISO 8601 datetime string to Modified Julian Date (MJD).
Parameters
----------
isot : str
The input datetime string in ISO 8601 format.
Returns
-------
float
The corresponding Modified Julian Date (MJD).
"""
dt = datetime.fromisoformat(isot)
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
else:
dt = dt.astimezone(timezone.utc)
return dt.timestamp() / _SECONDS_PER_DAY + _MJD_UNIX_OFFSET
[docs]
def mjd_to_isot_ms(mjd : float) -> str:
"""
Convert Modified Julian Date (MJD) to an ISO 8601 datetime string.
Parameters
----------
mjd : float
The input Modified Julian Date (MJD).
Returns
-------
str
The corresponding datetime string in ISO 8601 format.
"""
# Convert MJD -> seconds since Unix epoch, then format
seconds = (mjd - _MJD_UNIX_OFFSET) * _SECONDS_PER_DAY
dt = datetime(1970, 1, 1, tzinfo=timezone.utc) + timedelta(seconds=seconds)
return dt.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3]
[docs]
def datetime_to_isot_ms(dt : datetime) -> str:
"""
Convert a datetime object to an ISO 8601 string with millisecond precision.
Parameters
----------
dt : datetime
The input datetime object.
Returns
-------
str
The corresponding datetime string in ISO 8601 format with millisecond precision.
"""
return dt.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3]