recon_gen.common.intervals
BC.1 — Typed time-range interval value types.
Four types, two responsibilities:
Time-range:
DateInterval(closed-closed[start, end]over calendar dates) andDateTimeInterval(half-open[start, end_exclusive)over naive timestamps). One named type per endpoint convention. The business-facing convention pairs with the granularity that uses it most: dates are closed-closed (audit reports say “week of May 17 - May 23” meaning both endpoints), timestamps are half-open (math + SQLcol >= start AND col < end).Plant schedule:
SingleDayPlant(one day, derived from a window viaat_window_end/at_window_start/at_offset_from_end) andMultiDayPlant(spans every day, viaspans). Each plant generator declares which schedule it consumes in its type signature; the factory inplant_adapter.pyconstructs the right shape per generator from the test’s window. No “where in the window do I plant?” convention lives in generator bodies — it lives in the type.
Single-TZ invariant
All datetimes in this codebase are NAIVE (no tzinfo); their LOCAL
meaning is the operator’s machine TZ by convention (see
project_local_tz_convention in the auto-memory). The system assumes
ONE consistent TZ end-to-end. DateTimeInterval.__post_init__ rejects
aware datetimes at construction; DateInterval.as_half_open_datetimes()
returns naive datetimes. The intervals layer refuses to be the place
where TZ policy lives.
Wiring-site enforcement
Two AST lints in tests/unit/test_typing_smells.py:
no-naked-interval-ctor: bareDateInterval(...)/ etc. calls outside this module must be a.classmethod_name(...)form. The named-convention constructors are the only way wiring sites should mint these.no-raw-temporal-args: function/method parameters annotateddateordatetimeinsrc/recon_gen/**are a smell — wrap in one of these types orRunContext(Phase BD). Dataclass field annotations are unaffected (point values of real events, not policy-carrying params). Staged: enabled at end of BC.5 once the migration surface is wrapped.
Per feedback_invariants_in_types: types bring meaning with them,
convention hides it.
Classes
|
Closed-closed |
|
Half-open |
|
A plant whose effect spans every day of a window. |
|
A plant that lands on exactly one calendar day. |
- class recon_gen.common.intervals.DateInterval(start, end)[source]
Bases:
objectClosed-closed
[start, end]interval over calendar dates.Business-facing convention — audit reports, matview
BETWEENclauses, dashboard date-range filters. For SQL/math half-open shape convert via.as_half_open_datetimes()— the +1-day flip lives inside that method, not at each callsite.Invariant:
start <= end(a one-day interval isstart == end).- Parameters:
start (date)
end (date)
- as_half_open_datetimes()[source]
Convert
[start, end](closed dates) to[start 00:00, end+1 00:00)(half-open naive datetimes).Single-TZ invariant — returned datetimes are NAIVE. The +1-day flip widens “end-of-day on end” into “start-of-day on end+1,” so callers don’t write
end + timedelta(days=1)by hand.- Return type:
- classmethod closed(start, end)[source]
Explicit closed-closed construction.
- Return type:
Self- Parameters:
start (date)
end (date)
- contains(d)[source]
True iff
dis in[start, end](both endpoints inclusive).- Return type:
bool- Parameters:
d (date)
- property days: int
Number of days in the interval, both endpoints counted.
single_day(x).days == 1.
- end: date
- classmethod single_day(d)[source]
A one-day interval
[d, d].- Return type:
Self- Parameters:
d (date)
- start: date
- classmethod trailing_days_ending_today(today, days)[source]
[today - days + 1, today]— “last N days including today”.For live-streaming dashboards where today’s partial data counts.
Example:
trailing_days_ending_today(2026-05-24, 7)→[2026-05-18, 2026-05-24](7 days, today included).- Return type:
Self- Parameters:
today (date)
days (int)
- classmethod trailing_days_ending_yesterday(today, days)[source]
[today - days, today - 1]— the audit window convention.N days, ending on the most-recently-closed business day.
todayitself is excluded (the day isn’t closed yet — you can’t audit a day before it ends).Example:
trailing_days_ending_yesterday(2026-05-24, 7)→[2026-05-17, 2026-05-23](7 days, today excluded).- Return type:
Self- Parameters:
today (date)
days (int)
- class recon_gen.common.intervals.DateTimeInterval(start, end_exclusive)[source]
Bases:
objectHalf-open
[start, end_exclusive)interval over NAIVE timestamps.The math/SQL convention —
col >= start AND col < end_exclusive. Used bystuck_*matviews (posted_at >= now - interval), SQLBETWEENcallers that need “end of day” without hand-rolling +1 day, live-streaming dashboards that slide past midnight.Closed-closed daily counterpart is
DateInterval; convert viaDateInterval.as_half_open_datetimes().Single-TZ invariant:
startandend_exclusiveMUST be naive (tzinfo is None). Aware datetimes raise ValueError at construction. See module docstring.- Parameters:
start (datetime)
end_exclusive (datetime)
- contains(dt)[source]
True iff
dtis in[start, end_exclusive).- Return type:
bool- Parameters:
dt (datetime)
- property duration: timedelta
end_exclusive - start.
- end_exclusive: datetime
- classmethod half_open(start, end_exclusive)[source]
Explicit half-open construction.
- Return type:
Self- Parameters:
start (datetime)
end_exclusive (datetime)
- start: datetime
- class recon_gen.common.intervals.MultiDayPlant(window)[source]
Bases:
objectA plant whose effect spans every day of a window.
StuckUnbundledGenerator(unbundled-bucket stuck for N days),RailFiringGenerator(a rail fires repeatedly across a span). Generators that consumeMultiDayPlantwalkiter_days(); they DO NOT pick “the right day” internally.- Parameters:
window (DateInterval)
- iter_days()[source]
Yield every date in the underlying window (inclusive both ends).
- Return type:
Iterable[date]
- classmethod spans(window)[source]
Plant covers every day in
window. Named to mirror its policy: the plant SPANS the window, it doesn’t sample within it.- Return type:
Self- Parameters:
window (DateInterval)
- window: DateInterval
- class recon_gen.common.intervals.SingleDayPlant(day)[source]
Bases:
objectA plant that lands on exactly one calendar day.
DriftGenerator,OverdraftGenerator,LimitBreachGenerator— the invariants that fire on one day at a time consume this. The day is a derived value, not a free field: the factory uses one of the namedat_*constructors so the call site declares its policy.- Parameters:
day (date)
- classmethod at_offset_from_end(window, days_back)[source]
window.end - days_back, validated againstwindow.For generators that need to plant N days before the window’s tail (e.g. a drift that the audit catches late).
- Return type:
Self- Parameters:
window (DateInterval)
days_back (int)
- classmethod at_window_end(window)[source]
Most-recently-closed day in the window. The default for single-day plants whose existence implies a finished day.
- Return type:
Self- Parameters:
window (DateInterval)
- classmethod at_window_start(window)[source]
Earliest day in the window.
- Return type:
Self- Parameters:
window (DateInterval)
- day: date