324 lines
8.9 KiB
Go
324 lines
8.9 KiB
Go
// Package app owns application orchestration and top-level use cases.
|
|
package app
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/adapters/scriptorium"
|
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/adapters/weatherapi"
|
|
"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/timeutil"
|
|
)
|
|
|
|
type ReportKind string
|
|
|
|
const (
|
|
ReportDaily ReportKind = "daily"
|
|
ReportTomorrow ReportKind = "tomorrow"
|
|
ReportThreeDay ReportKind = "three-day"
|
|
ReportWeekend ReportKind = "weekend"
|
|
ReportStorm ReportKind = "storm"
|
|
)
|
|
|
|
type BatchKind string
|
|
|
|
const (
|
|
BatchMorning BatchKind = "morning"
|
|
BatchEvening BatchKind = "evening"
|
|
)
|
|
|
|
type GenerateRequest struct {
|
|
Config config.Config
|
|
Report ReportKind
|
|
OutputPath string
|
|
Date time.Time
|
|
StormStart time.Time
|
|
StormEnd time.Time
|
|
}
|
|
|
|
type BatchRequest struct {
|
|
Config config.Config
|
|
Batch BatchKind
|
|
}
|
|
|
|
type FetchBundleRequest struct {
|
|
Config config.Config
|
|
OutputPath string
|
|
}
|
|
|
|
type DailyBriefingRequest struct {
|
|
Config config.Config
|
|
Resolved report.Resolved
|
|
OutputPath string
|
|
}
|
|
|
|
type DailyPreparationRequest struct {
|
|
Config config.Config
|
|
Resolved report.Resolved
|
|
DataPackagePath string
|
|
Renderer Renderer
|
|
}
|
|
|
|
type DailyBriefingResult struct {
|
|
Package briefing.Package
|
|
OutputPath string
|
|
}
|
|
|
|
type DailyPreparationResult struct {
|
|
Briefing briefing.Package
|
|
BriefingPath string
|
|
DataPackage promptinput.Package
|
|
DataPackagePath string
|
|
PreflightPath string
|
|
RenderResult *scriptorium.RenderResult
|
|
}
|
|
|
|
type Renderer interface {
|
|
Render(context.Context, scriptorium.RenderRequest) (*scriptorium.RenderResult, error)
|
|
}
|
|
|
|
func Generate(ctx context.Context, req GenerateRequest) error {
|
|
resolved, err := ResolveGenerate(req, time.Now())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if resolved.Definition.ID == report.DailyToday {
|
|
_, err := PrepareDailyReport(ctx, DailyPreparationRequest{
|
|
Config: req.Config,
|
|
Resolved: resolved,
|
|
DataPackagePath: req.OutputPath,
|
|
})
|
|
return err
|
|
}
|
|
return fmt.Errorf("generate is not implemented")
|
|
}
|
|
|
|
func RunBatch(ctx context.Context, req BatchRequest) error {
|
|
_ = ctx
|
|
if _, err := ResolveBatch(req, time.Now()); err != nil {
|
|
return err
|
|
}
|
|
return fmt.Errorf("run is not implemented")
|
|
}
|
|
|
|
func ResolveGenerate(req GenerateRequest, now time.Time) (report.Resolved, error) {
|
|
location, err := timeutil.LoadLocation(req.Config.WeatherAPI.Timezone)
|
|
if err != nil {
|
|
return report.Resolved{}, err
|
|
}
|
|
id, err := reportIDForCommand(req.Report)
|
|
if err != nil {
|
|
return report.Resolved{}, err
|
|
}
|
|
return report.DefaultRegistry().Resolve(id, report.ResolveRequest{
|
|
Now: now,
|
|
Location: location,
|
|
Date: req.Date,
|
|
StormStart: req.StormStart,
|
|
StormEnd: req.StormEnd,
|
|
})
|
|
}
|
|
|
|
func ResolveBatch(req BatchRequest, now time.Time) ([]report.Resolved, error) {
|
|
location, err := timeutil.LoadLocation(req.Config.WeatherAPI.Timezone)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
batch, err := reportBatchForCommand(req.Batch)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return report.DefaultRegistry().BatchReports(batch, report.ResolveRequest{
|
|
Now: now,
|
|
Location: location,
|
|
})
|
|
}
|
|
|
|
func reportIDForCommand(kind ReportKind) (report.ID, error) {
|
|
switch kind {
|
|
case ReportDaily:
|
|
return report.DailyToday, nil
|
|
case ReportTomorrow:
|
|
return report.DailyTomorrow, nil
|
|
case ReportThreeDay:
|
|
return report.ThreeDay, nil
|
|
case ReportWeekend:
|
|
return report.Weekend, nil
|
|
case ReportStorm:
|
|
return report.Storm, nil
|
|
default:
|
|
return "", fmt.Errorf("unknown report command %q", kind)
|
|
}
|
|
}
|
|
|
|
func reportBatchForCommand(kind BatchKind) (report.Batch, error) {
|
|
switch kind {
|
|
case BatchMorning:
|
|
return report.Morning, nil
|
|
case BatchEvening:
|
|
return report.Evening, nil
|
|
default:
|
|
return "", fmt.Errorf("unknown batch command %q", kind)
|
|
}
|
|
}
|
|
|
|
func FetchBundle(ctx context.Context, req FetchBundleRequest) (*forecast.Bundle, error) {
|
|
client, err := weatherapi.New(req.Config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bundle, err := client.FetchBundle(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return bundle, nil
|
|
}
|
|
|
|
func FetchAndSaveBundle(ctx context.Context, req FetchBundleRequest) (*forecast.Bundle, error) {
|
|
if req.OutputPath == "" {
|
|
return nil, fmt.Errorf("output path is required")
|
|
}
|
|
bundle, err := FetchBundle(ctx, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := weatherapi.SaveBundle(req.OutputPath, bundle); err != nil {
|
|
return nil, err
|
|
}
|
|
return bundle, nil
|
|
}
|
|
|
|
func GenerateDailyBriefing(ctx context.Context, req DailyBriefingRequest) (*DailyBriefingResult, error) {
|
|
bundle, err := FetchBundle(ctx, FetchBundleRequest{Config: req.Config})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pkg, err := BuildDailyBriefing(req, bundle)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
outputPath := req.OutputPath
|
|
if outputPath == "" {
|
|
outputPath = defaultBriefingPath(req.Config, req.Resolved)
|
|
}
|
|
if err := briefing.Save(outputPath, pkg); err != nil {
|
|
return nil, err
|
|
}
|
|
return &DailyBriefingResult{Package: pkg, OutputPath: outputPath}, nil
|
|
}
|
|
|
|
func PrepareDailyReport(ctx context.Context, req DailyPreparationRequest) (*DailyPreparationResult, error) {
|
|
bundle, err := FetchBundle(ctx, FetchBundleRequest{Config: req.Config})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
briefingPackage, err := BuildDailyBriefing(DailyBriefingRequest{
|
|
Config: req.Config,
|
|
Resolved: req.Resolved,
|
|
}, bundle)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
briefingPath := defaultBriefingPath(req.Config, req.Resolved)
|
|
if err := briefing.Save(briefingPath, briefingPackage); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
dataPackage, err := promptinput.Build(briefingPackage)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dataPackagePath := req.DataPackagePath
|
|
if dataPackagePath == "" {
|
|
dataPackagePath = defaultDataPackagePath(req.Config, req.Resolved)
|
|
}
|
|
if err := promptinput.Save(dataPackagePath, dataPackage); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
renderer := req.Renderer
|
|
if renderer == nil {
|
|
renderer = scriptorium.Runner{
|
|
Binary: req.Config.Scriptorium.Binary,
|
|
ConfigPath: req.Config.Scriptorium.ConfigPath,
|
|
Profile: req.Config.Scriptorium.Profile,
|
|
Timeout: req.Config.Scriptorium.Timeout,
|
|
ExtraArgs: req.Config.Scriptorium.ExtraArgs,
|
|
}
|
|
}
|
|
renderResult, renderErr := renderer.Render(ctx, scriptorium.RenderRequest{
|
|
PromptID: req.Resolved.Definition.PromptID,
|
|
DataPackagePath: dataPackagePath,
|
|
})
|
|
|
|
preflightPath := defaultPreflightPath(req.Config, req.Resolved)
|
|
if renderResult != nil {
|
|
if err := scriptorium.SaveRenderResult(preflightPath, renderResult); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if renderErr != nil {
|
|
return nil, renderErr
|
|
}
|
|
|
|
return &DailyPreparationResult{
|
|
Briefing: briefingPackage,
|
|
BriefingPath: briefingPath,
|
|
DataPackage: dataPackage,
|
|
DataPackagePath: dataPackagePath,
|
|
PreflightPath: preflightPath,
|
|
RenderResult: renderResult,
|
|
}, nil
|
|
}
|
|
|
|
func BuildDailyBriefing(req DailyBriefingRequest, bundle *forecast.Bundle) (briefing.Package, error) {
|
|
location, err := timeutil.LoadLocation(req.Config.WeatherAPI.Timezone)
|
|
if err != nil {
|
|
return briefing.Package{}, err
|
|
}
|
|
dayparts := make([]forecast.DaypartDefinition, 0, len(req.Config.Dayparts))
|
|
for _, daypart := range req.Config.Dayparts {
|
|
dayparts = append(dayparts, forecast.DaypartDefinition{
|
|
Name: daypart.Name,
|
|
Start: daypart.Start,
|
|
End: daypart.End,
|
|
})
|
|
}
|
|
summary, err := forecast.BuildDailySummary(bundle, req.Resolved.ValidPeriod.Start, location, dayparts)
|
|
if err != nil {
|
|
return briefing.Package{}, err
|
|
}
|
|
return briefing.BuildDaily(briefing.BuildContext{
|
|
Resolved: req.Resolved,
|
|
Bundle: bundle,
|
|
Units: req.Config.WeatherAPI.Units,
|
|
Timezone: req.Config.WeatherAPI.Timezone,
|
|
}, summary)
|
|
}
|
|
|
|
func defaultBriefingPath(cfg config.Config, resolved report.Resolved) string {
|
|
validDate := resolved.ValidPeriod.Start.Format("2006-01-02")
|
|
filename := validDate + "." + string(resolved.Definition.ID) + ".briefing.json"
|
|
return filepath.Join(cfg.Workspace.Root, cfg.Workspace.SnapshotsDir, "daily", validDate, filename)
|
|
}
|
|
|
|
func defaultDataPackagePath(cfg config.Config, resolved report.Resolved) string {
|
|
validDate := resolved.ValidPeriod.Start.Format("2006-01-02")
|
|
filename := resolved.Metadata().RunID + ".data_package.json"
|
|
return filepath.Join(cfg.Workspace.Root, cfg.Workspace.DataPackagesDir, "daily", validDate, filename)
|
|
}
|
|
|
|
func defaultPreflightPath(cfg config.Config, resolved report.Resolved) string {
|
|
validDate := resolved.ValidPeriod.Start.Format("2006-01-02")
|
|
filename := resolved.Metadata().RunID + ".render.json"
|
|
return filepath.Join(cfg.Workspace.Root, cfg.Workspace.PreflightDir, "daily", validDate, filename)
|
|
}
|