Skip to content

Commit ba0707c

Browse files
[uss_qualifier] Establish and use TestTimeContext (#1265)
1 parent 9d22e0f commit ba0707c

File tree

31 files changed

+222
-337
lines changed

31 files changed

+222
-337
lines changed

.basedpyright/baseline.json

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17344,14 +17344,6 @@
1734417344
"endColumn": 98,
1734517345
"lineCount": 1
1734617346
}
17347-
},
17348-
{
17349-
"code": "reportAttributeAccessIssue",
17350-
"range": {
17351-
"startColumn": 104,
17352-
"endColumn": 110,
17353-
"lineCount": 1
17354-
}
1735517347
}
1735617348
],
1735717349
"./monitoring/uss_qualifier/scenarios/astm/utm/dss/report.py": [

monitoring/monitorlib/clients/flight_planning/flight_info_template.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
Volume4DCollection,
1515
Volume4DTemplateCollection,
1616
)
17-
from monitoring.monitorlib.temporal import Time, TimeDuringTest
17+
from monitoring.monitorlib.temporal import TestTimeContext
1818
from monitoring.monitorlib.transformations import Transformation
1919

2020

@@ -30,9 +30,9 @@ class BasicFlightPlanInformationTemplate(ImplicitDict):
3030
area: Volume4DTemplateCollection
3131
"""User intends to or may fly anywhere in this entire area."""
3232

33-
def resolve(self, times: dict[TimeDuringTest, Time]) -> BasicFlightPlanInformation:
33+
def resolve(self, context: TestTimeContext) -> BasicFlightPlanInformation:
3434
kwargs = {k: v for k, v in self.items()}
35-
kwargs["area"] = Volume4DCollection([t.resolve(times) for t in self.area])
35+
kwargs["area"] = Volume4DCollection([t.resolve(context) for t in self.area])
3636
return ImplicitDict.parse(kwargs, BasicFlightPlanInformation)
3737

3838

@@ -53,21 +53,21 @@ class FlightInfoTemplate(ImplicitDict):
5353
transformations: list[Transformation] | None
5454
"""If specified, transform this flight according to these transformations in order (after all templates are resolved)."""
5555

56-
def resolve(self, times: dict[TimeDuringTest, Time]) -> FlightInfo:
56+
def resolve(self, context: TestTimeContext) -> FlightInfo:
5757
kwargs = {k: v for k, v in self.items() if k not in {"transformations"}}
58-
basic_info = self.basic_information.resolve(times)
58+
basic_info = self.basic_information.resolve(context)
5959
if "transformations" in self and self.transformations:
6060
for xform in self.transformations:
6161
basic_info.area = [v.transform(xform) for v in basic_info.area]
6262
kwargs["basic_information"] = basic_info
6363
return ImplicitDict.parse(kwargs, FlightInfo)
6464

6565
def to_scd_inject_request(
66-
self, times: dict[TimeDuringTest, Time]
66+
self, context: TestTimeContext
6767
) -> scd_api.InjectFlightRequest:
6868
"""Render a legacy SCD injection API request object from this object."""
6969

70-
info = self.resolve(times)
70+
info = self.resolve(context)
7171
if "astm_f3548_21" not in info or not info.astm_f3548_21:
7272
raise ValueError(
7373
"Legacy SCD injection API requires astm_f3548_21 operational intent priority to be specified in FlightInfo"

monitoring/monitorlib/geotemporal.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@
1414

1515
from monitoring.monitorlib import geo
1616
from monitoring.monitorlib.geo import Altitude, Circle, LatLngPoint, Polygon, Volume3D
17-
from monitoring.monitorlib.temporal import TestTime, Time, TimeDuringTest
17+
from monitoring.monitorlib.temporal import (
18+
TestTime,
19+
TestTimeContext,
20+
Time,
21+
)
1822
from monitoring.monitorlib.transformations import Transformation
1923

2024

@@ -64,16 +68,16 @@ def resolve_3d(self) -> Volume3D:
6468
return result
6569

6670
def resolve_times(
67-
self, times: dict[TimeDuringTest, Time]
71+
self, context: TestTimeContext
6872
) -> tuple[Time | None, Time | None]:
6973
"""Resolve Volume4DTemplate into concrete temporal bounds (start, end)"""
7074
if self.start_time is not None:
71-
time_start = self.start_time.resolve(times)
75+
time_start = self.start_time.resolve(context)
7276
else:
7377
time_start = None
7478

7579
if self.end_time is not None:
76-
time_end = self.end_time.resolve(times)
80+
time_end = self.end_time.resolve(context)
7781
else:
7882
time_end = None
7983

@@ -93,14 +97,14 @@ def resolve_times(
9397

9498
return time_start, time_end
9599

96-
def resolve(self, times: dict[TimeDuringTest, Time]) -> Volume4D:
100+
def resolve(self, context: TestTimeContext) -> Volume4D:
97101
"""Resolve Volume4DTemplate into concrete Volume4D."""
98102
volume = self.resolve_3d()
99103

100104
# Make 4D volume
101105
kwargs = {"volume": volume}
102106

103-
time_start, time_end = self.resolve_times(times)
107+
time_start, time_end = self.resolve_times(context)
104108

105109
if time_start is not None:
106110
kwargs["time_start"] = time_start

monitoring/monitorlib/temporal.py

Lines changed: 75 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -62,16 +62,71 @@ class NextDay(ImplicitDict):
6262
"""Acceptable days of the week. Omit to indicate that any day of the week is acceptable."""
6363

6464

65-
class TimeDuringTest(str, Enum):
66-
StartOfTestRun = "StartOfTestRun"
65+
class TimeDuringTest(str):
66+
"""A particular time during the test, as identified/named by this value.
67+
68+
Names listed below are provided by the framework when appropriate:
69+
* StartOfTestRun: The time at which the test run started.
70+
* StartOfScenario: The time at which the current scenario started.
71+
* TimeOfEvaluation: The time at which a TestTime was resolved to an absolute time; generally close to 'now'.
72+
"""
73+
74+
StartOfTestRun: TimeDuringTest
6775
"""The time at which the test run started."""
6876

69-
StartOfScenario = "StartOfScenario"
77+
StartOfScenario: TimeDuringTest
7078
"""The time at which the current scenario started."""
7179

72-
TimeOfEvaluation = "TimeOfEvaluation"
80+
TimeOfEvaluation: TimeDuringTest
7381
"""The time at which a TestTime was resolved to an absolute time; generally close to 'now'."""
7482

83+
ProvidedByFramework: list[TimeDuringTest]
84+
"""The TimeDuringTests provided by the framework when appropriate"""
85+
86+
87+
TimeDuringTest.StartOfTestRun = TimeDuringTest("StartOfTestRun")
88+
TimeDuringTest.StartOfScenario = TimeDuringTest("StartOfScenario")
89+
TimeDuringTest.TimeOfEvaluation = TimeDuringTest("TimeOfEvaluation")
90+
TimeDuringTest.ProvidedByFramework = [
91+
TimeDuringTest.StartOfTestRun,
92+
TimeDuringTest.StartOfScenario,
93+
TimeDuringTest.TimeOfEvaluation,
94+
]
95+
96+
97+
class Time(StringBasedDateTime):
98+
def offset(self, dt: timedelta) -> Time:
99+
return Time(self.datetime + dt)
100+
101+
def to_f3548v21(self) -> f3548v21.Time:
102+
return f3548v21.Time(value=self)
103+
104+
105+
class TestTimeContext(dict[TimeDuringTest, Time]):
106+
"""Context in which TestTimes are evaluated.
107+
108+
Stores definitions for TimeDuringTests."""
109+
110+
def evaluate_now(self) -> TestTimeContext:
111+
"""Set TimeOfEvaluation in this context to the current time.
112+
113+
This should be performed once before resolving a TestTime."""
114+
self[TimeDuringTest.TimeOfEvaluation] = Time(arrow.utcnow().datetime)
115+
return self
116+
117+
@staticmethod
118+
def all_times_are(t: Time) -> TestTimeContext:
119+
"""Returns a TestTimeContext where all framework-provided times are the provided time.
120+
121+
For the purpose of testing/validation."""
122+
return TestTimeContext(
123+
{
124+
TimeDuringTest.StartOfTestRun: t,
125+
TimeDuringTest.StartOfScenario: t,
126+
TimeDuringTest.TimeOfEvaluation: t,
127+
}
128+
)
129+
75130

76131
class TestTime(ImplicitDict):
77132
"""Exactly one of the time option fields of this object must be specified."""
@@ -85,6 +140,9 @@ class TestTime(ImplicitDict):
85140
time_during_test: TimeDuringTest | None = None
86141
"""Time option field to, if specified, use a timestamp relating to the current test run."""
87142

143+
name: TimeDuringTest | None
144+
"""If specified, update the TestTimeContext with the time computed for this TestTime as this name, which may then later be referenced by a different TestTime via time_during_test."""
145+
88146
next_day: NextDay | None = None
89147
"""Time option field to use a timestamp equal to midnight beginning the next occurrence of any matching day following the specified reference timestamp."""
90148

@@ -101,20 +159,20 @@ class TestTime(ImplicitDict):
101159
* "-08:00" (ISO time zone)
102160
* "US/Pacific" (IANA time zone)"""
103161

104-
def resolve(self, times: dict[TimeDuringTest, Time]) -> Time:
162+
def resolve(self, context: TestTimeContext) -> Time:
105163
"""Resolve TestTime into specific Time."""
106164
result = None
107165
if self.absolute_time is not None:
108166
result = self.absolute_time.datetime
109167
elif self.time_during_test is not None:
110-
if self.time_during_test not in times:
168+
if self.time_during_test not in context:
111169
raise ValueError(
112-
f"Specified {self.time_during_test} time during test was not provided when resolving TestTime"
170+
f"Specified '{self.time_during_test}' time during test was not provided when resolving TestTime"
113171
)
114-
result = times[self.time_during_test].datetime
172+
result = context[self.time_during_test].datetime
115173
elif self.next_day is not None:
116174
t0 = (
117-
arrow.get(self.next_day.starting_from.resolve(times).datetime)
175+
arrow.get(self.next_day.starting_from.resolve(context).datetime)
118176
.to(self.next_day.time_zone)
119177
.datetime
120178
)
@@ -130,11 +188,11 @@ def resolve(self, times: dict[TimeDuringTest, Time]) -> Time:
130188
result = t
131189
elif self.offset_from is not None:
132190
result = (
133-
self.offset_from.starting_from.resolve(times).datetime
191+
self.offset_from.starting_from.resolve(context).datetime
134192
+ self.offset_from.offset.timedelta
135193
)
136194
elif self.next_sun_position is not None:
137-
t0 = self.next_sun_position.starting_from.resolve(times).datetime
195+
t0 = self.next_sun_position.starting_from.resolve(context).datetime
138196

139197
dt = timedelta(minutes=5)
140198
lat = self.next_sun_position.observed_from.lat
@@ -186,7 +244,12 @@ def resolve(self, times: dict[TimeDuringTest, Time]) -> Time:
186244
if self.use_timezone:
187245
result = arrow.get(result).to(self.use_timezone).datetime
188246

189-
return Time(result)
247+
t_result = Time(result)
248+
249+
if "name" in self and self.name:
250+
context[self.name] = t_result
251+
252+
return t_result
190253

191254

192255
_weekdays = [
@@ -212,11 +275,3 @@ def _sun_elevation(t: datetime, lat_deg: float, lng_deg: float) -> float:
212275
Returns: Degrees above the horizon of the center of the sun.
213276
"""
214277
return get_solarposition(t, lat_deg, lng_deg).elevation.values[0] # pyright:ignore[reportAttributeAccessIssue]
215-
216-
217-
class Time(StringBasedDateTime):
218-
def offset(self, dt: timedelta) -> Time:
219-
return Time(self.datetime + dt)
220-
221-
def to_f3548v21(self) -> f3548v21.Time:
222-
return f3548v21.Time(value=self)

monitoring/uss_qualifier/resources/flight_planning/flight_intent_validation.py

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
FlightInfoTemplate,
1515
)
1616
from monitoring.monitorlib.geotemporal import Volume4D, Volume4DCollection
17-
from monitoring.monitorlib.temporal import Time, TimeDuringTest
17+
from monitoring.monitorlib.temporal import TestTimeContext, Time, TimeDuringTest
1818
from monitoring.monitorlib.uspace import problems_with_flight_authorisation
1919
from monitoring.uss_qualifier.resources.flight_planning.flight_intent import (
2020
FlightIntentID,
@@ -53,23 +53,16 @@ def validate_flight_intent_templates(
5353
extents = Volume4DCollection([])
5454

5555
now = Time(arrow.utcnow().datetime)
56-
times = {
57-
TimeDuringTest.StartOfTestRun: now,
58-
TimeDuringTest.StartOfScenario: now,
59-
TimeDuringTest.TimeOfEvaluation: now,
60-
}
61-
flight_intents = {k: v.resolve(times) for k, v in templates.items()}
56+
context = TestTimeContext.all_times_are(now)
57+
flight_intents = {k: v.resolve(context) for k, v in templates.items()}
6258
for flight_intent in flight_intents.values():
6359
extents.extend(flight_intent.basic_information.area)
6460
validate_flight_intents(flight_intents, expected_intents, now)
6561

6662
later = Time(now.datetime + MAX_TEST_RUN_DURATION)
67-
times = {
68-
TimeDuringTest.StartOfTestRun: now,
69-
TimeDuringTest.StartOfScenario: later,
70-
TimeDuringTest.TimeOfEvaluation: later,
71-
}
72-
flight_intents = {k: v.resolve(times) for k, v in templates.items()}
63+
context = TestTimeContext.all_times_are(later)
64+
context[TimeDuringTest.StartOfTestRun] = now
65+
flight_intents = {k: v.resolve(context) for k, v in templates.items()}
7366
for flight_intent in flight_intents.values():
7467
extents.extend(flight_intent.basic_information.area)
7568
validate_flight_intents(flight_intents, expected_intents, later)

monitoring/uss_qualifier/resources/netrid/service_area.py

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44
import arrow
55
from implicitdict import ImplicitDict
66

7-
from monitoring.monitorlib.geotemporal import Volume4D
8-
from monitoring.monitorlib.temporal import Time, TimeDuringTest
7+
from monitoring.monitorlib.temporal import TestTimeContext, Time
98
from monitoring.uss_qualifier.resources import VolumeResource
109
from monitoring.uss_qualifier.resources.resource import Resource
1110

@@ -35,11 +34,7 @@ def __init__(
3534

3635
now = Time(arrow.utcnow().datetime)
3736
resolved_for_tests = self._volume.specification.template.resolve(
38-
{
39-
TimeDuringTest.StartOfTestRun: now,
40-
TimeDuringTest.StartOfScenario: now,
41-
TimeDuringTest.TimeOfEvaluation: now,
42-
}
37+
TestTimeContext.all_times_are(now)
4338
)
4439

4540
if (
@@ -55,10 +50,6 @@ def __init__(
5550
f"In order to be usable for a ServiceAreaResource, the provided VolumeResource must declare time bounds. The volume template was obtained from: {resource_origin}"
5651
)
5752

58-
def resolved_volume4d(self, times: dict[TimeDuringTest, Time]) -> Volume4D:
59-
times[TimeDuringTest.TimeOfEvaluation] = Time(arrow.utcnow().datetime)
60-
return self._volume.specification.template.resolve(times)
61-
6253
def s2_vertices(self):
6354
return self._volume.specification.s2_vertices()
6455

@@ -85,9 +76,11 @@ def altitude_max(self) -> float:
8576
return v3d.altitude_upper.to_w84_m()
8677

8778
def resolved_time_bounds(
88-
self, times: dict[TimeDuringTest, Time]
79+
self, context: TestTimeContext
8980
) -> tuple[datetime.datetime, datetime.datetime]:
90-
time_start, time_end = self._volume.specification.template.resolve_times(times)
81+
time_start, time_end = self._volume.specification.template.resolve_times(
82+
context
83+
)
9184
if time_start is None or time_end is None:
9285
# Note this should not happen as we check at construction time that these bounds exist
9386
raise ValueError("The underlying volume does not have time bounds")

monitoring/uss_qualifier/resources/planning_area.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from monitoring.monitorlib.geo import make_latlng_rect
1515
from monitoring.monitorlib.geotemporal import Volume4D
1616
from monitoring.monitorlib.subscription_params import SubscriptionParams
17-
from monitoring.monitorlib.temporal import Time, TimeDuringTest
17+
from monitoring.monitorlib.temporal import TestTimeContext, Time
1818
from monitoring.monitorlib.testing import make_fake_url
1919
from monitoring.uss_qualifier.resources.resource import Resource
2020
from monitoring.uss_qualifier.resources.volume import VolumeResource
@@ -56,8 +56,8 @@ def __init__(
5656
self.specification = specification
5757
self._volume = volume
5858

59-
def resolved_volume4d(self, times: dict[TimeDuringTest, Time]) -> Volume4D:
60-
return self._volume.specification.template.resolve(times)
59+
def resolved_volume4d(self, context: TestTimeContext) -> Volume4D:
60+
return self._volume.specification.template.resolve(context)
6161

6262
def resolved_volume4d_with_times(
6363
self, time_start: datetime.datetime | None, time_end: datetime.datetime | None

0 commit comments

Comments
 (0)