Skip to content

Commit 5190b29

Browse files
authored
Merge pull request #110 from lsst-sqre/tickets/DM-51345
DM-51345: Update DayObs parameter formats
2 parents d25089d + 70a92e1 commit 5190b29

File tree

13 files changed

+714
-234
lines changed

13 files changed

+714
-234
lines changed

.github/workflows/ci.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ env:
44
# Current supported uv version. The uv documentation recommends pinning
55
# this. The version should match the version used in .pre-commit-config.yaml
66
# and frozen in uv.lock.
7-
UV_VERSION: "0.7.7"
7+
UV_VERSION: "0.7.12"
88

99
"on":
1010
merge_group: {}

.github/workflows/periodic-ci.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ env:
99
# Current supported uv version. The uv documentation recommends pinning
1010
# this. The version should match the version used in .pre-commit-config.yaml
1111
# and frozen in uv.lock.
12-
UV_VERSION: "0.7.7"
12+
UV_VERSION: "0.7.12"
1313

1414
"on":
1515
schedule:

.pre-commit-config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ repos:
77
- id: trailing-whitespace
88

99
- repo: https://github.com/astral-sh/uv-pre-commit
10-
rev: 0.7.7
10+
rev: 0.7.12
1111
hooks:
1212
- id: uv-lock
1313

1414
- repo: https://github.com/astral-sh/ruff-pre-commit
15-
rev: v0.11.11
15+
rev: v0.11.13
1616
hooks:
1717
- id: ruff-check
1818
args: [--fix, --exit-non-zero-on-fix]

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,20 @@ Collect fragments into this file with: scriv collect --version X.Y.Z
88

99
<!-- scriv-insert-here -->
1010

11+
<a id='changelog-0.23.0'></a>
12+
13+
## 0.23.0 (2025-06-12)
14+
15+
### Backwards-incompatible changes
16+
17+
- The `dayobs` parameter format now creates an int value in the notebook rather than a string. For example, `mydate = 20250115` instead of `mydate = '20250115'`. This is a better match for the DayObs metadata standard described in [SITCOMTN-032](https://sitcomtn-032.lsst.io).
18+
19+
Any early adopters of the `dayobs` format will need to update their notebooks. Also consider using the new `dayobs-date` format instead.
20+
21+
### New features
22+
23+
- Added a new `dayobs-date` parameter format. This is similar to the original `dayobs` format in the use of the UTC-12 timezone, but expects dates in the `YYYY-MM-DD` format and assigns the parameters as `datetime.date` values in the notebook. Although this is not the standard usage of `dayobs`, it should be useful for many applications because the ISO 8601 string format is more readable and the `datetime.date` type is more convenient to use in Python code. `dayobs-date` works with dynamic defaults like `dayobs` and standard `date` formats.
24+
1125
<a id='changelog-0.22.0'></a>
1226

1327
## 0.22.0 (2025-06-05)

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
2323
FROM base-image AS install-image
2424

2525
# Install uv.
26-
COPY --from=ghcr.io/astral-sh/uv:0.7.7 /uv /bin/uv
26+
COPY --from=ghcr.io/astral-sh/uv:0.7.12 /uv /bin/uv
2727

2828
# Install some additional packages required for building dependencies.
2929
COPY scripts/install-dependency-packages.sh .

src/timessquare/domain/pageparameters/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66
from ._datedynamicdefault import DYNAMIC_DATE_PATTERN, DateDynamicDefault
77
from ._dateparameter import DateParameterSchema
88
from ._datetimeparameter import DatetimeParameterSchema
9+
from ._dayobsdateparameter import DayObsDateParameterSchema
10+
from ._dayobsparameter import DayObsParameterSchema
911
from ._integerparameter import IntegerParameterSchema
1012
from ._numberparameter import NumberParameterSchema
11-
from ._obsdateparameter import ObsDateParameterSchema
1213
from ._pageparameters import (
1314
PageParameters,
1415
create_and_validate_parameter_schema,
@@ -23,9 +24,10 @@
2324
"DateDynamicDefault",
2425
"DateParameterSchema",
2526
"DatetimeParameterSchema",
27+
"DayObsDateParameterSchema",
28+
"DayObsParameterSchema",
2629
"IntegerParameterSchema",
2730
"NumberParameterSchema",
28-
"ObsDateParameterSchema",
2931
"PageParameterSchema",
3032
"PageParameters",
3133
"StringParameterSchema",
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
from __future__ import annotations
2+
3+
import re
4+
from datetime import date, datetime, timedelta, timezone
5+
from typing import Any
6+
7+
from timessquare.exceptions import PageParameterValueCastingError
8+
9+
from ._datedynamicdefault import DateDynamicDefault
10+
from ._schemabase import PageParameterSchema
11+
12+
__all__ = ["DayObsDateParameterSchema"]
13+
14+
15+
class DayObsDateParameterSchema(PageParameterSchema):
16+
"""A parameter schema for the custom Rubin DayObs date format with dashes.
17+
18+
DayObsDate is defined as a date in the UTC-12 timezone with the string
19+
representation formatted as YYYY-MM-DD. Times Square parameters validate
20+
dayobs-date parameters as strings, but the Python assignment returns a
21+
datetime.date instance.
22+
"""
23+
24+
tz = timezone(-timedelta(hours=12))
25+
"""The timezone for DayObs parameters, UTC-12."""
26+
27+
@property
28+
def strict_schema(self) -> dict[str, Any]:
29+
"""Get the JSON schema without the custom format."""
30+
schema = super().strict_schema
31+
# Add a basic regex pattern to validate the YYYY-MM-DD format
32+
schema["pattern"] = r"^\d{4}-\d{2}-\d{2}$"
33+
return schema
34+
35+
def validate_default(self) -> bool:
36+
"""Validate the default value for the parameter.
37+
38+
Returns
39+
-------
40+
bool
41+
True if the default is valid, False otherwise.
42+
"""
43+
if "X-Dynamic-Default" in self.schema:
44+
try:
45+
DateDynamicDefault(self.schema["X-Dynamic-Default"])
46+
except ValueError:
47+
return False
48+
else:
49+
return True
50+
elif "default" in self.schema:
51+
try:
52+
self.cast_value(self.schema["default"])
53+
except PageParameterValueCastingError:
54+
return False
55+
else:
56+
return True
57+
else:
58+
return False
59+
60+
def cast_value(self, v: Any) -> date:
61+
"""Cast a value to its Python type."""
62+
try:
63+
# Parse a YYYY-MM-DD string, date object, or datetime object
64+
if isinstance(v, str):
65+
return self._cast_string(v)
66+
elif isinstance(v, datetime):
67+
# Check datetime before date because of inheritance
68+
return self._cast_datetime(v)
69+
elif isinstance(v, date):
70+
return self._cast_date(v)
71+
else:
72+
raise PageParameterValueCastingError.for_value(v, "date")
73+
except Exception as e:
74+
raise PageParameterValueCastingError.for_value(v, "date") from e
75+
76+
def _cast_string(self, v: str) -> date:
77+
"""Cast a string value to the date format."""
78+
match = re.match(r"^\d{4}-\d{2}-\d{2}$", v)
79+
if not match:
80+
raise ValueError(f"Invalid YYYY-MM-DD format: {v}")
81+
82+
year = int(v[:4])
83+
month = int(v[5:7])
84+
day = int(v[8:10])
85+
return date(year, month, day)
86+
87+
def _cast_date(self, v: date) -> date:
88+
"""Cast a date object directly (no conversion needed)."""
89+
return v
90+
91+
def _cast_datetime(self, v: datetime) -> date:
92+
"""Cast a datetime object to the DayObs date format."""
93+
if v.tzinfo is not None:
94+
# Convert to UTC-12 timezone
95+
v = v.astimezone(self.tz)
96+
else:
97+
# If no timezone info, assume it's in UTC-12
98+
v = v.replace(tzinfo=self.tz)
99+
return v.date()
100+
101+
def create_python_imports(self) -> list[str]:
102+
return ["import datetime"]
103+
104+
def create_python_assignment(self, name: str, value: Any) -> str:
105+
date_value = self.cast_value(value)
106+
str_value = date_value.isoformat()
107+
return f'{name} = datetime.date.fromisoformat("{str_value}")'
108+
109+
def create_json_value(self, value: Any) -> str:
110+
return self.cast_value(value).isoformat()
111+
112+
def create_qs_value(self, value: Any) -> str:
113+
return self.cast_value(value).isoformat()
114+
115+
@property
116+
def default(self) -> date:
117+
if "X-Dynamic-Default" in self.schema:
118+
dynamic_default = DateDynamicDefault(
119+
self.schema["X-Dynamic-Default"]
120+
)
121+
return dynamic_default(datetime.now(tz=self.tz).date())
122+
return self.cast_value(self.schema["default"])

src/timessquare/domain/pageparameters/_obsdateparameter.py renamed to src/timessquare/domain/pageparameters/_dayobsparameter.py

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,16 @@
99
from ._datedynamicdefault import DateDynamicDefault
1010
from ._schemabase import PageParameterSchema
1111

12-
__all__ = ["ObsDateParameterSchema"]
12+
__all__ = ["DayObsParameterSchema"]
1313

1414

15-
class ObsDateParameterSchema(PageParameterSchema):
15+
class DayObsParameterSchema(PageParameterSchema):
1616
"""A parameter schema for the custom Rubin DayObs format.
1717
1818
DayObs is defined as the date in the UTC-12 timezone. It's string
19-
representationformatted is YYYYMMDD. Times Square treats DayObs parameters
20-
as strings, although some applicatinos may use them as integers.
19+
representation formatted is YYYYMMDD. Times Square parameters validate
20+
dayobs parameters as int types in notebook code.
21+
integers.
2122
"""
2223

2324
tz = timezone(-timedelta(hours=12))
@@ -56,7 +57,7 @@ def validate_default(self) -> bool:
5657
else:
5758
return False
5859

59-
def cast_value(self, v: Any) -> str:
60+
def cast_value(self, v: Any) -> int:
6061
"""Cast a value to its Python type."""
6162
try:
6263
# Parse a YYYYMMDD string, integer, date object, or datetime object
@@ -74,7 +75,7 @@ def cast_value(self, v: Any) -> str:
7475
except Exception as e:
7576
raise PageParameterValueCastingError.for_value(v, "date") from e
7677

77-
def _cast_string(self, v: str) -> str:
78+
def _cast_string(self, v: str) -> int:
7879
"""Cast a string value to the DayObs format."""
7980
match = re.match(r"^\d{8}$", v)
8081
if not match:
@@ -85,15 +86,15 @@ def _cast_string(self, v: str) -> str:
8586
day = int(v[6:8])
8687
return self._cast_date(date(year, month, day))
8788

88-
def _cast_integer(self, v: int) -> str:
89+
def _cast_integer(self, v: int) -> int:
8990
"""Cast an integer value to the DayObs format."""
9091
return self._cast_string(str(v))
9192

92-
def _cast_date(self, v: date) -> str:
93+
def _cast_date(self, v: date) -> int:
9394
"""Cast a date object to the DayObs format."""
94-
return v.strftime("%Y%m%d")
95+
return int(v.strftime("%Y%m%d"))
9596

96-
def _cast_datetime(self, v: datetime) -> str:
97+
def _cast_datetime(self, v: datetime) -> int:
9798
"""Cast a datetime object to the DayObs format."""
9899
if v.tzinfo is not None:
99100
# Convert to UTC-12 timezone
@@ -104,22 +105,24 @@ def _cast_datetime(self, v: datetime) -> str:
104105
return self._cast_date(v.date())
105106

106107
def create_python_assignment(self, name: str, value: Any) -> str:
107-
date_value = self.cast_value(value)
108-
return f"{name} = {date_value!r}"
108+
int_value = self.cast_value(value)
109+
return f"{name} = {int_value}"
109110

110111
def create_json_value(self, value: Any) -> str:
111-
return self.cast_value(value)
112+
return str(self.cast_value(value))
112113

113114
def create_qs_value(self, value: Any) -> str:
114-
return self.cast_value(value)
115+
return str(self.cast_value(value))
115116

116117
@property
117118
def default(self) -> str:
118119
if "X-Dynamic-Default" in self.schema:
119120
dynamic_default = DateDynamicDefault(
120121
self.schema["X-Dynamic-Default"]
121122
)
122-
return self.cast_value(
123-
dynamic_default(datetime.now(tz=self.tz).date())
123+
return str(
124+
self.cast_value(
125+
dynamic_default(datetime.now(tz=self.tz).date())
126+
)
124127
)
125-
return self.cast_value(self.schema["default"])
128+
return str(self.cast_value(self.schema["default"]))

src/timessquare/domain/pageparameters/_pageparameters.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@
2222
from ._booleanparameter import BooleanParameterSchema
2323
from ._dateparameter import DateParameterSchema
2424
from ._datetimeparameter import DatetimeParameterSchema
25+
from ._dayobsdateparameter import DayObsDateParameterSchema
26+
from ._dayobsparameter import DayObsParameterSchema
2527
from ._integerparameter import IntegerParameterSchema
2628
from ._numberparameter import NumberParameterSchema
27-
from ._obsdateparameter import ObsDateParameterSchema
2829
from ._schemabase import PageParameterSchema
2930
from ._stringparameter import StringParameterSchema
3031

@@ -60,7 +61,9 @@ def create_page_parameter_schema(
6061
elif schema_type == "boolean":
6162
return BooleanParameterSchema(validator=validator)
6263
elif schema_type == "string" and schema_format == "dayobs":
63-
return ObsDateParameterSchema(validator=validator)
64+
return DayObsParameterSchema(validator=validator)
65+
elif schema_type == "string" and schema_format == "dayobs-date":
66+
return DayObsDateParameterSchema(validator=validator)
6467
elif schema_type == "string" and schema_format == "date":
6568
return DateParameterSchema(validator=validator)
6669
elif schema_type == "string" and schema_format == "date-time":

src/timessquare/storage/github/settingsfiles/_parameterschema.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,14 @@ class ParameterSchemaModel(BaseModel):
4646
]
4747

4848
format: Annotated[
49-
Literal["date", "date-time", "dayobs"] | None,
49+
Literal["date", "date-time", "dayobs", "dayobs-date"] | None,
5050
Field(
5151
title="The JSON schema format",
5252
description=(
5353
"For example, the format of a date or time. Only used for "
5454
"the string type. Times Square also supports extensions to "
55-
"format: 'dayobs' for Rubin DayObs dates."
55+
"format: 'dayobs' for Rubin DayObs dates and 'dayobs-date' "
56+
"for Rubin DayObs dates with dashes."
5657
),
5758
),
5859
] = None
@@ -120,6 +121,11 @@ def to_parameter_schema(self, name: str) -> PageParameterSchema:
120121
if "format" in json_schema and json_schema["format"] == "dayobs":
121122
del json_schema["format"]
122123
json_schema["X-TS-Format"] = "dayobs"
124+
elif (
125+
"format" in json_schema and json_schema["format"] == "dayobs-date"
126+
):
127+
del json_schema["format"]
128+
json_schema["X-TS-Format"] = "dayobs-date"
123129
return create_and_validate_parameter_schema(
124130
name=name, json_schema=json_schema
125131
)
@@ -133,10 +139,11 @@ def check_dynamic_default(self) -> Self:
133139
if self.type != JsonSchemaTypeEnum.string or self.format not in {
134140
"date",
135141
"dayobs",
142+
"dayobs-date",
136143
}:
137144
raise ValueError(
138145
"dynamic_default can only be set when type is 'string' "
139-
"and format is 'date' or 'dayobs'. "
146+
"and format is 'date', 'dayobs', or 'dayobs-date'. "
140147
)
141148
return self
142149

@@ -148,7 +155,7 @@ def check_dynamic_default_pattern(self) -> Self:
148155
if (
149156
self.dynamic_default is not None
150157
and self.type == JsonSchemaTypeEnum.string
151-
and self.format in {"date", "dayobs"}
158+
and self.format in {"date", "dayobs", "dayobs-date"}
152159
):
153160
if not DYNAMIC_DATE_PATTERN.match(self.dynamic_default):
154161
raise ValueError(

0 commit comments

Comments
 (0)