Split weather data types from forecast derivation
This commit is contained in:
@@ -14,7 +14,7 @@ prose.
|
||||
Inputs:
|
||||
|
||||
- resolved report definition, generation time, timezone, and valid period
|
||||
- forecast bundle with source provenance and warnings
|
||||
- `weatherdata.Bundle` with source provenance and warnings
|
||||
- derived daily or period summaries where required
|
||||
- configured units, timezone, and descriptive location context
|
||||
|
||||
|
||||
@@ -5,14 +5,14 @@ This document describes deterministic forecast summarization in
|
||||
|
||||
## Purpose
|
||||
|
||||
`internal/forecast` converts normalized bundle data into daily and period
|
||||
summaries used by briefing builders.
|
||||
`internal/forecast` converts normalized `weatherdata` bundle data into daily
|
||||
and period summaries used by briefing builders.
|
||||
|
||||
## Inputs And Outputs
|
||||
|
||||
Inputs:
|
||||
|
||||
- `forecast.Bundle`
|
||||
- `weatherdata.Bundle`
|
||||
- local date or resolved report period
|
||||
- timezone
|
||||
- configured daypart definitions
|
||||
@@ -43,7 +43,7 @@ configuration.
|
||||
|
||||
## External Adapters Used
|
||||
|
||||
None directly. Forecast data arrives through `forecast.Bundle`.
|
||||
None directly. Forecast data arrives through `weatherdata.Bundle`.
|
||||
|
||||
## State Or Manifest Behavior
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Weather Data Internals
|
||||
|
||||
This document describes Weather API ingestion into `forecast.Bundle`.
|
||||
This document describes Weather API ingestion into `weatherdata.Bundle`.
|
||||
|
||||
## Purpose
|
||||
|
||||
@@ -19,7 +19,7 @@ Inputs:
|
||||
|
||||
Outputs:
|
||||
|
||||
- `forecast.Bundle` with observation, current conditions, hourly forecast,
|
||||
- `weatherdata.Bundle` with observation, current conditions, hourly forecast,
|
||||
narrative forecast, active alerts, discussion, latest weather story, source
|
||||
records, and source warnings
|
||||
- stub source record for the daily forecast source slot
|
||||
|
||||
@@ -16,8 +16,9 @@ Developers and LLM coding agents should use it with
|
||||
- `internal/adapters/distributor`: Distributor upload adapter.
|
||||
- `internal/adapters/weatherapi`: Weather API HTTP adapter.
|
||||
- `internal/adapters/scriptorium`: Scriptorium subprocess adapter.
|
||||
- `internal/forecast`: normalized bundle types and deterministic forecast
|
||||
derivation.
|
||||
- `internal/weatherdata`: normalized weather source facts, source metadata, and
|
||||
source warnings.
|
||||
- `internal/forecast`: deterministic forecast derivation.
|
||||
- `internal/report`: report definitions, valid periods, batches, output names,
|
||||
and comparison declarations.
|
||||
- `internal/briefing`: report-specific briefing package builders.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Package weatherapi adapts the internal weather API to forecast bundles.
|
||||
// Package weatherapi adapts the internal weather API to weather data bundles.
|
||||
package weatherapi
|
||||
|
||||
import (
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/config"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/fileutil"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/forecast"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
@@ -83,11 +83,11 @@ func New(cfg config.Config, opts ...Option) (*Client, error) {
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (c *Client) FetchBundle(ctx context.Context) (*forecast.Bundle, error) {
|
||||
func (c *Client) FetchBundle(ctx context.Context) (*weatherdata.Bundle, error) {
|
||||
fetchedAt := c.now()
|
||||
builder := bundleBuilder{
|
||||
client: c,
|
||||
bundle: &forecast.Bundle{FetchedAt: fetchedAt},
|
||||
bundle: &weatherdata.Bundle{FetchedAt: fetchedAt},
|
||||
fetchedAt: fetchedAt,
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ func (c *Client) FetchBundle(ctx context.Context) (*forecast.Bundle, error) {
|
||||
|
||||
type bundleBuilder struct {
|
||||
client *Client
|
||||
bundle *forecast.Bundle
|
||||
bundle *weatherdata.Bundle
|
||||
fetchedAt time.Time
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ func (b *bundleBuilder) fetchObservation(ctx context.Context) error {
|
||||
if raw == nil {
|
||||
return b.handleMissing(&source, "observation data is missing", false)
|
||||
}
|
||||
var observation forecast.Observation
|
||||
var observation weatherdata.Observation
|
||||
if err := decodeSource(raw, &observation); err != nil {
|
||||
return b.handleMalformed(&source, err, false)
|
||||
}
|
||||
@@ -151,7 +151,7 @@ func (b *bundleBuilder) fetchCurrent(ctx context.Context) error {
|
||||
if raw == nil {
|
||||
return b.handleMissing(&source, "current conditions data is missing", false)
|
||||
}
|
||||
var current forecast.Current
|
||||
var current weatherdata.Current
|
||||
if err := decodeSource(raw, ¤t); err != nil {
|
||||
return b.handleMalformed(&source, err, false)
|
||||
}
|
||||
@@ -168,7 +168,7 @@ func (b *bundleBuilder) fetchHourly(ctx context.Context) error {
|
||||
if raw == nil {
|
||||
return b.handleMissing(&source, "hourly forecast data is missing", true)
|
||||
}
|
||||
var hourly forecast.ForecastRun
|
||||
var hourly weatherdata.ForecastRun
|
||||
if err := decodeSource(raw, &hourly); err != nil {
|
||||
return fmt.Errorf("decode hourly forecast from %s: %w", source.Endpoint, err)
|
||||
}
|
||||
@@ -190,7 +190,7 @@ func (b *bundleBuilder) fetchNarrative(ctx context.Context) error {
|
||||
if raw == nil {
|
||||
return b.handleMissing(&source, "narrative forecast data is missing", false)
|
||||
}
|
||||
var narrative forecast.ForecastRun
|
||||
var narrative weatherdata.ForecastRun
|
||||
if err := decodeSource(raw, &narrative); err != nil {
|
||||
return b.handleMalformed(&source, err, false)
|
||||
}
|
||||
@@ -210,11 +210,11 @@ func (b *bundleBuilder) fetchAlerts(ctx context.Context) error {
|
||||
return b.handleMissing(&source, "active alerts data is missing", false)
|
||||
}
|
||||
if isJSONNull(raw) {
|
||||
b.bundle.Alerts = &forecast.AlertRun{Raw: append(json.RawMessage(nil), raw...)}
|
||||
b.bundle.Alerts = &weatherdata.AlertRun{Raw: append(json.RawMessage(nil), raw...)}
|
||||
b.addSource(source)
|
||||
return nil
|
||||
}
|
||||
var alerts forecast.AlertRun
|
||||
var alerts weatherdata.AlertRun
|
||||
if err := decodeSource(raw, &alerts); err != nil {
|
||||
return b.handleMalformed(&source, err, false)
|
||||
}
|
||||
@@ -235,7 +235,7 @@ func (b *bundleBuilder) fetchDiscussion(ctx context.Context) error {
|
||||
if raw == nil {
|
||||
return b.handleMissing(&source, "forecast discussion data is missing", false)
|
||||
}
|
||||
var discussion forecast.Discussion
|
||||
var discussion weatherdata.Discussion
|
||||
if err := decodeSource(raw, &discussion); err != nil {
|
||||
return b.handleMalformed(&source, err, false)
|
||||
}
|
||||
@@ -254,7 +254,7 @@ func (b *bundleBuilder) fetchWeatherStory(ctx context.Context) error {
|
||||
if raw == nil {
|
||||
return b.handleMissing(&source, "NWS weather story data is missing", false)
|
||||
}
|
||||
var story forecast.WeatherStory
|
||||
var story weatherdata.WeatherStory
|
||||
if err := decodeSource(raw, &story); err != nil {
|
||||
return b.handleMalformed(&source, err, false)
|
||||
}
|
||||
@@ -268,7 +268,7 @@ func (b *bundleBuilder) fetchWeatherStory(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (b *bundleBuilder) addStub(sourceName string, message string) error {
|
||||
source := forecast.Source{
|
||||
source := weatherdata.Source{
|
||||
Name: sourceName,
|
||||
FetchedAt: b.fetchedAt,
|
||||
Missing: true,
|
||||
@@ -276,7 +276,7 @@ func (b *bundleBuilder) addStub(sourceName string, message string) error {
|
||||
return b.applyMissingPolicy(&source, "missing_source", message)
|
||||
}
|
||||
|
||||
func (b *bundleBuilder) handleMissing(source *forecast.Source, message string, required bool) error {
|
||||
func (b *bundleBuilder) handleMissing(source *weatherdata.Source, message string, required bool) error {
|
||||
source.Missing = true
|
||||
if required {
|
||||
return fmt.Errorf("%s from %s is required", message, source.Endpoint)
|
||||
@@ -284,7 +284,7 @@ func (b *bundleBuilder) handleMissing(source *forecast.Source, message string, r
|
||||
return b.applyMissingPolicy(source, "missing_source", message)
|
||||
}
|
||||
|
||||
func (b *bundleBuilder) handleMalformed(source *forecast.Source, err error, required bool) error {
|
||||
func (b *bundleBuilder) handleMalformed(source *weatherdata.Source, err error, required bool) error {
|
||||
if required {
|
||||
return fmt.Errorf("decode %s from %s: %w", source.Name, source.Endpoint, err)
|
||||
}
|
||||
@@ -292,13 +292,13 @@ func (b *bundleBuilder) handleMalformed(source *forecast.Source, err error, requ
|
||||
return b.applyMissingPolicy(source, "malformed_source", fmt.Sprintf("malformed %s data: %v", source.Name, err))
|
||||
}
|
||||
|
||||
func (b *bundleBuilder) applyMissingPolicy(source *forecast.Source, code string, message string) error {
|
||||
func (b *bundleBuilder) applyMissingPolicy(source *weatherdata.Source, code string, message string) error {
|
||||
policy := b.client.policyFor(source.Name)
|
||||
if policy == config.MissingSourceError {
|
||||
return fmt.Errorf("%s: %s", source.Name, message)
|
||||
}
|
||||
if policy == config.MissingSourceWarn {
|
||||
warning := forecast.SourceWarning{
|
||||
warning := weatherdata.SourceWarning{
|
||||
Source: source.Name,
|
||||
Code: code,
|
||||
Severity: "warning",
|
||||
@@ -313,7 +313,7 @@ func (b *bundleBuilder) applyMissingPolicy(source *forecast.Source, code string,
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *bundleBuilder) addSource(source forecast.Source) {
|
||||
func (b *bundleBuilder) addSource(source weatherdata.Source) {
|
||||
b.bundle.Sources = append(b.bundle.Sources, source)
|
||||
}
|
||||
|
||||
@@ -335,33 +335,33 @@ type envelope struct {
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
|
||||
func (c *Client) fetch(ctx context.Context, sourceName string, endpoint string, opts queryOptions) (json.RawMessage, forecast.Source, error) {
|
||||
func (c *Client) fetch(ctx context.Context, sourceName string, endpoint string, opts queryOptions) (json.RawMessage, weatherdata.Source, error) {
|
||||
reqURL := c.endpointURL(endpoint, opts)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL.String(), nil)
|
||||
if err != nil {
|
||||
return nil, forecast.Source{}, fmt.Errorf("create request for %s: %w", endpoint, err)
|
||||
return nil, weatherdata.Source{}, fmt.Errorf("create request for %s: %w", endpoint, err)
|
||||
}
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, forecast.Source{}, fmt.Errorf("fetch %s: %w", endpoint, err)
|
||||
return nil, weatherdata.Source{}, fmt.Errorf("fetch %s: %w", endpoint, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 10<<20))
|
||||
if err != nil {
|
||||
return nil, forecast.Source{}, fmt.Errorf("read %s response: %w", endpoint, err)
|
||||
return nil, weatherdata.Source{}, fmt.Errorf("read %s response: %w", endpoint, err)
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return nil, forecast.Source{}, fmt.Errorf("fetch %s: unexpected HTTP status %d: %s", endpoint, resp.StatusCode, strings.TrimSpace(string(body)))
|
||||
return nil, weatherdata.Source{}, fmt.Errorf("fetch %s: unexpected HTTP status %d: %s", endpoint, resp.StatusCode, strings.TrimSpace(string(body)))
|
||||
}
|
||||
|
||||
var env envelope
|
||||
if err := json.Unmarshal(body, &env); err != nil {
|
||||
return nil, forecast.Source{}, fmt.Errorf("decode %s envelope: %w", endpoint, err)
|
||||
return nil, weatherdata.Source{}, fmt.Errorf("decode %s envelope: %w", endpoint, err)
|
||||
}
|
||||
|
||||
source := forecast.Source{
|
||||
source := weatherdata.Source{
|
||||
Name: sourceName,
|
||||
Endpoint: endpoint,
|
||||
Query: queryMap(reqURL.Query()),
|
||||
@@ -430,7 +430,7 @@ func sourceHash(raw json.RawMessage) (string, error) {
|
||||
return hex.EncodeToString(sum[:]), nil
|
||||
}
|
||||
|
||||
func SaveBundle(path string, bundle *forecast.Bundle) error {
|
||||
func SaveBundle(path string, bundle *weatherdata.Bundle) error {
|
||||
if err := fileutil.WriteJSONAtomic(path, bundle); err != nil {
|
||||
return fmt.Errorf("save bundle: %w", err)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/config"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/forecast"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata"
|
||||
)
|
||||
|
||||
func TestFetchBundleFromFixtures(t *testing.T) {
|
||||
@@ -423,7 +423,7 @@ func containsPath(requested []string, path string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func sourceByName(t *testing.T, sources []forecast.Source, name string) forecast.Source {
|
||||
func sourceByName(t *testing.T, sources []weatherdata.Source, name string) weatherdata.Source {
|
||||
t.Helper()
|
||||
for _, source := range sources {
|
||||
if source.Name == name {
|
||||
@@ -431,7 +431,7 @@ func sourceByName(t *testing.T, sources []forecast.Source, name string) forecast
|
||||
}
|
||||
}
|
||||
t.Fatalf("source %q not found in %#v", name, sources)
|
||||
return forecast.Source{}
|
||||
return weatherdata.Source{}
|
||||
}
|
||||
|
||||
func hashFixtureData(t *testing.T, fixture string) string {
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/state"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata"
|
||||
)
|
||||
|
||||
type ReportKind string
|
||||
@@ -393,7 +394,7 @@ func reportBatchForCommand(kind BatchKind) (report.Batch, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func FetchBundle(ctx context.Context, req FetchBundleRequest) (*forecast.Bundle, error) {
|
||||
func FetchBundle(ctx context.Context, req FetchBundleRequest) (*weatherdata.Bundle, error) {
|
||||
client, err := weatherapi.New(req.Config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -405,7 +406,7 @@ func FetchBundle(ctx context.Context, req FetchBundleRequest) (*forecast.Bundle,
|
||||
return bundle, nil
|
||||
}
|
||||
|
||||
func FetchAndSaveBundle(ctx context.Context, req FetchBundleRequest) (*forecast.Bundle, error) {
|
||||
func FetchAndSaveBundle(ctx context.Context, req FetchBundleRequest) (*weatherdata.Bundle, error) {
|
||||
if req.OutputPath == "" {
|
||||
return nil, fmt.Errorf("output path is required")
|
||||
}
|
||||
@@ -787,7 +788,7 @@ func distributorUploadFiles(sourcePath string, bundlePaths []string) []distribut
|
||||
return files
|
||||
}
|
||||
|
||||
func BuildBriefing(req BriefingRequest, bundle *forecast.Bundle) (briefing.Package, error) {
|
||||
func BuildBriefing(req BriefingRequest, bundle *weatherdata.Bundle) (briefing.Package, error) {
|
||||
location, err := timeutil.LoadLocation(req.Config.WeatherAPI.Timezone)
|
||||
if err != nil {
|
||||
return briefing.Package{}, err
|
||||
|
||||
@@ -6,11 +6,11 @@ import (
|
||||
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/briefing"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/config"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/forecast"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/state"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata"
|
||||
)
|
||||
|
||||
type InspectReportsRequest struct {
|
||||
@@ -24,11 +24,11 @@ type InspectRunRequest struct {
|
||||
}
|
||||
|
||||
type SourceInspection struct {
|
||||
RunID string `json:"runId"`
|
||||
ReportID report.ID `json:"reportId"`
|
||||
SourceLocation string `json:"sourceLocation,omitempty"`
|
||||
Sources []briefing.SourceMetadata `json:"sources,omitempty"`
|
||||
Warnings []forecast.SourceWarning `json:"warnings,omitempty"`
|
||||
RunID string `json:"runId"`
|
||||
ReportID report.ID `json:"reportId"`
|
||||
SourceLocation string `json:"sourceLocation,omitempty"`
|
||||
Sources []briefing.SourceMetadata `json:"sources,omitempty"`
|
||||
Warnings []weatherdata.SourceWarning `json:"warnings,omitempty"`
|
||||
}
|
||||
|
||||
func InspectReports(ctx context.Context, req InspectReportsRequest) ([]state.ReportRecord, error) {
|
||||
|
||||
@@ -9,18 +9,19 @@ import (
|
||||
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/forecast"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata"
|
||||
)
|
||||
|
||||
type Daily struct {
|
||||
BottomLine BottomLine `json:"bottomLine"`
|
||||
Dayparts []forecast.DaypartSummary `json:"dayparts"`
|
||||
RelevantAlerts []forecast.AlertOverlap `json:"relevantAlerts,omitempty"`
|
||||
OutdoorWindows OutdoorWindows `json:"outdoorWindows"`
|
||||
Planning *TomorrowPlanning `json:"planning,omitempty"`
|
||||
NarrativePeriods []forecast.ForecastPeriod `json:"narrativePeriods,omitempty"`
|
||||
Discussion DiscussionContext `json:"discussion,omitempty"`
|
||||
WeatherStory *WeatherStoryContext `json:"weatherStory,omitempty"`
|
||||
ForecastSummaryDate string `json:"forecastSummaryDate"`
|
||||
BottomLine BottomLine `json:"bottomLine"`
|
||||
Dayparts []forecast.DaypartSummary `json:"dayparts"`
|
||||
RelevantAlerts []forecast.AlertOverlap `json:"relevantAlerts,omitempty"`
|
||||
OutdoorWindows OutdoorWindows `json:"outdoorWindows"`
|
||||
Planning *TomorrowPlanning `json:"planning,omitempty"`
|
||||
NarrativePeriods []weatherdata.ForecastPeriod `json:"narrativePeriods,omitempty"`
|
||||
Discussion DiscussionContext `json:"discussion,omitempty"`
|
||||
WeatherStory *WeatherStoryContext `json:"weatherStory,omitempty"`
|
||||
ForecastSummaryDate string `json:"forecastSummaryDate"`
|
||||
}
|
||||
|
||||
type BottomLine struct {
|
||||
@@ -250,7 +251,7 @@ func daypartNamed(dayparts []forecast.DaypartSummary, name string) *forecast.Day
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildDiscussion(discussion *forecast.Discussion) DiscussionContext {
|
||||
func buildDiscussion(discussion *weatherdata.Discussion) DiscussionContext {
|
||||
if discussion == nil {
|
||||
return DiscussionContext{}
|
||||
}
|
||||
@@ -267,7 +268,7 @@ func buildDiscussion(discussion *forecast.Discussion) DiscussionContext {
|
||||
return ctx
|
||||
}
|
||||
|
||||
func buildWeatherStory(bundle *forecast.Bundle) *WeatherStoryContext {
|
||||
func buildWeatherStory(bundle *weatherdata.Bundle) *WeatherStoryContext {
|
||||
if bundle == nil || bundle.WeatherStory == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/forecast"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata"
|
||||
)
|
||||
|
||||
func TestDailyBriefingFromRepresentativeFixture(t *testing.T) {
|
||||
@@ -20,7 +21,7 @@ func TestDailyBriefingFromRepresentativeFixture(t *testing.T) {
|
||||
currentFeelsLike := 76.1
|
||||
currentHumidity := 56.0
|
||||
currentWind := 10.7
|
||||
bundle.Current = &forecast.Current{
|
||||
bundle.Current = &weatherdata.Current{
|
||||
ConditionText: "Partly cloudy",
|
||||
IsDay: ¤tIsDay,
|
||||
TemperatureF: ¤tTemp,
|
||||
@@ -29,7 +30,7 @@ func TestDailyBriefingFromRepresentativeFixture(t *testing.T) {
|
||||
WindSpeedMph: ¤tWind,
|
||||
}
|
||||
bundle.Sources[0].DataSHA256 = "abc123"
|
||||
bundle.Warnings = []forecast.SourceWarning{{Source: "daily", Code: "missing_source", Severity: "warning"}}
|
||||
bundle.Warnings = []weatherdata.SourceWarning{{Source: "daily", Code: "missing_source", Severity: "warning"}}
|
||||
location := mustLocation(t)
|
||||
resolved := mustResolveDaily(t, location)
|
||||
summary, err := forecast.BuildDailySummary(bundle, resolved.ValidPeriod.Start, location, defaultDayparts())
|
||||
@@ -112,17 +113,17 @@ func TestDailyBriefingFromRepresentativeFixture(t *testing.T) {
|
||||
func TestDailyBriefingQuietWeather(t *testing.T) {
|
||||
location := mustLocation(t)
|
||||
resolved := mustResolveDaily(t, location)
|
||||
bundle := &forecast.Bundle{
|
||||
Hourly: &forecast.ForecastRun{Periods: []forecast.ForecastPeriod{
|
||||
bundle := &weatherdata.Bundle{
|
||||
Hourly: &weatherdata.ForecastRun{Periods: []weatherdata.ForecastPeriod{
|
||||
quietHour("2026-05-29T09:00:00-05:00", "2026-05-29T10:00:00-05:00", 72),
|
||||
}},
|
||||
Alerts: &forecast.AlertRun{},
|
||||
Sources: []forecast.Source{
|
||||
Alerts: &weatherdata.AlertRun{},
|
||||
Sources: []weatherdata.Source{
|
||||
{Name: "hourly", FetchedAt: time.Now()},
|
||||
{Name: "alerts", Endpoint: "/alerts/active", FetchedAt: time.Now()},
|
||||
{Name: "current", Endpoint: "/conditions/current", FetchedAt: time.Now(), Missing: true},
|
||||
},
|
||||
Warnings: []forecast.SourceWarning{{Source: "current", Code: "missing_source", Severity: "warning"}},
|
||||
Warnings: []weatherdata.SourceWarning{{Source: "current", Code: "missing_source", Severity: "warning"}},
|
||||
}
|
||||
summary, err := forecast.BuildDailySummary(bundle, resolved.ValidPeriod.Start, location, defaultDayparts())
|
||||
if err != nil {
|
||||
@@ -163,7 +164,7 @@ func TestDailyBriefingAlertExclusion(t *testing.T) {
|
||||
location := mustLocation(t)
|
||||
resolved := mustResolveDaily(t, location)
|
||||
bundle := loadBundleFixture(t)
|
||||
bundle.Alerts = &forecast.AlertRun{Alerts: []json.RawMessage{
|
||||
bundle.Alerts = &weatherdata.AlertRun{Alerts: []json.RawMessage{
|
||||
json.RawMessage(`{"event":"Future Watch","effective":"2026-06-01T00:00:00-05:00","expires":"2026-06-01T06:00:00-05:00"}`),
|
||||
}}
|
||||
summary, err := forecast.BuildDailySummary(bundle, resolved.ValidPeriod.Start, location, defaultDayparts())
|
||||
@@ -265,13 +266,13 @@ func TestSaveBriefingPackage(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func loadBundleFixture(t *testing.T) *forecast.Bundle {
|
||||
func loadBundleFixture(t *testing.T) *weatherdata.Bundle {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(filepath.Join("..", "forecast", "testdata", "daily_bundle.json"))
|
||||
if err != nil {
|
||||
t.Fatalf("read bundle fixture: %v", err)
|
||||
}
|
||||
var bundle forecast.Bundle
|
||||
var bundle weatherdata.Bundle
|
||||
if err := json.Unmarshal(data, &bundle); err != nil {
|
||||
t.Fatalf("decode bundle fixture: %v", err)
|
||||
}
|
||||
@@ -300,8 +301,8 @@ func defaultDayparts() []forecast.DaypartDefinition {
|
||||
}
|
||||
}
|
||||
|
||||
func quietHour(start string, end string, temperature float64) forecast.ForecastPeriod {
|
||||
return forecast.ForecastPeriod{
|
||||
func quietHour(start string, end string, temperature float64) weatherdata.ForecastPeriod {
|
||||
return weatherdata.ForecastPeriod{
|
||||
StartTime: mustParse(start),
|
||||
EndTime: mustParse(end),
|
||||
TextDescription: "Clear",
|
||||
|
||||
@@ -6,9 +6,9 @@ import (
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/fileutil"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/forecast"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata"
|
||||
)
|
||||
|
||||
const SchemaVersion = "weatherreporter.briefing.v1"
|
||||
@@ -23,21 +23,21 @@ type Package struct {
|
||||
}
|
||||
|
||||
type Metadata struct {
|
||||
SchemaVersion string `json:"schemaVersion"`
|
||||
RunID string `json:"runId"`
|
||||
ReportID report.ID `json:"reportId"`
|
||||
Variant string `json:"variant,omitempty"`
|
||||
PromptID string `json:"promptId"`
|
||||
GeneratedAt time.Time `json:"generatedAt"`
|
||||
Units string `json:"units"`
|
||||
Timezone string `json:"timezone"`
|
||||
ValidPeriod timeutil.Period `json:"validPeriod"`
|
||||
Location *LocationContext `json:"location,omitempty"`
|
||||
SourceLocationID string `json:"sourceLocationId,omitempty"`
|
||||
SourceLocation string `json:"sourceLocation,omitempty"`
|
||||
Sources []SourceMetadata `json:"sources,omitempty"`
|
||||
SourceWarnings []forecast.SourceWarning `json:"sourceWarnings,omitempty"`
|
||||
Alerts *AlertStatus `json:"alerts,omitempty"`
|
||||
SchemaVersion string `json:"schemaVersion"`
|
||||
RunID string `json:"runId"`
|
||||
ReportID report.ID `json:"reportId"`
|
||||
Variant string `json:"variant,omitempty"`
|
||||
PromptID string `json:"promptId"`
|
||||
GeneratedAt time.Time `json:"generatedAt"`
|
||||
Units string `json:"units"`
|
||||
Timezone string `json:"timezone"`
|
||||
ValidPeriod timeutil.Period `json:"validPeriod"`
|
||||
Location *LocationContext `json:"location,omitempty"`
|
||||
SourceLocationID string `json:"sourceLocationId,omitempty"`
|
||||
SourceLocation string `json:"sourceLocation,omitempty"`
|
||||
Sources []SourceMetadata `json:"sources,omitempty"`
|
||||
SourceWarnings []weatherdata.SourceWarning `json:"sourceWarnings,omitempty"`
|
||||
Alerts *AlertStatus `json:"alerts,omitempty"`
|
||||
}
|
||||
|
||||
type LocationContext struct {
|
||||
@@ -63,14 +63,14 @@ type CurrentConditionsContext struct {
|
||||
}
|
||||
|
||||
type SourceMetadata struct {
|
||||
Name string `json:"name"`
|
||||
Endpoint string `json:"endpoint,omitempty"`
|
||||
FetchedAt time.Time `json:"fetchedAt"`
|
||||
IssuedAt *time.Time `json:"issuedAt,omitempty"`
|
||||
UpdatedAt *time.Time `json:"updatedAt,omitempty"`
|
||||
DataSHA256 string `json:"dataSha256,omitempty"`
|
||||
Missing bool `json:"missing,omitempty"`
|
||||
Warnings []forecast.SourceWarning `json:"warnings,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Endpoint string `json:"endpoint,omitempty"`
|
||||
FetchedAt time.Time `json:"fetchedAt"`
|
||||
IssuedAt *time.Time `json:"issuedAt,omitempty"`
|
||||
UpdatedAt *time.Time `json:"updatedAt,omitempty"`
|
||||
DataSHA256 string `json:"dataSha256,omitempty"`
|
||||
Missing bool `json:"missing,omitempty"`
|
||||
Warnings []weatherdata.SourceWarning `json:"warnings,omitempty"`
|
||||
}
|
||||
|
||||
type AlertStatus struct {
|
||||
@@ -82,7 +82,7 @@ type AlertStatus struct {
|
||||
|
||||
type BuildContext struct {
|
||||
Resolved report.Resolved
|
||||
Bundle *forecast.Bundle
|
||||
Bundle *weatherdata.Bundle
|
||||
Units string
|
||||
Timezone string
|
||||
Location *LocationContext
|
||||
@@ -125,7 +125,7 @@ func copyLocation(location *LocationContext) *LocationContext {
|
||||
return &copied
|
||||
}
|
||||
|
||||
func currentConditions(bundle *forecast.Bundle) *CurrentConditionsContext {
|
||||
func currentConditions(bundle *weatherdata.Bundle) *CurrentConditionsContext {
|
||||
if bundle == nil || bundle.Current == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -184,11 +184,11 @@ func Save(path string, pkg Package) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func sourceLocation(bundle *forecast.Bundle) (string, string) {
|
||||
func sourceLocation(bundle *weatherdata.Bundle) (string, string) {
|
||||
if bundle == nil {
|
||||
return "", ""
|
||||
}
|
||||
for _, run := range []*forecast.ForecastRun{bundle.Hourly, bundle.Narrative, bundle.Daily} {
|
||||
for _, run := range []*weatherdata.ForecastRun{bundle.Hourly, bundle.Narrative, bundle.Daily} {
|
||||
if run == nil {
|
||||
continue
|
||||
}
|
||||
@@ -199,7 +199,7 @@ func sourceLocation(bundle *forecast.Bundle) (string, string) {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
func sourceMetadata(bundle *forecast.Bundle) []SourceMetadata {
|
||||
func sourceMetadata(bundle *weatherdata.Bundle) []SourceMetadata {
|
||||
if bundle == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -219,14 +219,14 @@ func sourceMetadata(bundle *forecast.Bundle) []SourceMetadata {
|
||||
return out
|
||||
}
|
||||
|
||||
func sourceWarnings(bundle *forecast.Bundle) []forecast.SourceWarning {
|
||||
func sourceWarnings(bundle *weatherdata.Bundle) []weatherdata.SourceWarning {
|
||||
if bundle == nil {
|
||||
return nil
|
||||
}
|
||||
return bundle.Warnings
|
||||
}
|
||||
|
||||
func alertStatus(bundle *forecast.Bundle) *AlertStatus {
|
||||
func alertStatus(bundle *weatherdata.Bundle) *AlertStatus {
|
||||
if bundle == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,23 +6,24 @@ import (
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/forecast"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata"
|
||||
)
|
||||
|
||||
type Storm struct {
|
||||
TimingWindow timeutil.Period `json:"timingWindow"`
|
||||
EventHeadlines []string `json:"eventHeadlines,omitempty"`
|
||||
Hazards []string `json:"hazards,omitempty"`
|
||||
MostLikelyScenario []string `json:"mostLikelyScenario,omitempty"`
|
||||
ReasonableWorstCase []string `json:"reasonableWorstCase,omitempty"`
|
||||
ConfidenceInputs []string `json:"confidenceInputs,omitempty"`
|
||||
WhatToWatchNext []string `json:"whatToWatchNext,omitempty"`
|
||||
RelevantAlerts []forecast.AlertOverlap `json:"relevantAlerts,omitempty"`
|
||||
HourlyPeriods []forecast.ForecastPeriod `json:"hourlyPeriods,omitempty"`
|
||||
DailyPeriods []forecast.ForecastPeriod `json:"dailyPeriods,omitempty"`
|
||||
NarrativePeriods []forecast.ForecastPeriod `json:"narrativePeriods,omitempty"`
|
||||
WindowSummary forecast.DaypartSummary `json:"windowSummary"`
|
||||
Discussion DiscussionContext `json:"discussion,omitempty"`
|
||||
WeatherStory *WeatherStoryContext `json:"weatherStory,omitempty"`
|
||||
TimingWindow timeutil.Period `json:"timingWindow"`
|
||||
EventHeadlines []string `json:"eventHeadlines,omitempty"`
|
||||
Hazards []string `json:"hazards,omitempty"`
|
||||
MostLikelyScenario []string `json:"mostLikelyScenario,omitempty"`
|
||||
ReasonableWorstCase []string `json:"reasonableWorstCase,omitempty"`
|
||||
ConfidenceInputs []string `json:"confidenceInputs,omitempty"`
|
||||
WhatToWatchNext []string `json:"whatToWatchNext,omitempty"`
|
||||
RelevantAlerts []forecast.AlertOverlap `json:"relevantAlerts,omitempty"`
|
||||
HourlyPeriods []weatherdata.ForecastPeriod `json:"hourlyPeriods,omitempty"`
|
||||
DailyPeriods []weatherdata.ForecastPeriod `json:"dailyPeriods,omitempty"`
|
||||
NarrativePeriods []weatherdata.ForecastPeriod `json:"narrativePeriods,omitempty"`
|
||||
WindowSummary forecast.DaypartSummary `json:"windowSummary"`
|
||||
Discussion DiscussionContext `json:"discussion,omitempty"`
|
||||
WeatherStory *WeatherStoryContext `json:"weatherStory,omitempty"`
|
||||
}
|
||||
|
||||
func BuildStorm(ctx BuildContext) (Package, error) {
|
||||
@@ -102,7 +103,7 @@ func stormHazards(alerts []forecast.AlertOverlap, summary forecast.DaypartSummar
|
||||
return out
|
||||
}
|
||||
|
||||
func mostLikelyStormScenario(hourly []forecast.ForecastPeriod, narrative []forecast.ForecastPeriod, summary forecast.DaypartSummary) []string {
|
||||
func mostLikelyStormScenario(hourly []weatherdata.ForecastPeriod, narrative []weatherdata.ForecastPeriod, summary forecast.DaypartSummary) []string {
|
||||
var items []string
|
||||
if summary.DominantCondition != "" {
|
||||
items = append(items, "Dominant hourly condition: "+summary.DominantCondition+".")
|
||||
@@ -151,7 +152,7 @@ func reasonableWorstCase(alerts []forecast.AlertOverlap, summary forecast.Daypar
|
||||
return items
|
||||
}
|
||||
|
||||
func stormConfidenceInputs(bundle *forecast.Bundle) []string {
|
||||
func stormConfidenceInputs(bundle *weatherdata.Bundle) []string {
|
||||
var items []string
|
||||
if bundle == nil {
|
||||
return []string{"No source bundle was available for confidence context."}
|
||||
@@ -180,7 +181,7 @@ func stormConfidenceInputs(bundle *forecast.Bundle) []string {
|
||||
return items
|
||||
}
|
||||
|
||||
func stormWatchItems(alerts []forecast.AlertOverlap, summary forecast.DaypartSummary, bundle *forecast.Bundle) []string {
|
||||
func stormWatchItems(alerts []forecast.AlertOverlap, summary forecast.DaypartSummary, bundle *weatherdata.Bundle) []string {
|
||||
var items []string
|
||||
if len(alerts) > 0 {
|
||||
items = append(items, "Watch for alert extensions, cancellations, or upgrades.")
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/forecast"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata"
|
||||
)
|
||||
|
||||
func TestStormBriefingWithActiveAlert(t *testing.T) {
|
||||
@@ -23,34 +23,34 @@ func TestStormBriefingWithActiveAlert(t *testing.T) {
|
||||
}
|
||||
precip := 80.0
|
||||
gust := 42.0
|
||||
bundle := &forecast.Bundle{
|
||||
Hourly: &forecast.ForecastRun{Periods: []forecast.ForecastPeriod{{
|
||||
bundle := &weatherdata.Bundle{
|
||||
Hourly: &weatherdata.ForecastRun{Periods: []weatherdata.ForecastPeriod{{
|
||||
StartTime: mustParse("2026-05-29T07:00:00-05:00"),
|
||||
EndTime: mustParse("2026-05-29T08:00:00-05:00"),
|
||||
TextDescription: "Severe thunderstorms and gusty wind",
|
||||
ProbabilityOfPrecipitationPercent: &precip,
|
||||
WindGustMph: &gust,
|
||||
}}},
|
||||
Daily: &forecast.ForecastRun{Periods: []forecast.ForecastPeriod{{
|
||||
Daily: &weatherdata.ForecastRun{Periods: []weatherdata.ForecastPeriod{{
|
||||
StartTime: mustParse("2026-05-29T06:00:00-05:00"),
|
||||
EndTime: mustParse("2026-05-29T18:00:00-05:00"),
|
||||
TextDescription: "Storms likely.",
|
||||
}}},
|
||||
Narrative: &forecast.ForecastRun{Periods: []forecast.ForecastPeriod{{
|
||||
Narrative: &weatherdata.ForecastRun{Periods: []weatherdata.ForecastPeriod{{
|
||||
StartTime: mustParse("2026-05-29T06:00:00-05:00"),
|
||||
EndTime: mustParse("2026-05-29T18:00:00-05:00"),
|
||||
TextDescription: "Damaging wind possible in stronger storms.",
|
||||
}}},
|
||||
Alerts: &forecast.AlertRun{Alerts: []json.RawMessage{
|
||||
Alerts: &weatherdata.AlertRun{Alerts: []json.RawMessage{
|
||||
json.RawMessage(`{"event":"Severe Thunderstorm Warning","headline":"Severe storms near Testville","severity":"Severe","effective":"2026-05-29T06:30:00-05:00","expires":"2026-05-29T08:30:00-05:00"}`),
|
||||
}},
|
||||
Discussion: &forecast.Discussion{
|
||||
Discussion: &weatherdata.Discussion{
|
||||
Product: "discussion",
|
||||
KeyMessages: []string{"Storms may intensify quickly."},
|
||||
ShortTerm: &forecast.DiscussionSection{Text: "Short-term storm coverage peaks this morning."},
|
||||
LongTerm: &forecast.DiscussionSection{Text: "Long-term pattern stays unsettled after the event."},
|
||||
ShortTerm: &weatherdata.DiscussionSection{Text: "Short-term storm coverage peaks this morning."},
|
||||
LongTerm: &weatherdata.DiscussionSection{Text: "Long-term pattern stays unsettled after the event."},
|
||||
},
|
||||
WeatherStory: &forecast.WeatherStory{
|
||||
WeatherStory: &weatherdata.WeatherStory{
|
||||
OfficeID: "LSX",
|
||||
StartTime: mustParse("2026-05-29T06:00:00Z"),
|
||||
EndTime: mustParse("2026-05-29T18:00:00Z"),
|
||||
@@ -59,7 +59,7 @@ func TestStormBriefingWithActiveAlert(t *testing.T) {
|
||||
AltText: "Weather story graphic showing storm risk.",
|
||||
Order: 1,
|
||||
},
|
||||
Sources: []forecast.Source{{Name: "hourly", FetchedAt: time.Now()}},
|
||||
Sources: []weatherdata.Source{{Name: "hourly", FetchedAt: time.Now()}},
|
||||
}
|
||||
|
||||
pkg, err := BuildStorm(BuildContext{Resolved: resolved, Bundle: bundle, Units: "us", Timezone: "America/Chicago"})
|
||||
@@ -113,11 +113,11 @@ func TestStormBriefingWithDiscussionButNoAlert(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("resolve storm: %v", err)
|
||||
}
|
||||
bundle := &forecast.Bundle{
|
||||
Hourly: &forecast.ForecastRun{Periods: []forecast.ForecastPeriod{{StartTime: mustParse("2026-05-29T07:00:00-05:00"), EndTime: mustParse("2026-05-29T08:00:00-05:00"), TextDescription: "Showers"}}},
|
||||
Alerts: &forecast.AlertRun{},
|
||||
Discussion: &forecast.Discussion{Product: "discussion", KeyMessages: []string{"Confidence is moderate."}},
|
||||
Sources: []forecast.Source{{Name: "hourly", FetchedAt: time.Now()}},
|
||||
bundle := &weatherdata.Bundle{
|
||||
Hourly: &weatherdata.ForecastRun{Periods: []weatherdata.ForecastPeriod{{StartTime: mustParse("2026-05-29T07:00:00-05:00"), EndTime: mustParse("2026-05-29T08:00:00-05:00"), TextDescription: "Showers"}}},
|
||||
Alerts: &weatherdata.AlertRun{},
|
||||
Discussion: &weatherdata.Discussion{Product: "discussion", KeyMessages: []string{"Confidence is moderate."}},
|
||||
Sources: []weatherdata.Source{{Name: "hourly", FetchedAt: time.Now()}},
|
||||
}
|
||||
|
||||
pkg, err := BuildStorm(BuildContext{Resolved: resolved, Bundle: bundle, Units: "us", Timezone: "America/Chicago"})
|
||||
@@ -150,9 +150,9 @@ func TestStormBriefingQuietWindow(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("resolve storm: %v", err)
|
||||
}
|
||||
bundle := &forecast.Bundle{
|
||||
Hourly: &forecast.ForecastRun{Periods: []forecast.ForecastPeriod{{StartTime: mustParse("2026-05-29T07:00:00-05:00"), EndTime: mustParse("2026-05-29T08:00:00-05:00"), TextDescription: "Clear"}}},
|
||||
Sources: []forecast.Source{{Name: "hourly", FetchedAt: time.Now()}},
|
||||
bundle := &weatherdata.Bundle{
|
||||
Hourly: &weatherdata.ForecastRun{Periods: []weatherdata.ForecastPeriod{{StartTime: mustParse("2026-05-29T07:00:00-05:00"), EndTime: mustParse("2026-05-29T08:00:00-05:00"), TextDescription: "Clear"}}},
|
||||
Sources: []weatherdata.Source{{Name: "hourly", FetchedAt: time.Now()}},
|
||||
}
|
||||
|
||||
pkg, err := BuildStorm(BuildContext{Resolved: resolved, Bundle: bundle, Units: "us", Timezone: "America/Chicago"})
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/forecast"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata"
|
||||
)
|
||||
|
||||
func TestThreeDayBriefingBuildsOutlookDays(t *testing.T) {
|
||||
@@ -43,11 +44,11 @@ func TestThreeDayBriefingBuildsOutlookDays(t *testing.T) {
|
||||
},
|
||||
},
|
||||
AlertOverlaps: []forecast.AlertOverlap{{Event: "Flood Watch"}},
|
||||
Discussion: &forecast.Discussion{
|
||||
Discussion: &weatherdata.Discussion{
|
||||
Product: "discussion",
|
||||
KeyMessages: []string{"Unsettled stretch."},
|
||||
ShortTerm: &forecast.DiscussionSection{Text: "Short-term rain chances remain focused today."},
|
||||
LongTerm: &forecast.DiscussionSection{Text: "Long-term warmth builds into the weekend."},
|
||||
ShortTerm: &weatherdata.DiscussionSection{Text: "Short-term rain chances remain focused today."},
|
||||
LongTerm: &weatherdata.DiscussionSection{Text: "Long-term warmth builds into the weekend."},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/forecast"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata"
|
||||
)
|
||||
|
||||
type Weekend struct {
|
||||
@@ -45,7 +46,7 @@ func BuildWeekend(ctx BuildContext, summaries []forecast.DailySummary) (Package,
|
||||
return pkg, nil
|
||||
}
|
||||
|
||||
func buildWeekendPlanning(days []OutlookDay, discussion DiscussionContext, bundle *forecast.Bundle) WeekendPlanning {
|
||||
func buildWeekendPlanning(days []OutlookDay, discussion DiscussionContext, bundle *weatherdata.Bundle) WeekendPlanning {
|
||||
planning := WeekendPlanning{}
|
||||
for _, day := range days {
|
||||
if day.OutdoorWindows.Best != nil {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/forecast"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata"
|
||||
)
|
||||
|
||||
func TestWeekendBriefingBuildsPlanningInputs(t *testing.T) {
|
||||
@@ -44,7 +45,7 @@ func TestWeekendBriefingBuildsPlanningInputs(t *testing.T) {
|
||||
Time: mustParse("2026-05-30T16:00:00-05:00"),
|
||||
},
|
||||
Indicators: forecast.Indicators{Wind: true},
|
||||
HourlyPeriods: []forecast.ForecastPeriod{
|
||||
HourlyPeriods: []weatherdata.ForecastPeriod{
|
||||
{
|
||||
StartTime: mustParse("2026-05-30T15:00:00-05:00"),
|
||||
EndTime: mustParse("2026-05-30T16:00:00-05:00"),
|
||||
@@ -54,11 +55,11 @@ func TestWeekendBriefingBuildsPlanningInputs(t *testing.T) {
|
||||
},
|
||||
},
|
||||
AlertOverlaps: []forecast.AlertOverlap{{Event: "Flood Watch"}},
|
||||
Discussion: &forecast.Discussion{
|
||||
Discussion: &weatherdata.Discussion{
|
||||
Product: "discussion",
|
||||
KeyMessages: []string{"Timing may shift."},
|
||||
ShortTerm: &forecast.DiscussionSection{Text: "Short-term showers exit before the weekend."},
|
||||
LongTerm: &forecast.DiscussionSection{Text: "Long-term weekend rain timing remains uncertain."},
|
||||
ShortTerm: &weatherdata.DiscussionSection{Text: "Short-term showers exit before the weekend."},
|
||||
LongTerm: &weatherdata.DiscussionSection{Text: "Long-term weekend rain timing remains uncertain."},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata"
|
||||
)
|
||||
|
||||
type DaypartDefinition struct {
|
||||
@@ -48,6 +49,6 @@ func ResolveDayparts(date time.Time, location *time.Location, definitions []Dayp
|
||||
return windows, nil
|
||||
}
|
||||
|
||||
func PeriodForForecastPeriod(period ForecastPeriod) timeutil.Period {
|
||||
func PeriodForForecastPeriod(period weatherdata.ForecastPeriod) timeutil.Period {
|
||||
return timeutil.Period{Start: period.StartTime, End: period.EndTime}
|
||||
}
|
||||
|
||||
@@ -8,32 +8,33 @@ import (
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata"
|
||||
)
|
||||
|
||||
type DailySummary struct {
|
||||
Date string `json:"date"`
|
||||
Period timeutil.Period `json:"period"`
|
||||
Dayparts []DaypartSummary `json:"dayparts"`
|
||||
NarrativePeriods []ForecastPeriod `json:"narrativePeriods,omitempty"`
|
||||
AlertOverlaps []AlertOverlap `json:"alertOverlaps,omitempty"`
|
||||
Discussion *Discussion `json:"discussion,omitempty"`
|
||||
SourceWarnings []SourceWarning `json:"sourceWarnings,omitempty"`
|
||||
SourceProvenance []Source `json:"sourceProvenance,omitempty"`
|
||||
Date string `json:"date"`
|
||||
Period timeutil.Period `json:"period"`
|
||||
Dayparts []DaypartSummary `json:"dayparts"`
|
||||
NarrativePeriods []weatherdata.ForecastPeriod `json:"narrativePeriods,omitempty"`
|
||||
AlertOverlaps []AlertOverlap `json:"alertOverlaps,omitempty"`
|
||||
Discussion *weatherdata.Discussion `json:"discussion,omitempty"`
|
||||
SourceWarnings []weatherdata.SourceWarning `json:"sourceWarnings,omitempty"`
|
||||
SourceProvenance []weatherdata.Source `json:"sourceProvenance,omitempty"`
|
||||
}
|
||||
|
||||
type DaypartSummary struct {
|
||||
Name string `json:"name"`
|
||||
Period timeutil.Period `json:"period"`
|
||||
HourlyPeriods []ForecastPeriod `json:"hourlyPeriods"`
|
||||
Temperature Range `json:"temperature,omitempty"`
|
||||
ApparentTemperature Range `json:"apparentTemperature,omitempty"`
|
||||
MaxPrecipitationProbability *TimedValue `json:"maxPrecipitationProbability,omitempty"`
|
||||
PeakWindSpeed *TimedValue `json:"peakWindSpeed,omitempty"`
|
||||
PeakWindGust *TimedValue `json:"peakWindGust,omitempty"`
|
||||
DominantCondition string `json:"dominantCondition,omitempty"`
|
||||
NotableConditions []string `json:"notableConditions,omitempty"`
|
||||
Indicators Indicators `json:"indicators"`
|
||||
AlertOverlaps []AlertOverlap `json:"alertOverlaps,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Period timeutil.Period `json:"period"`
|
||||
HourlyPeriods []weatherdata.ForecastPeriod `json:"hourlyPeriods"`
|
||||
Temperature Range `json:"temperature,omitempty"`
|
||||
ApparentTemperature Range `json:"apparentTemperature,omitempty"`
|
||||
MaxPrecipitationProbability *TimedValue `json:"maxPrecipitationProbability,omitempty"`
|
||||
PeakWindSpeed *TimedValue `json:"peakWindSpeed,omitempty"`
|
||||
PeakWindGust *TimedValue `json:"peakWindGust,omitempty"`
|
||||
DominantCondition string `json:"dominantCondition,omitempty"`
|
||||
NotableConditions []string `json:"notableConditions,omitempty"`
|
||||
Indicators Indicators `json:"indicators"`
|
||||
AlertOverlaps []AlertOverlap `json:"alertOverlaps,omitempty"`
|
||||
}
|
||||
|
||||
type Range struct {
|
||||
@@ -64,7 +65,7 @@ type AlertOverlap struct {
|
||||
Description string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
func BuildDailySummary(bundle *Bundle, date time.Time, location *time.Location, dayparts []DaypartDefinition) (*DailySummary, error) {
|
||||
func BuildDailySummary(bundle *weatherdata.Bundle, date time.Time, location *time.Location, dayparts []DaypartDefinition) (*DailySummary, error) {
|
||||
if bundle == nil {
|
||||
return nil, fmt.Errorf("forecast bundle is required")
|
||||
}
|
||||
@@ -99,7 +100,7 @@ func BuildDailySummary(bundle *Bundle, date time.Time, location *time.Location,
|
||||
return summary, nil
|
||||
}
|
||||
|
||||
func BuildPeriodDailySummaries(bundle *Bundle, period timeutil.Period, location *time.Location, dayparts []DaypartDefinition) ([]DailySummary, error) {
|
||||
func BuildPeriodDailySummaries(bundle *weatherdata.Bundle, period timeutil.Period, location *time.Location, dayparts []DaypartDefinition) ([]DailySummary, error) {
|
||||
if !period.IsValid() {
|
||||
return nil, fmt.Errorf("valid forecast period is required")
|
||||
}
|
||||
@@ -121,7 +122,7 @@ func BuildPeriodDailySummaries(bundle *Bundle, period timeutil.Period, location
|
||||
return summaries, nil
|
||||
}
|
||||
|
||||
func buildDailySummaryForPeriod(bundle *Bundle, period timeutil.Period, location *time.Location, dayparts []DaypartDefinition) (*DailySummary, error) {
|
||||
func buildDailySummaryForPeriod(bundle *weatherdata.Bundle, period timeutil.Period, location *time.Location, dayparts []DaypartDefinition) (*DailySummary, error) {
|
||||
if bundle == nil {
|
||||
return nil, fmt.Errorf("forecast bundle is required")
|
||||
}
|
||||
@@ -155,11 +156,11 @@ func buildDailySummaryForPeriod(bundle *Bundle, period timeutil.Period, location
|
||||
return summary, nil
|
||||
}
|
||||
|
||||
func SelectHourlyPeriods(run *ForecastRun, period timeutil.Period) []ForecastPeriod {
|
||||
func SelectHourlyPeriods(run *weatherdata.ForecastRun, period timeutil.Period) []weatherdata.ForecastPeriod {
|
||||
if run == nil {
|
||||
return nil
|
||||
}
|
||||
var selected []ForecastPeriod
|
||||
var selected []weatherdata.ForecastPeriod
|
||||
for _, forecastPeriod := range run.Periods {
|
||||
if PeriodForForecastPeriod(forecastPeriod).Overlaps(period) {
|
||||
selected = append(selected, forecastPeriod)
|
||||
@@ -171,21 +172,21 @@ func SelectHourlyPeriods(run *ForecastRun, period timeutil.Period) []ForecastPer
|
||||
return selected
|
||||
}
|
||||
|
||||
func SelectNarrativePeriods(bundle *Bundle, period timeutil.Period) []ForecastPeriod {
|
||||
func SelectNarrativePeriods(bundle *weatherdata.Bundle, period timeutil.Period) []weatherdata.ForecastPeriod {
|
||||
if bundle == nil || bundle.Narrative == nil {
|
||||
return nil
|
||||
}
|
||||
return SelectHourlyPeriods(bundle.Narrative, period)
|
||||
}
|
||||
|
||||
func SelectDiscussion(bundle *Bundle) *Discussion {
|
||||
func SelectDiscussion(bundle *weatherdata.Bundle) *weatherdata.Discussion {
|
||||
if bundle == nil {
|
||||
return nil
|
||||
}
|
||||
return bundle.Discussion
|
||||
}
|
||||
|
||||
func SummarizeDaypart(name string, period timeutil.Period, periods []ForecastPeriod) DaypartSummary {
|
||||
func SummarizeDaypart(name string, period timeutil.Period, periods []weatherdata.ForecastPeriod) DaypartSummary {
|
||||
summary := DaypartSummary{
|
||||
Name: name,
|
||||
Period: period,
|
||||
@@ -215,7 +216,7 @@ func SummarizeDaypart(name string, period timeutil.Period, periods []ForecastPer
|
||||
return summary
|
||||
}
|
||||
|
||||
func periodTemperatureValues(period ForecastPeriod) []*float64 {
|
||||
func periodTemperatureValues(period weatherdata.ForecastPeriod) []*float64 {
|
||||
values := []*float64{}
|
||||
values = append(values, valueFromPointers(period.TemperatureF, period.TemperatureC)...)
|
||||
values = append(values, valueFromPointers(period.TemperatureFMin, period.TemperatureCMin)...)
|
||||
@@ -297,7 +298,7 @@ func indicatorsForText(text string) Indicators {
|
||||
}
|
||||
}
|
||||
|
||||
func numericIndicators(period ForecastPeriod) Indicators {
|
||||
func numericIndicators(period weatherdata.ForecastPeriod) Indicators {
|
||||
windGust := firstValue(period.WindGustMph, period.WindGustKmh)
|
||||
windSpeed := firstValue(period.WindSpeedMph, period.WindSpeedKmh)
|
||||
indicators := Indicators{}
|
||||
@@ -325,7 +326,7 @@ func mergeIndicators(left Indicators, right Indicators) Indicators {
|
||||
}
|
||||
}
|
||||
|
||||
func AlertOverlaps(alertRun *AlertRun, period timeutil.Period) []AlertOverlap {
|
||||
func AlertOverlaps(alertRun *weatherdata.AlertRun, period timeutil.Period) []AlertOverlap {
|
||||
if alertRun == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata"
|
||||
)
|
||||
|
||||
func TestBuildDailySummaryGroupsDaypartsAndComputesMetrics(t *testing.T) {
|
||||
@@ -72,7 +73,7 @@ func TestBuildDailySummaryFromFixtureBundle(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("read fixture bundle: %v", err)
|
||||
}
|
||||
var bundle Bundle
|
||||
var bundle weatherdata.Bundle
|
||||
if err := json.Unmarshal(data, &bundle); err != nil {
|
||||
t.Fatalf("decode fixture bundle: %v", err)
|
||||
}
|
||||
@@ -98,7 +99,7 @@ func TestBuildDailySummaryFromFixtureBundle(t *testing.T) {
|
||||
func TestOvernightGroupingAcrossMidnight(t *testing.T) {
|
||||
location := time.FixedZone("Test", -5*60*60)
|
||||
date := time.Date(2026, 5, 29, 12, 0, 0, 0, location)
|
||||
bundle := &Bundle{Hourly: &ForecastRun{Periods: []ForecastPeriod{
|
||||
bundle := &weatherdata.Bundle{Hourly: &weatherdata.ForecastRun{Periods: []weatherdata.ForecastPeriod{
|
||||
hour(location, "2026-05-29T23:00:00-05:00", "2026-05-30T00:00:00-05:00", "Snow", 31, nil, nil, nil, nil),
|
||||
hour(location, "2026-05-30T05:00:00-05:00", "2026-05-30T06:00:00-05:00", "Fog", 30, nil, nil, nil, nil),
|
||||
hour(location, "2026-05-30T06:00:00-05:00", "2026-05-30T07:00:00-05:00", "Clear", 35, nil, nil, nil, nil),
|
||||
@@ -122,7 +123,7 @@ func TestOvernightGroupingAcrossMidnight(t *testing.T) {
|
||||
func TestBoundaryTimestampsAtDaypartEdges(t *testing.T) {
|
||||
location := time.FixedZone("Test", -5*60*60)
|
||||
date := time.Date(2026, 5, 29, 12, 0, 0, 0, location)
|
||||
bundle := &Bundle{Hourly: &ForecastRun{Periods: []ForecastPeriod{
|
||||
bundle := &weatherdata.Bundle{Hourly: &weatherdata.ForecastRun{Periods: []weatherdata.ForecastPeriod{
|
||||
hour(location, "2026-05-29T05:00:00-05:00", "2026-05-29T06:00:00-05:00", "Before", 55, nil, nil, nil, nil),
|
||||
hour(location, "2026-05-29T06:00:00-05:00", "2026-05-29T07:00:00-05:00", "Start", 56, nil, nil, nil, nil),
|
||||
hour(location, "2026-05-29T12:00:00-05:00", "2026-05-29T13:00:00-05:00", "After", 70, nil, nil, nil, nil),
|
||||
@@ -145,7 +146,7 @@ func TestBoundaryTimestampsAtDaypartEdges(t *testing.T) {
|
||||
|
||||
func TestBuildDailySummaryRequiresHourlyData(t *testing.T) {
|
||||
location := time.UTC
|
||||
_, err := BuildDailySummary(&Bundle{}, time.Now(), location, []DaypartDefinition{
|
||||
_, err := BuildDailySummary(&weatherdata.Bundle{}, time.Now(), location, []DaypartDefinition{
|
||||
{Name: "morning", Start: "06:00", End: "12:00"},
|
||||
})
|
||||
if err == nil {
|
||||
@@ -155,7 +156,7 @@ func TestBuildDailySummaryRequiresHourlyData(t *testing.T) {
|
||||
|
||||
func TestBuildPeriodDailySummariesClipsPartialDays(t *testing.T) {
|
||||
location := time.FixedZone("Test", -5*60*60)
|
||||
bundle := &Bundle{Hourly: &ForecastRun{Periods: []ForecastPeriod{
|
||||
bundle := &weatherdata.Bundle{Hourly: &weatherdata.ForecastRun{Periods: []weatherdata.ForecastPeriod{
|
||||
hour(location, "2026-05-29T05:00:00-05:00", "2026-05-29T06:00:00-05:00", "Before", 50, nil, nil, nil, nil),
|
||||
hour(location, "2026-05-29T08:00:00-05:00", "2026-05-29T09:00:00-05:00", "Showers", 60, nil, ptr(60), nil, nil),
|
||||
hour(location, "2026-05-30T14:00:00-05:00", "2026-05-30T15:00:00-05:00", "Hot", 95, nil, nil, nil, nil),
|
||||
@@ -191,7 +192,7 @@ func TestBuildPeriodDailySummariesClipsPartialDays(t *testing.T) {
|
||||
func TestAlertOverlap(t *testing.T) {
|
||||
location := time.FixedZone("Test", -5*60*60)
|
||||
raw := json.RawMessage(`{"event":"Flood Watch","headline":"Flooding possible","severity":"Moderate","effective":"2026-05-29T07:00:00-05:00","expires":"2026-05-29T10:00:00-05:00"}`)
|
||||
alertRun := &AlertRun{Alerts: []json.RawMessage{raw}}
|
||||
alertRun := &weatherdata.AlertRun{Alerts: []json.RawMessage{raw}}
|
||||
period := timeutil.Period{
|
||||
Start: mustParse("2026-05-29T06:00:00-05:00").In(location),
|
||||
End: mustParse("2026-05-29T09:00:00-05:00").In(location),
|
||||
@@ -221,31 +222,31 @@ func TestThresholdHelpers(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func testBundle(location *time.Location) *Bundle {
|
||||
return &Bundle{
|
||||
Hourly: &ForecastRun{Periods: []ForecastPeriod{
|
||||
func testBundle(location *time.Location) *weatherdata.Bundle {
|
||||
return &weatherdata.Bundle{
|
||||
Hourly: &weatherdata.ForecastRun{Periods: []weatherdata.ForecastPeriod{
|
||||
hour(location, "2026-05-29T05:00:00-05:00", "2026-05-29T06:00:00-05:00", "Cloudy", 55, nil, nil, nil, nil),
|
||||
hour(location, "2026-05-29T06:00:00-05:00", "2026-05-29T07:00:00-05:00", "Thunderstorms and gusty wind", 58, ptr(57), ptr(70), ptr(22), ptr(40)),
|
||||
hour(location, "2026-05-29T11:00:00-05:00", "2026-05-29T12:00:00-05:00", "Thunderstorms and gusty wind", 72, ptr(74), ptr(60), ptr(18), ptr(35)),
|
||||
hour(location, "2026-05-29T14:00:00-05:00", "2026-05-29T15:00:00-05:00", "Hot and sunny", 96, ptr(100), ptr(5), ptr(10), ptr(12)),
|
||||
}},
|
||||
Narrative: &ForecastRun{Periods: []ForecastPeriod{
|
||||
Narrative: &weatherdata.ForecastRun{Periods: []weatherdata.ForecastPeriod{
|
||||
hour(location, "2026-05-29T06:00:00-05:00", "2026-05-29T18:00:00-05:00", "Storms early, hot later.", 96, nil, nil, nil, nil),
|
||||
}},
|
||||
Alerts: &AlertRun{Alerts: []json.RawMessage{
|
||||
Alerts: &weatherdata.AlertRun{Alerts: []json.RawMessage{
|
||||
json.RawMessage(`{"event":"Severe Thunderstorm Watch","headline":"Storms possible","severity":"Severe","effective":"2026-05-29T06:30:00-05:00","expires":"2026-05-29T11:30:00-05:00"}`),
|
||||
}},
|
||||
Discussion: &Discussion{Product: "discussion", KeyMessages: []string{"Storms possible."}},
|
||||
Sources: []Source{{Name: "hourly"}},
|
||||
Warnings: []SourceWarning{{Source: "daily", Code: "missing_source"}},
|
||||
Discussion: &weatherdata.Discussion{Product: "discussion", KeyMessages: []string{"Storms possible."}},
|
||||
Sources: []weatherdata.Source{{Name: "hourly"}},
|
||||
Warnings: []weatherdata.SourceWarning{{Source: "daily", Code: "missing_source"}},
|
||||
}
|
||||
}
|
||||
|
||||
func hour(location *time.Location, start string, end string, text string, temperature float64, apparent *float64, precip *float64, wind *float64, gust *float64) ForecastPeriod {
|
||||
func hour(location *time.Location, start string, end string, text string, temperature float64, apparent *float64, precip *float64, wind *float64, gust *float64) weatherdata.ForecastPeriod {
|
||||
startTime := mustParse(start).In(location)
|
||||
endTime := mustParse(end).In(location)
|
||||
temp := temperature
|
||||
return ForecastPeriod{
|
||||
return weatherdata.ForecastPeriod{
|
||||
StartTime: startTime,
|
||||
EndTime: endTime,
|
||||
TextDescription: text,
|
||||
|
||||
@@ -8,20 +8,20 @@ import (
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/briefing"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/changes"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/fileutil"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/forecast"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata"
|
||||
)
|
||||
|
||||
const SchemaVersion = "weatherreporter.data_package.v1"
|
||||
|
||||
type Package struct {
|
||||
SchemaVersion string `json:"schemaVersion"`
|
||||
RunID string `json:"runId"`
|
||||
Report Report `json:"report"`
|
||||
Briefing briefing.Package `json:"briefing"`
|
||||
RecentChanges RecentChanges `json:"recentChanges"`
|
||||
SourceWarnings []forecast.SourceWarning `json:"sourceWarnings,omitempty"`
|
||||
SchemaVersion string `json:"schemaVersion"`
|
||||
RunID string `json:"runId"`
|
||||
Report Report `json:"report"`
|
||||
Briefing briefing.Package `json:"briefing"`
|
||||
RecentChanges RecentChanges `json:"recentChanges"`
|
||||
SourceWarnings []weatherdata.SourceWarning `json:"sourceWarnings,omitempty"`
|
||||
}
|
||||
|
||||
type Report struct {
|
||||
|
||||
@@ -4,33 +4,33 @@ import (
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/briefing"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/forecast"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata"
|
||||
)
|
||||
|
||||
const MetadataSchemaVersion = "weatherreporter.metadata.v1"
|
||||
|
||||
type Metadata struct {
|
||||
SchemaVersion string `json:"schemaVersion"`
|
||||
RunID string `json:"runId"`
|
||||
MetadataPath string `json:"-"`
|
||||
ReportID report.ID `json:"reportId"`
|
||||
Variant string `json:"variant,omitempty"`
|
||||
PromptID string `json:"promptId"`
|
||||
GeneratedAt time.Time `json:"generatedAt"`
|
||||
Timezone string `json:"timezone"`
|
||||
ValidPeriod timeutil.Period `json:"validPeriod"`
|
||||
Location *briefing.LocationContext `json:"location,omitempty"`
|
||||
SourceLocationID string `json:"sourceLocationId,omitempty"`
|
||||
SourceLocation string `json:"sourceLocation,omitempty"`
|
||||
Sources []briefing.SourceMetadata `json:"sources,omitempty"`
|
||||
SourceWarnings []forecast.SourceWarning `json:"sourceWarnings,omitempty"`
|
||||
BriefingPath string `json:"briefingPath"`
|
||||
DataPackagePath string `json:"dataPackagePath"`
|
||||
PreflightPath string `json:"preflightPath"`
|
||||
NotificationPath string `json:"notificationPath,omitempty"`
|
||||
RenderedReportPath string `json:"renderedReportPath,omitempty"`
|
||||
SchemaVersion string `json:"schemaVersion"`
|
||||
RunID string `json:"runId"`
|
||||
MetadataPath string `json:"-"`
|
||||
ReportID report.ID `json:"reportId"`
|
||||
Variant string `json:"variant,omitempty"`
|
||||
PromptID string `json:"promptId"`
|
||||
GeneratedAt time.Time `json:"generatedAt"`
|
||||
Timezone string `json:"timezone"`
|
||||
ValidPeriod timeutil.Period `json:"validPeriod"`
|
||||
Location *briefing.LocationContext `json:"location,omitempty"`
|
||||
SourceLocationID string `json:"sourceLocationId,omitempty"`
|
||||
SourceLocation string `json:"sourceLocation,omitempty"`
|
||||
Sources []briefing.SourceMetadata `json:"sources,omitempty"`
|
||||
SourceWarnings []weatherdata.SourceWarning `json:"sourceWarnings,omitempty"`
|
||||
BriefingPath string `json:"briefingPath"`
|
||||
DataPackagePath string `json:"dataPackagePath"`
|
||||
PreflightPath string `json:"preflightPath"`
|
||||
NotificationPath string `json:"notificationPath,omitempty"`
|
||||
RenderedReportPath string `json:"renderedReportPath,omitempty"`
|
||||
}
|
||||
|
||||
func BuildMetadata(resolved report.Resolved, briefingPackage briefing.Package, paths ArtifactPaths) Metadata {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Package forecast defines normalized weather data consumed by report builders.
|
||||
package forecast
|
||||
// Package weatherdata defines normalized weather data collected from sources.
|
||||
package weatherdata
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
Reference in New Issue
Block a user