From 66875b327e24cea79883e5cf3fe2b3f9f3417675 Mon Sep 17 00:00:00 2001 From: Luke Granger-Brown Date: Thu, 18 Nov 2021 22:24:20 +0000 Subject: [PATCH] go/trains: init --- ci-root.nix | 1 + go/default.nix | 1 + go/trains/cmd/darwiningestd/darwiningestd.go | 221 ++++ go/trains/cmd/darwiningestd/default.nix | 24 + go/trains/cmd/db2web/db2web.go | 463 +++++++++ go/trains/cmd/db2web/default.nix | 15 + go/trains/cmd/default.nix | 17 + go/trains/cmd/rttingest/default.nix | 15 + go/trains/cmd/rttingest/tiploc.go | 131 +++ go/trains/cmd/train2livesplit/default.nix | 12 + .../cmd/train2livesplit/train2livesplit.go | 247 +++++ go/trains/darwin/darwindb/_schema.sql | 309 ++++++ go/trains/darwin/darwindb/affected.go | 65 ++ go/trains/darwin/darwindb/darwindb.go | 71 ++ go/trains/darwin/darwindb/ddbref.go | 103 ++ go/trains/darwin/darwindb/ddbschedule.go | 112 ++ go/trains/darwin/darwindb/ddbtrainstatus.go | 130 +++ go/trains/darwin/darwindb/default.nix | 21 + go/trains/darwin/darwindb/tsutil.go | 74 ++ go/trains/darwin/darwingest/darwingest.go | 310 ++++++ .../darwingest/darwingestftp/darwingestftp.go | 169 +++ .../darwingest/darwingestftp/default.nix | 17 + .../darwingest/darwingests3/darwingests3.go | 214 ++++ .../darwingest/darwingests3/default.nix | 17 + .../darwingeststomp/darwingeststomp.go | 34 + .../darwingest/darwingeststomp/default.nix | 17 + go/trains/darwin/darwingest/default.nix | 27 + go/trains/darwin/default.nix | 18 + go/trains/darwin/msg.go | 436 ++++++++ go/trains/darwin/refdata.go | 45 + go/trains/darwin/timezone.go | 15 + go/trains/darwin/util.go | 18 + go/trains/default.nix | 11 + go/trains/go.mod | 38 + go/trains/go.sum | 959 ++++++++++++++++++ go/trains/webapi/default.nix | 12 + go/trains/webapi/structs.go | 197 ++++ .../cenkalti/backoff/v4/default.nix | 14 + .../github.com/go-stomp/stomp/v3/default.nix | 14 + .../github.com/jlaffaye/ftp/default.nix | 14 + 40 files changed, 4628 insertions(+) create mode 100644 go/trains/cmd/darwiningestd/darwiningestd.go create mode 100644 go/trains/cmd/darwiningestd/default.nix create mode 100644 go/trains/cmd/db2web/db2web.go create mode 100644 go/trains/cmd/db2web/default.nix create mode 100644 go/trains/cmd/default.nix create mode 100644 go/trains/cmd/rttingest/default.nix create mode 100644 go/trains/cmd/rttingest/tiploc.go create mode 100644 go/trains/cmd/train2livesplit/default.nix create mode 100644 go/trains/cmd/train2livesplit/train2livesplit.go create mode 100644 go/trains/darwin/darwindb/_schema.sql create mode 100644 go/trains/darwin/darwindb/affected.go create mode 100644 go/trains/darwin/darwindb/darwindb.go create mode 100644 go/trains/darwin/darwindb/ddbref.go create mode 100644 go/trains/darwin/darwindb/ddbschedule.go create mode 100644 go/trains/darwin/darwindb/ddbtrainstatus.go create mode 100644 go/trains/darwin/darwindb/default.nix create mode 100644 go/trains/darwin/darwindb/tsutil.go create mode 100644 go/trains/darwin/darwingest/darwingest.go create mode 100644 go/trains/darwin/darwingest/darwingestftp/darwingestftp.go create mode 100644 go/trains/darwin/darwingest/darwingestftp/default.nix create mode 100644 go/trains/darwin/darwingest/darwingests3/darwingests3.go create mode 100644 go/trains/darwin/darwingest/darwingests3/default.nix create mode 100644 go/trains/darwin/darwingest/darwingeststomp/darwingeststomp.go create mode 100644 go/trains/darwin/darwingest/darwingeststomp/default.nix create mode 100644 go/trains/darwin/darwingest/default.nix create mode 100644 go/trains/darwin/default.nix create mode 100644 go/trains/darwin/msg.go create mode 100644 go/trains/darwin/refdata.go create mode 100644 go/trains/darwin/timezone.go create mode 100644 go/trains/darwin/util.go create mode 100644 go/trains/default.nix create mode 100644 go/trains/go.mod create mode 100644 go/trains/go.sum create mode 100644 go/trains/webapi/default.nix create mode 100644 go/trains/webapi/structs.go create mode 100644 third_party/gopkgs/github.com/cenkalti/backoff/v4/default.nix create mode 100644 third_party/gopkgs/github.com/go-stomp/stomp/v3/default.nix create mode 100644 third_party/gopkgs/github.com/jlaffaye/ftp/default.nix diff --git a/ci-root.nix b/ci-root.nix index 49751b2029..4aa6512368 100644 --- a/ci-root.nix +++ b/ci-root.nix @@ -15,6 +15,7 @@ let pkgs = builtins.removeAttrs depot.nix.pkgs [ "grafana-plugins" "windows" "unifiHacked" "nixpkgs-mozilla" "nightlyRust" "nightlyRustPlatform" ]; pkg-grafana-plugins = depot.nix.pkgs.grafana-plugins; web = lib.filterAttrs (n: v: !lib.isFunction v) depot.web; + trains = depot.go.trains.cmd.bins; other = { twitterchiver-archiver = depot.go.twitterchiver.archiver; twitterchiver-archiver-docker = depot.go.twitterchiver.archiver.dockerImage; diff --git a/go/default.nix b/go/default.nix index a3a089f017..f7f6249017 100644 --- a/go/default.nix +++ b/go/default.nix @@ -7,4 +7,5 @@ args: { twitternuke = import ./twitternuke args; minotarproxy = import ./minotarproxy args; streetworks = import ./streetworks args; + trains = import ./trains args; } diff --git a/go/trains/cmd/darwiningestd/darwiningestd.go b/go/trains/cmd/darwiningestd/darwiningestd.go new file mode 100644 index 0000000000..fdde5be560 --- /dev/null +++ b/go/trains/cmd/darwiningestd/darwiningestd.go @@ -0,0 +1,221 @@ +package main + +import ( + "context" + "flag" + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + awscreds "github.com/aws/aws-sdk-go/aws/credentials" + awssession "github.com/aws/aws-sdk-go/aws/session" + "github.com/go-stomp/stomp/v3" + pgx "github.com/jackc/pgx/v4" + "github.com/jlaffaye/ftp" + "gocloud.dev/blob" + "gocloud.dev/blob/s3blob" + "hg.lukegb.com/lukegb/depot/go/trains/darwin" + "hg.lukegb.com/lukegb/depot/go/trains/darwin/darwindb" + "hg.lukegb.com/lukegb/depot/go/trains/darwin/darwingest" + "hg.lukegb.com/lukegb/depot/go/trains/darwin/darwingest/darwingeststomp" +) + +var ( + cleanStart = flag.Bool("clean_start", false, "Start by downloading all timetabling and reference data and restart from snapshot") + + awsAccessKey = requiredString("s3_access_key", "Darwin XML timetable S3 access key") + awsAccessSecret = requiredString("s3_access_secret", "Darwin XML timetable S3 access secret") + awsBucket = flag.String("s3_bucket", "darwin.xmltimetable", "Darwin XML timetable S3 bucket") + awsPrefix = flag.String("s3_prefix", "PPTimetable/", "Darwin XML timetable S3 bucket key prefix") + + postgresString = flag.String("pg_string", "host=/var/run/postgresql database=trains search_path=darwindb", "String to connect to PG") + + stompHostport = requiredString("stomp_address", "Darwin STOMP address (host:port)") + stompUsername = requiredString("stomp_username", "Darwin STOMP username") + stompPassword = requiredString("stomp_password", "Darwin STOMP password") + stompClientID = requiredString("stomp_client_id", "Darwin STOMP client ID") + stompTopic = flag.String("stomp_topic", "/topic/darwin.pushport-v16", "Darwin STOMP topic") + + ftpHostport = requiredString("ftp_address", "Darwin FTP address (host:port)") + ftpUsername = requiredString("ftp_username", "Darwin FTP username") + ftpPassword = requiredString("ftp_password", "Darwin FTP password") + + requiredFlags []string +) + +func requiredString(name, descr string) *string { + requireFlag(name) + return flag.String(name, "", descr) +} + +func requireFlag(names ...string) { + requiredFlags = append(requiredFlags, names...) +} + +func checkRequiredFlags() { + for _, fname := range requiredFlags { + f := flag.CommandLine.Lookup(fname) + if f == nil { + log.Fatalf("required flag %q doesn't actually exist", fname) + } + v := f.Value.(flag.Getter).Get() + if v == nil { + log.Fatalf("required flag %q not set", fname) + } + if v, ok := v.(string); ok && v == "" { + log.Fatalf("required flag %q not set or empty", fname) + } + } +} + +func main() { + flag.Parse() + checkRequiredFlags() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + awsSess, err := awssession.NewSession(&aws.Config{ + Region: aws.String("eu-west-1"), + Credentials: awscreds.NewStaticCredentials(*awsAccessKey, *awsAccessSecret, ""), + }) + if err != nil { + log.Fatalf("awssession.NewSession: %w", err) + } + s3Bucket, err := s3blob.OpenBucket(ctx, awsSess, *awsBucket, nil) + if err != nil { + log.Fatalf("s3blob.OpenBucket: %w", err) + } + defer s3Bucket.Close() + + bucket := blob.PrefixedBucket(s3Bucket, *awsPrefix) + defer bucket.Close() + + pc, err := pgx.Connect(ctx, *postgresString) + if err != nil { + log.Fatalf("pgx.Connect: %v", err) + } + defer pc.Close(context.Background()) + + catchupTx, err := pc.Begin(ctx) + if err != nil { + log.Fatalf("beginning catchup transaction: %v", err) + } + catchupAffected := darwindb.NewAffected() + + gimmeTransaction := func(ctx context.Context, cb func(ctx context.Context, tx pgx.Tx) error) error { + if catchupTx != nil { + return cb(ctx, catchupTx) + } + + tx, err := pc.Begin(ctx) + if err != nil { + return fmt.Errorf("starting transaction: %w", err) + } + defer tx.Rollback(ctx) + + if err := cb(ctx, tx); err != nil { + return err + } + + return tx.Commit(ctx) + } + sendAffectedNotifications := func(ctx context.Context, tx pgx.Tx, affected *darwindb.Affected) error { + for tsid := range affected.TSIDs { + if _, err := tx.Exec(ctx, fmt.Sprintf(`NOTIFY "tsid-%d"`, tsid)); err != nil { + return fmt.Errorf("notifying channel for tsid %d: %w", tsid, err) + } + } + return nil + } + processMessage := func(ctx context.Context, pp *darwin.PushPort) error { + return gimmeTransaction(ctx, func(ctx context.Context, tx pgx.Tx) error { + affected := catchupAffected + sendAffected := false + if affected == nil { + affected = darwindb.NewAffected() + sendAffected = true + } + if err := darwindb.Process(ctx, tx, pp, affected); err != nil { + return fmt.Errorf("darwindb.Process: %w", err) + } + if sendAffected { + return sendAffectedNotifications(ctx, tx, affected) + } + return nil + }) + } + + cfg := darwingest.Config{ + Verbose: true, + DialStomp: func(ctx context.Context) (*stomp.Conn, error) { + return darwingeststomp.Dial(ctx, darwingeststomp.Config{ + Hostname: *stompHostport, + Username: *stompUsername, + Password: *stompPassword, + ClientID: *stompClientID, + }) + }, + StompSubscribe: func(ctx context.Context, c *stomp.Conn) (*stomp.Subscription, error) { + return c.Subscribe(*stompTopic, stomp.AckClientIndividual) + }, + DialFTP: func(ctx context.Context) (*ftp.ServerConn, error) { + sc, err := ftp.Dial(*ftpHostport, ftp.DialWithContext(ctx)) + if err != nil { + return nil, fmt.Errorf("ftp.Dial: %w", err) + } + + if err := sc.Login(*ftpUsername, *ftpPassword); err != nil { + sc.Quit() + return nil, fmt.Errorf("ftp.ServerConn.Login: %w", err) + } + return sc, nil + }, + + Timetable: func(ppt *darwin.PushPortTimetable) error { + return gimmeTransaction(ctx, func(ctx context.Context, tx pgx.Tx) error { + if err := darwindb.ProcessTimetable(ctx, tx, ppt, catchupAffected); err != nil { + return fmt.Errorf("processing timetable data: %w", err) + } + return nil + }) + }, + ReferenceData: func(pprd *darwin.PushPortReferenceData) error { + return gimmeTransaction(ctx, func(ctx context.Context, tx pgx.Tx) error { + if err := darwindb.ProcessReferenceData(ctx, tx, pprd, catchupAffected); err != nil { + return fmt.Errorf("processing reference data: %w", err) + } + return nil + }) + }, + MessageCatchUp: func(pp *darwin.PushPort) error { + return processMessage(ctx, pp) + }, + CatchUpComplete: func(ctx context.Context) error { + log.Printf("catchup affected: %v", catchupAffected.Summary()) + if err := sendAffectedNotifications(ctx, catchupTx, catchupAffected); err != nil { + return fmt.Errorf("sending affected notifications: %v", err) + } + if err := catchupTx.Commit(ctx); err != nil { + return fmt.Errorf("committing catchup transaction: %v", err) + } + catchupAffected = nil + catchupTx = nil + return nil + }, + Message: func(pp *darwin.PushPort) error { + return processMessage(ctx, pp) + }, + } + if *cleanStart { + if err := darwingest.CatchUpAndStart(ctx, bucket, cfg); err != nil { + log.Printf("CatchUpAndStart: %v", err) + } + } else { + if err := darwingest.Start(ctx, bucket, cfg); err != nil { + log.Printf("Start: %v", err) + } + } + + log.Println("terminating...") +} diff --git a/go/trains/cmd/darwiningestd/default.nix b/go/trains/cmd/darwiningestd/default.nix new file mode 100644 index 0000000000..90cbe9e22f --- /dev/null +++ b/go/trains/cmd/darwiningestd/default.nix @@ -0,0 +1,24 @@ +# SPDX-FileCopyrightText: 2020 Luke Granger-Brown +# +# SPDX-License-Identifier: Apache-2.0 + +{ depot, ... }: + +depot.third_party.buildGo.program { + name = "darwiningestd"; + srcs = [ ./darwiningestd.go ]; + deps = with depot.third_party; [ + gopkgs."github.com".aws.aws-sdk-go.aws + gopkgs."github.com".aws.aws-sdk-go.aws.credentials + gopkgs."github.com".aws.aws-sdk-go.aws.session + gopkgs."github.com".go-stomp.stomp.v3 + gopkgs."github.com".jackc.pgx.v4 + gopkgs."github.com".jlaffaye.ftp + gopkgs."gocloud.dev".blob + gopkgs."gocloud.dev".blob.s3blob + depot.go.trains.darwin + depot.go.trains.darwin.darwindb + depot.go.trains.darwin.darwingest + depot.go.trains.darwin.darwingest.darwingeststomp + ]; +} diff --git a/go/trains/cmd/db2web/db2web.go b/go/trains/cmd/db2web/db2web.go new file mode 100644 index 0000000000..d22c2f38a7 --- /dev/null +++ b/go/trains/cmd/db2web/db2web.go @@ -0,0 +1,463 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "log" + "net/http" + "net/url" + "regexp" + "strconv" + "time" + + "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v4/pgxpool" + "hg.lukegb.com/lukegb/depot/go/trains/webapi" +) + +type querier interface { + Query(ctx context.Context, sql string, args ...interface{}) (pgx.Rows, error) + QueryRow(ctx context.Context, sql string, args ...interface{}) pgx.Row +} + +func summarizeService(ctx context.Context, pc querier, id int) (*webapi.ServiceData, error) { + row := pc.QueryRow(ctx, ` +SELECT + ts.id, ts.rid, ts.uid, ts.rsid, ts.headcode, ts.scheduled_start_date::varchar, + ts.train_operator, rop.name, rop.url, + COALESCE(ts.delay_reason_code::int, 0), COALESCE(rlr.text, ''), COALESCE(ts.delay_reason_tiploc, ''), COALESCE(rlrloc.name, ''), rlrloc.crs, COALESCE(rlrloc.toc, ''), rlrop.name, rlrop.url, COALESCE(ts.delay_reason_tiploc_near, false), + COALESCE(ts.cancellation_reason_code::int, 0), COALESCE(rlr.text, ''), COALESCE(ts.cancellation_reason_tiploc, ''), COALESCE(rlrloc.name, ''), rlrloc.crs, COALESCE(rlrloc.toc, ''), rlrop.name, rlrop.url, COALESCE(ts.cancellation_reason_tiploc_near, false), + ts.active, ts.deleted, ts.cancelled +FROM + train_services ts + LEFT JOIN ref_tocs rop ON rop.toc=ts.train_operator + + LEFT JOIN ref_late_running_reasons rlr ON rlr.code=ts.delay_reason_code::int + LEFT JOIN ref_locations rlrloc ON rlrloc.tiploc=ts.delay_reason_tiploc + LEFT JOIN ref_tocs rlrop ON rlrop.toc=rlrloc.toc + + LEFT JOIN ref_cancel_reasons rc ON rc.code=ts.cancellation_reason_code::int + LEFT JOIN ref_locations rcloc ON rcloc.tiploc=ts.cancellation_reason_tiploc + LEFT JOIN ref_tocs rcop ON rcop.toc=rcloc.toc +WHERE + ts.id=$1 +`, id) + + sd := webapi.ServiceData{ + DelayReason: &webapi.DisruptionReason{Location: &webapi.Location{TOC: &webapi.TrainOperator{}}}, + CancelReason: &webapi.DisruptionReason{Location: &webapi.Location{TOC: &webapi.TrainOperator{}}}, + } + if err := row.Scan( + &sd.ID, &sd.RID, &sd.UID, &sd.RSID, &sd.Headcode, &sd.StartDate, + &sd.TrainOperator.Code, &sd.TrainOperator.Name, &sd.TrainOperator.URL, + &sd.DelayReason.Code, &sd.DelayReason.Text, &sd.DelayReason.Location.TIPLOC, &sd.DelayReason.Location.Name, &sd.DelayReason.Location.CRS, &sd.DelayReason.Location.TOC.Code, &sd.DelayReason.Location.TOC.Name, &sd.DelayReason.Location.TOC.URL, &sd.DelayReason.NearLocation, + &sd.CancelReason.Code, &sd.CancelReason.Text, &sd.CancelReason.Location.TIPLOC, &sd.CancelReason.Location.Name, &sd.CancelReason.Location.CRS, &sd.CancelReason.Location.TOC.Code, &sd.CancelReason.Location.TOC.Name, &sd.CancelReason.Location.TOC.URL, &sd.CancelReason.NearLocation, + &sd.Active, &sd.Deleted, &sd.Cancelled, + ); err != nil { + return nil, fmt.Errorf("reading from train_services for %d: %w", id, err) + } + sd.Canonicalize() + + // Now for the locations. + rows, err := pc.Query(ctx, ` +SELECT + tl.id, + tl.tiploc, COALESCE(rloc.name, ''), COALESCE(rloc.crs, ''), COALESCE(rloc.toc, ''), rloctoc.name, rloctoc.url, + tl.calling_point::varchar, tl.train_length, tl.service_suppressed, + tl.schedule_cancelled, + COALESCE(tl.schedule_platform, ''), COALESCE(tl.platform, ''), COALESCE(tl.platform_confirmed, false), COALESCE(tl.platform_suppressed, false), + tl.schedule_false_destination_tiploc, COALESCE(rfdloc.name, ''), COALESCE(rfdloc.crs, ''), COALESCE(rfdloc.toc, ''), rfdloctoc.name, rfdloctoc.url, + + tl.schedule_public_arrival, tl.schedule_working_arrival, tl.estimated_arrival, tl.working_estimated_arrival, tl.actual_arrival, + tl.schedule_public_departure, tl.schedule_working_departure, tl.estimated_departure, tl.working_estimated_departure, tl.actual_departure, + /* no public_pass */ tl.schedule_working_pass, tl.estimated_pass, tl.working_estimated_pass, tl.actual_pass +FROM + train_locations tl + LEFT JOIN ref_locations rloc ON rloc.tiploc=tl.tiploc + LEFT JOIN ref_tocs rloctoc ON rloctoc.toc=rloc.toc + + LEFT JOIN ref_locations rfdloc ON rfdloc.tiploc=tl.schedule_false_destination_tiploc + LEFT JOIN ref_tocs rfdloctoc ON rfdloctoc.toc=rfdloc.toc +WHERE + tl.tsid=$1 AND tl.active_in_schedule +ORDER BY + COALESCE(tl.schedule_working_arrival, tl.schedule_working_pass, tl.schedule_working_departure) +`, id) + if err != nil { + return nil, fmt.Errorf("querying locations for %d: %w", id, err) + } + defer rows.Close() + for rows.Next() { + loc := webapi.ServiceLocation{ + Location: &webapi.Location{TOC: &webapi.TrainOperator{}}, + Platform: &webapi.PlatformData{}, + FalseDestination: &webapi.Location{TOC: &webapi.TrainOperator{}}, + ArrivalTiming: &webapi.TimingData{}, + DepartureTiming: &webapi.TimingData{}, + PassTiming: &webapi.TimingData{}, + } + if err := rows.Scan( + &loc.ID, + &loc.Location.TIPLOC, &loc.Location.Name, &loc.Location.CRS, &loc.Location.TOC.Code, &loc.Location.TOC.Name, &loc.Location.TOC.URL, + &loc.CallingPointType, &loc.Length, &loc.Suppressed, + &loc.Cancelled, + &loc.Platform.Scheduled, &loc.Platform.Live, &loc.Platform.Confirmed, &loc.Platform.Suppressed, + &loc.FalseDestination.TIPLOC, &loc.FalseDestination.Name, &loc.FalseDestination.CRS, &loc.FalseDestination.TOC.Code, &loc.FalseDestination.TOC.Name, &loc.FalseDestination.TOC.URL, + &loc.ArrivalTiming.PublicScheduled, &loc.ArrivalTiming.WorkingScheduled, &loc.ArrivalTiming.PublicEstimated, &loc.ArrivalTiming.WorkingEstimated, &loc.ArrivalTiming.Actual, + &loc.DepartureTiming.PublicScheduled, &loc.DepartureTiming.WorkingScheduled, &loc.DepartureTiming.PublicEstimated, &loc.DepartureTiming.WorkingEstimated, &loc.DepartureTiming.Actual, + /*&loc.PassTiming.PublicScheduled,*/ &loc.PassTiming.WorkingScheduled, &loc.PassTiming.PublicEstimated, &loc.PassTiming.WorkingEstimated, &loc.PassTiming.Actual, + ); err != nil { + return nil, fmt.Errorf("scanning locations for %d: %w", id, err) + } + loc.Canonicalize() + sd.Locations = append(sd.Locations, loc) + } + if rows.Err() != nil { + return nil, fmt.Errorf("iterating over locations for %d: %w", id, err) + } + + return &sd, nil +} + +type watchingResponseWriter struct { + http.ResponseWriter + http.Flusher + WrittenTo bool + WroteStatus int + WroteContentBytes int +} + +func (rw *watchingResponseWriter) Flush() { + if rw.Flusher != nil { + rw.Flusher.Flush() + } +} + +func (rw *watchingResponseWriter) Header() http.Header { return rw.ResponseWriter.Header() } +func (rw *watchingResponseWriter) Write(bs []byte) (int, error) { + if !rw.WrittenTo { + rw.WroteStatus = http.StatusOK + } + rw.WrittenTo = true + rw.WroteContentBytes += len(bs) + return rw.ResponseWriter.Write(bs) +} +func (rw *watchingResponseWriter) WriteHeader(statusCode int) { + if rw.WrittenTo { + return + } + rw.WrittenTo = true + rw.WroteStatus = statusCode + rw.ResponseWriter.WriteHeader(statusCode) +} + +type httpError struct { + err error + httpStatusCode int + publicError string +} + +func (he httpError) Error() string { + if he.err == nil { + return fmt.Sprintf("HTTP %d: %v", he.httpStatusCode, he.publicError) + } + return he.err.Error() +} + +type server struct { + dbPool *pgxpool.Pool +} + +var jsonPathRegexp = regexp.MustCompile(`/([1-9][0-9]*)$`) + +func (s *server) handleJSON(ctx context.Context, rw http.ResponseWriter, r *http.Request) error { + // / + ms := jsonPathRegexp.FindStringSubmatch(r.URL.Path) + if len(ms) != 2 { + return httpError{ + httpStatusCode: http.StatusNotFound, + publicError: "not found", + } + } + id, err := strconv.Atoi(ms[1]) + if err != nil { + return httpError{ + err: err, + httpStatusCode: http.StatusBadRequest, + publicError: "bad id", + } + } + + conn, err := s.dbPool.Acquire(ctx) + if err != nil { + return fmt.Errorf("acquiring database connection: %w", err) + } + defer conn.Release() + + svc, err := summarizeService(ctx, conn, id) + if errors.Is(err, pgx.ErrNoRows) { + return httpError{ + err: err, + httpStatusCode: http.StatusNotFound, + publicError: "no train with that ID", + } + } else if err != nil { + return err + } + + rw.Header().Set("Content-Type", "application/json; charset=utf-8") + if err := json.NewEncoder(rw).Encode(svc); err != nil { + return fmt.Errorf("encoding JSON: %w", err) + } + + return nil +} + +var channelTIDRegex = regexp.MustCompile(`tsid-([0-9]+)`) + +func (s *server) handleEventStream(ctx context.Context, rw http.ResponseWriter, r *http.Request) error { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + if r.URL.Path != "/" { + return httpError{ + httpStatusCode: http.StatusNotFound, + publicError: "not found", + } + } + qs, err := url.ParseQuery(r.URL.RawQuery) + if err != nil { + return httpError{ + err: err, + httpStatusCode: http.StatusBadRequest, + publicError: "can't parse query string", + } + } + + var ids []int + for _, idStr := range qs["id"] { + id, err := strconv.Atoi(idStr) + if err != nil { + return httpError{ + err: err, + httpStatusCode: http.StatusBadRequest, + publicError: "id parameter bad", + } + } + ids = append(ids, id) + } + if len(ids) == 0 { + return httpError{ + httpStatusCode: http.StatusBadRequest, + publicError: "need at least one ?id= to watch", + } + } + + listenConn, err := s.dbPool.Acquire(ctx) + if err != nil { + return fmt.Errorf("acquiring database connection: %w", err) + } + defer listenConn.Release() + + for _, id := range ids { + _, err := listenConn.Exec(ctx, fmt.Sprintf(`LISTEN "tsid-%d"`, id)) + if err != nil { + return fmt.Errorf("watching for %d: %w", id, err) + } + } + + rw.Header().Set("Content-Type", "text/event-stream") + rw.Header().Set("Cache-Control", "no-cache") + + je := json.NewEncoder(rw) + + encodeSvc := func(conn querier, id int) error { + svc, err := summarizeService(ctx, conn, id) + if errors.Is(err, pgx.ErrNoRows) { + if _, err := fmt.Fprintf(rw, "event: notyet\ndata: %d\n\n", id); err != nil { + return err + } + } else if err != nil { + return fmt.Errorf("summarizeService(%d): %w", id, err) + } + + if _, err := fmt.Fprint(rw, "data: "); err != nil { + return fmt.Errorf("writing data: prefix for service %d: %w", id, err) + } + if err := je.Encode(svc); err != nil { + return fmt.Errorf("je.Encode service %d: %w", id, err) + } + + if _, err := fmt.Fprint(rw, "\n\n"); err != nil { + return fmt.Errorf("printing separator after service %d: %w", id, err) + } + + if f, ok := rw.(http.Flusher); ok { + f.Flush() + } + + return nil + } + + for _, id := range ids { + if err := encodeSvc(listenConn, id); err != nil { + return fmt.Errorf("initial data for service %d: %w", err) + } + } + + type updatedMsg struct { + TID int + Err error + } + updatedCh := make(chan updatedMsg) + + go func() { + defer close(updatedCh) + for { + n, err := listenConn.Conn().WaitForNotification(ctx) + if err != nil { + select { + case <-ctx.Done(): + return + case updatedCh <- updatedMsg{Err: err}: + } + } + + ms := channelTIDRegex.FindStringSubmatch(n.Channel) + if len(ms) != 2 { + log.Printf("unrecognised notification channel %q", n.Channel) + continue + } + + id, err := strconv.Atoi(ms[1]) + if err != nil { + log.Printf("failed to parse TID from notification channel %q: %v", n.Channel, err) + continue + } + + select { + case <-ctx.Done(): + return + case updatedCh <- updatedMsg{TID: id}: + } + } + }() + + pingCh := time.NewTicker(1 * time.Minute) + defer pingCh.Stop() + + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-pingCh.C: + if _, err := fmt.Fprint(rw, "event: ping\n\n"); err != nil { + return err + } + if f, ok := rw.(http.Flusher); ok { + f.Flush() + } + case umsg, ok := <-updatedCh: + if !ok { + log.Printf("updatedCh closed, shutting down conn") + return nil + } + if umsg.Err != nil { + return umsg.Err + } + sconn, err := s.dbPool.Acquire(ctx) + if err != nil { + return fmt.Errorf("acquiring database connection: %w", err) + } + err = encodeSvc(sconn, umsg.TID) + sconn.Release() + if err != nil { + return err + } + } + } +} + +func (s *server) handleHTTP(ctx context.Context, rw http.ResponseWriter, r *http.Request) error { + accept := r.Header.Get("Accept") + switch accept { + case "application/json": + return s.handleJSON(ctx, rw, r) + case "text/event-stream": + return s.handleEventStream(ctx, rw, r) + default: + return httpError{ + httpStatusCode: http.StatusNotAcceptable, + publicError: "Accept should be text/event-stream or application/json", + } + } +} + +func (s *server) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + wrw := &watchingResponseWriter{ResponseWriter: rw, Flusher: rw.(http.Flusher)} + if err := s.handleHTTP(r.Context(), wrw, r); err != nil { + statusCode := http.StatusInternalServerError + errText := fmt.Sprintf("internal server error: %v", err.Error()) + + var he httpError + if errors.As(err, &he) { + if he.httpStatusCode != 0 { + statusCode = he.httpStatusCode + } + if he.publicError != "" { + errText = he.publicError + } + } + log.Printf("[%v] %v %v -- %v (%d %v)", r.RemoteAddr, r.Method, r.RequestURI, err, statusCode, errText) + + if !wrw.WrittenTo { + + for k := range wrw.Header() { + wrw.Header().Del(k) + } + wrw.Header().Set("Content-Type", "text/plain") + wrw.WriteHeader(statusCode) + fmt.Fprintf(wrw, "%s\n", errText) + } + } else { + log.Printf("[%v] %v %v -- (%d) %d bytes", r.RemoteAddr, r.Method, r.RequestURI, wrw.WroteStatus, wrw.WroteContentBytes) + } +} + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + cfg, err := pgxpool.ParseConfig("host=/var/run/postgresql database=trains search_path=darwindb") + if err != nil { + log.Fatalf("pgxpool.ParseConfig: %v", err) + } + cfg.AfterRelease = func(pc *pgx.Conn) bool { + // true to return to pool, false to destroy + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + + _, err := pc.Exec(ctx, "UNLISTEN *") + if err != nil { + log.Printf("pg connection cleanup: UNLISTEN failed: %v", err) + return false + } + + return true + } + + pcpool, err := pgxpool.ConnectConfig(ctx, cfg) + if err != nil { + log.Fatalf("pgxpool.ConnectConfig: %v", err) + } + defer pcpool.Close() + + s := &server{ + dbPool: pcpool, + } + listen := ":13974" + log.Printf("listening on %s", listen) + log.Fatal(http.ListenAndServe(listen, s)) +} diff --git a/go/trains/cmd/db2web/default.nix b/go/trains/cmd/db2web/default.nix new file mode 100644 index 0000000000..e25986e0c3 --- /dev/null +++ b/go/trains/cmd/db2web/default.nix @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2020 Luke Granger-Brown +# +# SPDX-License-Identifier: Apache-2.0 + +{ depot, ... }: + +depot.third_party.buildGo.program { + name = "db2web"; + srcs = [ ./db2web.go ]; + deps = with depot.third_party; [ + gopkgs."github.com".jackc.pgx.v4 + gopkgs."github.com".jackc.pgx.v4.pgxpool + depot.go.trains.webapi + ]; +} diff --git a/go/trains/cmd/default.nix b/go/trains/cmd/default.nix new file mode 100644 index 0000000000..95c6c563b3 --- /dev/null +++ b/go/trains/cmd/default.nix @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2020 Luke Granger-Brown +# +# SPDX-License-Identifier: Apache-2.0 + +{ depot, ... }@args: + +rec { + darwiningestd = import ./darwiningestd args; + db2web = import ./db2web args; + rttingest = import ./rttingest args; + train2livesplit = import ./train2livesplit args; + + bins = { + inherit darwiningestd db2web train2livesplit; + inherit (rttingest) tiploc; + }; +} diff --git a/go/trains/cmd/rttingest/default.nix b/go/trains/cmd/rttingest/default.nix new file mode 100644 index 0000000000..734ef1b518 --- /dev/null +++ b/go/trains/cmd/rttingest/default.nix @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2020 Luke Granger-Brown +# +# SPDX-License-Identifier: Apache-2.0 + +{ depot, ... }: + +{ + tiploc = depot.third_party.buildGo.program { + name = "tiploc"; + srcs = [ ./tiploc.go ]; + deps = with depot.third_party; [ + gopkgs."github.com".jackc.pgx.v4 + ]; + }; +} diff --git a/go/trains/cmd/rttingest/tiploc.go b/go/trains/cmd/rttingest/tiploc.go new file mode 100644 index 0000000000..3b22eede4d --- /dev/null +++ b/go/trains/cmd/rttingest/tiploc.go @@ -0,0 +1,131 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net/http" + + pgx "github.com/jackc/pgx/v4" +) + +func fetchWork(ctx context.Context, pc *pgx.Conn) ([]string, error) { + rows, err := pc.Query(ctx, `SELECT tiploc FROM ref_locations WHERE name=tiploc AND override_name IS NULL`) + if err != nil { + return nil, fmt.Errorf("querying for work: %w", err) + } + defer rows.Close() + + var tiplocs []string + for rows.Next() { + var tiploc string + if err := rows.Scan(&tiploc); err != nil { + return nil, fmt.Errorf("scanning row: %w", err) + } + tiplocs = append(tiplocs, tiploc) + } + if rows.Err() != nil { + return nil, fmt.Errorf("retrieving rows for work: %w", err) + } + + return tiplocs, nil +} + +type RTT struct { + Username, Password string +} + +type RTTLocationDetail struct { + TIPLOC string `json:"tiploc"` + CRS string `json:"crs"` + Name string `json:"name"` + Description string `json:"description"` +} + +type RTTLocationContainer struct { + Location *RTTLocationDetail `json:"location"` +} + +func (r RTT) LocationDetail(ctx context.Context, inp string) (*RTTLocationDetail, error) { + req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://api.rtt.io/api/v1/json/search/%v", inp), nil) + if err != nil { + return nil, fmt.Errorf("formulating query for %q: %w", inp, err) + } + req.SetBasicAuth(r.Username, r.Password) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, fmt.Errorf("querying API for %q: %w", inp, err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("status for %q was %d %v; wanted 200 OK", inp, resp.StatusCode, resp.Status) + } + + var cont RTTLocationContainer + if err := json.NewDecoder(resp.Body).Decode(&cont); err != nil { + return nil, fmt.Errorf("unmarshalling API response for %q: %w", inp, err) + } + + return cont.Location, nil +} + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + pc, err := pgx.Connect(ctx, "host=/var/run/postgresql database=trains search_path=darwindb") + if err != nil { + log.Fatalf("pgx.Connect: %v", err) + } + defer pc.Close(context.Background()) + + tiplocs, err := fetchWork(ctx, pc) + if err != nil { + log.Fatalf("fetchWork: %v", err) + } + log.Printf("%d tiplocs to process", len(tiplocs)) + + r := RTT{Username: "rttapi_lukegb", Password: "5c1e5ae8a88882b090cd2e55225a3f0f581ec57c"} + + var rttApiErrCount int + for n, t := range tiplocs { + if rttApiErrCount == 20 { + log.Println("Too many errors from RTT, stopping") + break + } + if n%20 == 0 { + log.Printf("%d/%d (%d%%)", n, len(tiplocs), (n*100)/len(tiplocs)) + } + + d, err := r.LocationDetail(ctx, t) + if err != nil { + log.Printf("LocationDetail(ctx, %q): %v", t, err) + rttApiErrCount++ + continue + } + if d == nil { + log.Printf("got a nil location for %q!", t) + rttApiErrCount++ + continue + } + rttApiErrCount = 0 // reset the breaker + if d.Name == "" { + log.Printf("No name for %q", t) + continue + } + + ct, err := pc.Exec(ctx, `UPDATE ref_locations SET name=$2, override_name=$2, override_name_src='RTT' WHERE tiploc=$1 AND override_name IS NULL AND name=tiploc`, t, d.Name) + if err != nil { + log.Printf("Updating database for %q failed: %w", t, err) + break + } + if ra := ct.RowsAffected(); ra != 1 { + log.Printf("Updated %d rows for %q; wanted 1???", ra, t) + } + } + + log.Println("terminating...") +} diff --git a/go/trains/cmd/train2livesplit/default.nix b/go/trains/cmd/train2livesplit/default.nix new file mode 100644 index 0000000000..086ad27672 --- /dev/null +++ b/go/trains/cmd/train2livesplit/default.nix @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: 2020 Luke Granger-Brown +# +# SPDX-License-Identifier: Apache-2.0 + +{ depot, ... }: +depot.third_party.buildGo.program { + name = "train2livesplit"; + srcs = [ ./train2livesplit.go ]; + deps = with depot.third_party; [ + depot.go.trains.webapi + ]; +} diff --git a/go/trains/cmd/train2livesplit/train2livesplit.go b/go/trains/cmd/train2livesplit/train2livesplit.go new file mode 100644 index 0000000000..24dcfe9e4f --- /dev/null +++ b/go/trains/cmd/train2livesplit/train2livesplit.go @@ -0,0 +1,247 @@ +package main + +import ( + "bufio" + "context" + "encoding/json" + "flag" + "fmt" + "log" + "net/http" + "os" + "strings" + "text/template" + "time" + + "hg.lukegb.com/lukegb/depot/go/trains/webapi" +) + +var ( + header = template.Must(template.New("script").Parse(` +startscript C# +this["model"].Reset(); +var run = this["state"].Run; +run.CategoryName = "{{if .Service.Headcode}}{{.Service.Headcode}} {{end}}{{.FirstLocation.DepartureTiming.PublicScheduled.Format "1504"}} {{.FirstLocation.Location.Name}} to {{.LastLocation.Location.Name}}"; +run.GameName = "{{.Service.StartDate}}"; +run.AttemptCount = 0; +run.AttemptHistory.Clear(); +run.AutoSplitter = null; +run.HasChanged = true; +run.Clear(); + +var ass = run.GetType().Assembly; +var timingMethodRealTime = Enum.GetValues(ass.GetType("LiveSplit.Model.TimingMethod"))[0]; +this["state"].CurrentTimingMethod = timingMethodRealTime; + +var timeStampType = ass.GetType("LiveSplit.Model.TimeStamp"); +var atomicDateTime = ass.GetType("LiveSplit.Model.AtomicDateTime"); // (System.DateTime, synced) + +var runExtensionsType = ass.GetType("LiveSplit.Model.RunExtensions"); +var runExtensionsAddSegment = runExtensionsType.GetMethod("AddSegment"); + +var timeType = ass.GetType("LiveSplit.Model.Time"); +var defaultTime = Activator.CreateInstance(timeType); +var lastSplitTime = defaultTime; + +Func timeFromSeconds = delegate (int seconds) { + if (seconds < 0) { + return defaultTime; + } + var time = Activator.CreateInstance(timeType); + time.RealTime = TimeSpan.FromSeconds(seconds); + return time; +}; +Action addSplit = delegate (string name, int timetableSeconds, int realSeconds) { + var timetableTime = Convert.ChangeType(timeFromSeconds(timetableSeconds), timeType); + var lastTimetableTime = Convert.ChangeType(timeFromSeconds(0), timeType); + if (this["state"].Run.Count > 0) { + lastTimetableTime = this["state"].Run[this["state"].Run.Count-1].PersonalBestSplitTime; + } + var timetableSegmentTime = timetableTime - lastTimetableTime; + var realTime = timeFromSeconds(realSeconds); + runExtensionsAddSegment.Invoke(null, new object[] { run, name, timetableTime, timetableSegmentTime, null, realTime, null }); + lastSplitTime = this["state"].Run[this["state"].Run.Count-1].SplitTime; +}; + +// run, name, pbSplitTime, bestSegmentTime, icon, splitTime, segmentHistory +{{.AddSplitFunctions}} +endscript +`)) + + trainId = flag.Int("train_id", -1, "Train ID") + endpoint = flag.String("endpoint", "http://localhost:13974", "db2web endpoint") +) + +type templateVars struct { + Service *webapi.ServiceData + FirstLocation *webapi.ServiceLocation + LastLocation *webapi.ServiceLocation + AddSplitFunctions string +} + +func fetchTrainOnce(ctx context.Context, id int) (*webapi.ServiceData, error) { + req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/%d", *endpoint, id), nil) + if err != nil { + return nil, fmt.Errorf("constructing new request for %d: %w", id, err) + } + req.Header.Set("Accept", "application/json") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, fmt.Errorf("doing request for %d: %w", id, err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("http status for %d was %d %v", id, resp.StatusCode, resp.Status) + } + + var sd webapi.ServiceData + if err := json.NewDecoder(resp.Body).Decode(&sd); err != nil { + return nil, fmt.Errorf("decoding train %d: %w", id, err) + } + return &sd, nil +} + +func generateLiveSplitScriptData(sd *webapi.ServiceData) (*templateVars, error) { + asf := new(strings.Builder) + currentSplit := -1 + usingTime := func(td *webapi.TimingData) time.Time { + if td.PublicScheduled != nil { + return *td.PublicScheduled + } + return *td.WorkingScheduled + } + timetableDepartureTime := usingTime(sd.Locations[0].DepartureTiming) + var lastHadActual bool + for n, l := range sd.Locations { + var usingData *webapi.TimingData + switch { + case l.DepartureTiming != nil: + usingData = l.DepartureTiming + case l.PassTiming != nil: + usingData = l.PassTiming + case l.ArrivalTiming != nil: + usingData = l.ArrivalTiming + } + usingTimetableTime := usingTime(usingData) + + var secondsOnTimetable, secondsActual int + secondsOnTimetable = int((usingTimetableTime.Sub(timetableDepartureTime)) / time.Second) + secondsActual = -1 + + if lastHadActual { + currentSplit = n + lastHadActual = false + } + if usingData.Actual != nil { + secondsActual = int((usingData.Actual.Sub(timetableDepartureTime)) / time.Second) + lastHadActual = true + } + + fmt.Fprintf(asf, "addSplit(%q, %d, %d);\n", l.Location.Name, secondsOnTimetable, secondsActual) + } + if lastHadActual { + fmt.Fprintf(asf, ` +this["state"].Run.Offset = TimeSpan.FromSeconds(%d); +this["model"].Start(); +this["state"].CurrentSplitIndex = %d-1; +this["model"].Split(); +this["state"].Run[%d-1].SplitTime = lastSplitTime; +`, (time.Now().Sub(timetableDepartureTime))/time.Second, len(sd.Locations), len(sd.Locations)) + } else { + fmt.Fprintf(asf, ` +this["state"].Run.Offset = TimeSpan.FromSeconds(%d); +this["model"].Start(); +this["state"].CurrentSplitIndex = %d; +`, (time.Now().Sub(timetableDepartureTime))/time.Second, currentSplit) + } + + return &templateVars{ + Service: sd, + FirstLocation: &sd.Locations[0], + LastLocation: &sd.Locations[len(sd.Locations)-1], + AddSplitFunctions: asf.String(), + }, nil +} + +func fetchTrainAndOutputLiveSplit(ctx context.Context, id int) error { + req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/?id=%d", *endpoint, id), nil) + if err != nil { + return fmt.Errorf("constructing new request for %d: %w", id, err) + } + req.Header.Set("Accept", "text/event-stream") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("doing request for %d: %w", id, err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("http status for %d was %d %v", id, resp.StatusCode, resp.Status) + } + + sc := bufio.NewScanner(resp.Body) + var buffer []string + for sc.Scan() { + buffer = append(buffer, sc.Text()) + if buffer[0] == "" { + buffer = buffer[1:] + } + fmt.Fprintf(os.Stderr, "%#v\n\n", buffer) + if len(buffer) < 2 || buffer[len(buffer)-1] != "" { + continue + } + if strings.HasPrefix(buffer[0], "event:") { + buffer = nil + continue + } + + var xs []string + for _, s := range buffer { + xs = append(xs, strings.TrimPrefix(s, "data: ")) + } + + var sd webapi.ServiceData + if err := json.Unmarshal([]byte(strings.Join(xs, "")), &sd); err != nil { + return err + } + + tv, err := generateLiveSplitScriptData(&sd) + if err != nil { + return err + } + + if err := header.Execute(os.Stdout, tv); err != nil { + return err + } + + buffer = nil + } + + return nil +} + +func main() { + flag.Parse() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + log.Fatal(fetchTrainAndOutputLiveSplit(ctx, *trainId)) + + sd, err := fetchTrainOnce(ctx, 33634) + if err != nil { + log.Fatal(err) + } + tv, err := generateLiveSplitScriptData(sd) + if err != nil { + log.Fatal(err) + } + + if err := header.Execute(os.Stdout, tv); err != nil { + log.Fatal(err) + } + +} diff --git a/go/trains/darwin/darwindb/_schema.sql b/go/trains/darwin/darwindb/_schema.sql new file mode 100644 index 0000000000..8593a2c335 --- /dev/null +++ b/go/trains/darwin/darwindb/_schema.sql @@ -0,0 +1,309 @@ +DROP SCHEMA darwindb CASCADE; +CREATE SCHEMA darwindb; +SET search_path TO darwindb; + +CREATE TABLE train_services ( + id BIGSERIAL NOT NULL PRIMARY KEY, + rid VARCHAR(16) NOT NULL UNIQUE, + uid VARCHAR(6) NOT NULL, + rsid VARCHAR(8) NOT NULL, + headcode VARCHAR(4) NOT NULL, + scheduled_start_date DATE NOT NULL, + effective_start_time TIMESTAMPTZ NULL, /* computed for ease of use */ + + train_operator VARCHAR(2) NOT NULL, + status VARCHAR(2) NOT NULL DEFAULT 'P', + train_category VARCHAR(2) NOT NULL DEFAULT 'OO', + is_passenger_svc BOOLEAN NOT NULL DEFAULT TRUE, + is_charter BOOLEAN NOT NULL DEFAULT FALSE, + is_q_train BOOLEAN NOT NULL DEFAULT FALSE, + + delay_reason_code VARCHAR NULL, + delay_reason_tiploc VARCHAR NULL, + delay_reason_tiploc_near BOOLEAN NULL, + + cancellation_reason_code VARCHAR NULL, + cancellation_reason_tiploc VARCHAR NULL, + cancellation_reason_tiploc_near BOOLEAN NULL, + + active BOOLEAN NOT NULL DEFAULT TRUE, + deleted BOOLEAN NOT NULL DEFAULT FALSE, + cancelled BOOLEAN NOT NULL DEFAULT FALSE +); + +CREATE INDEX train_services_by_ssd_uid ON train_services (scheduled_start_date, uid); +CREATE INDEX train_services_by_ssd_rsid ON train_services (scheduled_start_date, rsid); +CREATE INDEX train_services_by_ssd_headcode ON train_services (scheduled_start_date, headcode); + +CREATE TYPE timestamp_type AS ENUM ('est', 'act'); +CREATE TYPE platform_source AS ENUM ('planned', 'automatic', 'manual'); +CREATE TYPE calling_point_type AS ENUM ('OR', 'OPOR', 'IP', 'OPIP', 'PP', 'DT', 'OPDT'); + +CREATE TABLE train_locations ( + id BIGSERIAL NOT NULL PRIMARY KEY, + tsid BIGINT NOT NULL REFERENCES train_services (id) ON DELETE CASCADE, + rid VARCHAR(16) NOT NULL REFERENCES train_services (rid) ON DELETE CASCADE, + tiploc VARCHAR(7) NOT NULL, + + -- From Darwin Schedule + calling_point calling_point_type NULL, + schedule_public_arrival TIMESTAMPTZ NULL, + schedule_working_arrival TIMESTAMPTZ NULL, + schedule_public_departure TIMESTAMPTZ NULL, + schedule_working_departure TIMESTAMPTZ NULL, + -- No concept of a "public pass" + schedule_working_pass TIMESTAMPTZ NULL, + schedule_route_delay SMALLINT NULL, + schedule_activity VARCHAR(12) NULL, + schedule_planned_activity VARCHAR(12) NULL, + schedule_cancelled BOOLEAN NOT NULL DEFAULT false, + schedule_false_destination_tiploc VARCHAR(7) NULL, + schedule_platform VARCHAR(5) NULL, + + -- Indicates whether this appeared in a schedule we've seen previously, but not in the latest version. + active_in_schedule BOOLEAN NOT NULL DEFAULT true, + + -- From Darwin TrainStatus + service_suppressed BOOLEAN NOT NULL DEFAULT false, + platform VARCHAR(5) NULL, + platform_suppressed BOOLEAN NOT NULL DEFAULT false, + cis_platform_suppression BOOLEAN NOT NULL DEFAULT false, + platform_source platform_source NOT NULL DEFAULT 'planned', + platform_confirmed BOOLEAN NOT NULL DEFAULT false, + train_length SMALLINT NULL, + + -- estimated, working, actual + -- arrival, departure, pass + estimated_arrival TIMESTAMPTZ NULL, + working_estimated_arrival TIMESTAMPTZ NULL, + actual_arrival TIMESTAMPTZ NULL, + arrival_data JSONB NULL, + + estimated_departure TIMESTAMPTZ NULL, + working_estimated_departure TIMESTAMPTZ NULL, + actual_departure TIMESTAMPTZ NULL, + departure_data JSONB NULL, + + estimated_pass TIMESTAMPTZ NULL, + working_estimated_pass TIMESTAMPTZ NULL, + actual_pass TIMESTAMPTZ NULL, + pass_data JSONB NULL, + + CONSTRAINT train_locations_tiploc_wtd UNIQUE (tsid, tiploc, schedule_working_departure), + CONSTRAINT train_locations_tiploc_ptd UNIQUE (tsid, tiploc, schedule_public_departure), + CONSTRAINT train_locations_tiploc_wta UNIQUE (tsid, tiploc, schedule_working_arrival), + CONSTRAINT train_locations_tiploc_pta UNIQUE (tsid, tiploc, schedule_public_arrival), + CONSTRAINT train_locations_tiploc_wtp UNIQUE (tsid, tiploc, schedule_working_pass) +); + +CREATE INDEX train_locations_by_tsid ON train_locations (tsid, tiploc); +CREATE INDEX train_locations_by_rid ON train_locations (rid, tiploc); + +CREATE FUNCTION train_locations_upsert_from_schedule ( + p_tsid BIGINT, + p_rid VARCHAR(16), + p_tiploc VARCHAR(7), + p_calling_point calling_point_type, + p_schedule_public_arrival TIMESTAMPTZ, + p_schedule_working_arrival TIMESTAMPTZ, + p_schedule_public_departure TIMESTAMPTZ, + p_schedule_working_departure TIMESTAMPTZ, + p_schedule_working_pass TIMESTAMPTZ, + p_schedule_route_delay SMALLINT, + p_schedule_activity VARCHAR(12), + p_schedule_planned_activity VARCHAR(12), + p_schedule_cancelled BOOLEAN, + p_schedule_false_destination_tiploc VARCHAR(7), + p_schedule_platform VARCHAR(3)) RETURNS BIGINT AS +$$ +-- There is one problem with this function, which is why it's not generic and using ON CONFLICT DO UPDATE: +-- if we notice that one particular constraint has caused a problem, so we do an update, and _that_ update causes a problem +-- then we're stuck; in this instance we'll just explode. +-- +-- Imagine the following (unlikely) case: +-- Old schedule: (tsid, tiploc, wtd, ptd): +-- (1, "AAAAAA", 01:00, 01:00) +-- (1, "AAAAAA", 01:10, 01:10) +-- If we then try to update with a new schedule location (1, "AAAAAA", 01:00, 01:10), we cause a conflict. +BEGIN + LOOP + DECLARE v_locid BIGINT; + DECLARE v_constraint_name TEXT; + BEGIN + INSERT INTO train_locations + (tsid, rid, tiploc, calling_point, + schedule_public_arrival, schedule_working_arrival, + schedule_public_departure, schedule_working_departure, + schedule_working_pass, + schedule_route_delay, schedule_activity, schedule_planned_activity, schedule_cancelled, + schedule_false_destination_tiploc, schedule_platform) + VALUES + (p_tsid, p_rid, p_tiploc, p_calling_point, + p_schedule_public_arrival, p_schedule_working_arrival, + p_schedule_public_departure, p_schedule_working_departure, + p_schedule_working_pass, + p_schedule_route_delay, p_schedule_activity, p_schedule_planned_activity, p_schedule_cancelled, + p_schedule_false_destination_tiploc, p_schedule_platform) + RETURNING id INTO v_locid; + RETURN v_locid; + EXCEPTION WHEN unique_violation THEN + -- Work out which constraint caused this. + GET STACKED DIAGNOSTICS v_constraint_name = CONSTRAINT_NAME; + CASE v_constraint_name + WHEN 'train_locations_tiploc_wtd' THEN + UPDATE train_locations SET + calling_point=p_calling_point, + schedule_public_arrival=p_schedule_public_arrival, schedule_working_arrival=p_schedule_working_arrival, + schedule_public_departure=p_schedule_public_departure, /* wtd */ + schedule_working_pass=p_schedule_working_pass, + schedule_route_delay=p_schedule_route_delay, schedule_activity=p_schedule_activity, schedule_planned_activity=p_schedule_planned_activity, schedule_cancelled=p_schedule_cancelled, + schedule_false_destination_tiploc=p_schedule_false_destination_tiploc, schedule_platform=p_schedule_platform, + active_in_schedule=true + WHERE + tsid=p_tsid AND tiploc=p_tiploc AND schedule_working_departure=p_schedule_working_departure + RETURNING id INTO v_locid; + IF found THEN RETURN v_locid; END IF; + -- We didn't find it? Try again. + WHEN 'train_locations_tiploc_ptd' THEN + UPDATE train_locations SET + calling_point=p_calling_point, + schedule_public_arrival=p_schedule_public_arrival, schedule_working_arrival=p_schedule_working_arrival, + /* ptd */ schedule_working_departure=p_schedule_working_departure, + schedule_working_pass=p_schedule_working_pass, + schedule_route_delay=p_schedule_route_delay, schedule_activity=p_schedule_activity, schedule_planned_activity=p_schedule_planned_activity, schedule_cancelled=p_schedule_cancelled, + schedule_false_destination_tiploc=p_schedule_false_destination_tiploc, schedule_platform=p_schedule_platform, + active_in_schedule=true + WHERE + tsid=p_tsid AND tiploc=p_tiploc AND schedule_public_departure=p_schedule_public_departure + RETURNING id INTO v_locid; + IF found THEN RETURN v_locid; END IF; + -- We didn't find it? Try again. + WHEN 'train_locations_tiploc_wta' THEN + UPDATE train_locations SET + calling_point=p_calling_point, + schedule_public_arrival=p_schedule_public_arrival, /* wta */ + schedule_public_departure=p_schedule_public_departure, schedule_working_departure=p_schedule_working_departure, + schedule_working_pass=p_schedule_working_pass, + schedule_route_delay=p_schedule_route_delay, schedule_activity=p_schedule_activity, schedule_planned_activity=p_schedule_planned_activity, schedule_cancelled=p_schedule_cancelled, + schedule_false_destination_tiploc=p_schedule_false_destination_tiploc, schedule_platform=p_schedule_platform, + active_in_schedule=true + WHERE + tsid=p_tsid AND tiploc=p_tiploc AND schedule_working_arrival=p_schedule_working_arrival + RETURNING id INTO v_locid; + IF found THEN RETURN v_locid; END IF; + -- We didn't find it? Try again. + WHEN 'train_locations_tiploc_pta' THEN + UPDATE train_locations SET + calling_point=p_calling_point, + /* pta */ schedule_working_arrival=p_schedule_working_arrival, + schedule_public_departure=p_schedule_public_departure, schedule_working_departure=p_schedule_working_departure, + schedule_working_pass=p_schedule_working_pass, + schedule_route_delay=p_schedule_route_delay, schedule_activity=p_schedule_activity, schedule_planned_activity=p_schedule_planned_activity, schedule_cancelled=p_schedule_cancelled, + schedule_false_destination_tiploc=p_schedule_false_destination_tiploc, schedule_platform=p_schedule_platform, + active_in_schedule=true + WHERE + tsid=p_tsid AND tiploc=p_tiploc AND schedule_public_arrival=p_schedule_public_arrival + RETURNING id INTO v_locid; + IF found THEN RETURN v_locid; END IF; + -- We didn't find it? Try again. + WHEN 'train_locations_tiploc_wtp' THEN + UPDATE train_locations SET + calling_point=p_calling_point, + schedule_public_arrival=p_schedule_public_arrival, schedule_working_arrival=p_schedule_working_arrival, + schedule_public_departure=p_schedule_public_departure, schedule_working_departure=p_schedule_working_departure, + /* wtp */ + schedule_route_delay=p_schedule_route_delay, schedule_activity=p_schedule_activity, schedule_planned_activity=p_schedule_planned_activity, schedule_cancelled=p_schedule_cancelled, + schedule_false_destination_tiploc=p_schedule_false_destination_tiploc, schedule_platform=p_schedule_platform, + active_in_schedule=true + WHERE + tsid=p_tsid AND tiploc=p_tiploc AND schedule_working_pass=p_schedule_working_pass + RETURNING id INTO v_locid; + IF found THEN RETURN v_locid; END IF; + -- We didn't find it? Try again. + WHEN 'train_locations_tiploc_wtd' THEN + UPDATE train_locations SET + calling_point=p_calling_point, + schedule_public_arrival=p_schedule_public_arrival, schedule_working_arrival=p_schedule_working_arrival, + schedule_public_departure=p_schedule_public_departure, schedule_working_departure=p_schedule_working_departure, + schedule_working_pass=p_schedule_working_pass, + schedule_route_delay=p_schedule_route_delay, schedule_activity=p_schedule_activity, schedule_planned_activity=p_schedule_planned_activity, schedule_cancelled=p_schedule_cancelled, + schedule_false_destination_tiploc=p_schedule_false_destination_tiploc, schedule_platform=p_schedule_platform, + active_in_schedule=true + WHERE + tsid=p_tsid AND tiploc=p_tiploc AND schedule_working_departure=p_schedule_working_departure + RETURNING id INTO v_locid; + IF found THEN RETURN v_locid; END IF; + -- We didn't find it? Try again. + ELSE + RAISE; -- don't know how to deal with this particular constraint. + END CASE; + END; + END LOOP; +END; +$$ +LANGUAGE plpgsql; + +CREATE TABLE train_alerts ( + id BIGSERIAL NOT NULL PRIMARY KEY, + alert_id BIGINT NOT NULL, + + source VARCHAR(10) NOT NULL, + alert_text TEXT NOT NULL, + audience VARCHAR(15) NOT NULL, + alert_type VARCHAR(15) NOT NULL, + + send_alert_by_sms BOOLEAN NOT NULL, + send_alert_by_email BOOLEAN NOT NULL, + send_alert_by_twitter BOOLEAN NOT NULL +); + +CREATE TABLE train_alert_services ( + id BIGSERIAL NOT NULL PRIMARY KEY, + ta_id BIGINT NOT NULL REFERENCES train_alerts (id) ON DELETE CASCADE, + service_id BIGINT NOT NULL REFERENCES train_services (id) ON DELETE CASCADE, + crs VARCHAR(3) NULL, + + CONSTRAINT train_alert_services_by_service_id_crs UNIQUE (service_id, crs, ta_id) +); + +CREATE TABLE ref_tocs ( + toc VARCHAR(2) PRIMARY KEY NOT NULL, + name VARCHAR(256) NOT NULL, + url VARCHAR(512) NULL +); + +CREATE TABLE ref_locations ( + tiploc VARCHAR(7) PRIMARY KEY NOT NULL, + name VARCHAR(30) NULL, + override_name VARCHAR(30) NULL, + crs VARCHAR(3) NULL, + toc VARCHAR(2) NULL +); + +CREATE INDEX ref_locations_by_crs ON ref_locations (crs); + +CREATE TABLE ref_cancel_reasons ( + code INT NOT NULL PRIMARY KEY, + text VARCHAR(256) NOT NULL +); + +CREATE TABLE ref_late_running_reasons ( + code INT NOT NULL PRIMARY KEY, + text VARCHAR(256) NOT NULL +); + +CREATE TABLE ref_via ( + id BIGSERIAL NOT NULL PRIMARY KEY, + at_crs VARCHAR(3) NOT NULL, + dest_tiploc VARCHAR(7) NOT NULL, + loc1_tiploc VARCHAR(7) NOT NULL, + loc2_tiploc VARCHAR(7) NOT NULL DEFAULT '', + text VARCHAR(256) NOT NULL, + + CONSTRAINT ref_via_by_at_dest_loc1_loc2 UNIQUE (at_crs, dest_tiploc, loc1_tiploc, loc2_tiploc) +); + +CREATE TABLE ref_cis ( + code VARCHAR(4) NOT NULL PRIMARY KEY, + name VARCHAR(60) NOT NULL +); diff --git a/go/trains/darwin/darwindb/affected.go b/go/trains/darwin/darwindb/affected.go new file mode 100644 index 0000000000..e069f23d8c --- /dev/null +++ b/go/trains/darwin/darwindb/affected.go @@ -0,0 +1,65 @@ +package darwindb + +import ( + "fmt" + "strings" +) + +// Affected tracks the TIPLOCs, RIDs and TSIDs which have been updated. +type Affected struct { + TIPLOCs map[string]bool + RIDs map[string]bool + TSIDs map[int]bool +} + +// NewAffected creates a new Affected instance. +func NewAffected() *Affected { + return &Affected{ + TIPLOCs: make(map[string]bool), + RIDs: make(map[string]bool), + TSIDs: make(map[int]bool), + } +} + +// TIPLOC marks the given TIPLOC code as affected. +func (a *Affected) TIPLOC(t string) { + if a != nil { + a.TIPLOCs[t] = true + } +} + +// RID marks the given train RID identifier as affected. +func (a *Affected) RID(r string) { + if a != nil { + a.RIDs[r] = true + } +} + +// TSID marks the given train service ID (internal ID) as affected. +func (a *Affected) TSID(t int) { + if a != nil { + a.TSIDs[t] = true + } +} + +// Summary returns a string summary of affected assets. +func (a *Affected) Summary() string { + if a == nil { + return "change tracking disabled" + } + + var s []string + if len(a.TIPLOCs) > 0 { + s = append(s, fmt.Sprintf("%d TIPLOCs", len(a.TIPLOCs))) + } + if len(a.RIDs) > 0 { + s = append(s, fmt.Sprintf("%d RIDs", len(a.RIDs))) + } + if len(a.TSIDs) > 0 { + s = append(s, fmt.Sprintf("%d TSIDs", len(a.TSIDs))) + } + if len(s) == 0 { + return "no changes" + } + return fmt.Sprintf("%v", strings.Join(s, ", ")) +} diff --git a/go/trains/darwin/darwindb/darwindb.go b/go/trains/darwin/darwindb/darwindb.go new file mode 100644 index 0000000000..0e7b6bc247 --- /dev/null +++ b/go/trains/darwin/darwindb/darwindb.go @@ -0,0 +1,71 @@ +package darwindb + +import ( + "context" + "fmt" + "log" + + pgx "github.com/jackc/pgx/v4" + "hg.lukegb.com/lukegb/depot/go/trains/darwin" +) + +// handleDataResponse handles a Darwin DataResponse message (i.e. a SnapshotResponse or a UpdateResponse). +func handleDataResponse(ctx context.Context, tx pgx.Tx, dr *darwin.DataResponse, a *Affected) error { + for _, s := range dr.Schedule { + if err := handleSchedule(ctx, tx, &s, a, scheduleModeOverwrite); err != nil { + return fmt.Errorf("handling schedule for RID %v: %w", s.RID, err) + } + } + for _, ts := range dr.TrainStatus { + if err := handleTrainStatus(ctx, tx, &ts, a); err != nil { + return fmt.Errorf("handling trainstatus for RID %v: %w", ts.RID, err) + } + } + for _, ds := range dr.Deactivated { + var tid int + row := tx.QueryRow(ctx, "UPDATE train_services SET active=FALSE WHERE rid=$1 RETURNING id", ds.RID) + if err := row.Scan(&tid); err == pgx.ErrNoRows { + log.Printf("deactivated schedule for unknown RID %v", ds.RID) + } else if err != nil { + return fmt.Errorf("handling deactivateschedule for RID %v: %w", ds.RID, err) + } + a.RID(ds.RID) + a.TSID(tid) + } + return nil +} + +// ProcessTimetable updates the database by processing a PportTimetable message in the given transaction. +func ProcessTimetable(ctx context.Context, tx pgx.Tx, ppt *darwin.PushPortTimetable, a *Affected) error { + for _, s := range ppt.Journey { + if err := handleSchedule(ctx, tx, &s, a, scheduleModeIgnoreExisting); err != nil { + return fmt.Errorf("handling timetable journey for RID %v: %w", s.RID, err) + } + } + + return nil +} + +// Process updates the database by processing a single PushPort message in the given transaction. +func Process(ctx context.Context, tx pgx.Tx, pp *darwin.PushPort, a *Affected) error { + drs := make([]*darwin.DataResponse, 0, len(pp.SnapshotResp)+len(pp.UpdateResp)) + for _, r := range pp.SnapshotResp { + drs = append(drs, &r.DataResponse) + } + for _, r := range pp.UpdateResp { + drs = append(drs, &r.DataResponse) + } + + if len(drs) == 0 { + // Short-circuit if we don't have anything we're interested in - which is very unlikely. + return nil + } + + for _, dr := range drs { + if err := handleDataResponse(ctx, tx, dr, a); err != nil { + return fmt.Errorf("handling data response: %w", err) + } + } + + return nil +} diff --git a/go/trains/darwin/darwindb/ddbref.go b/go/trains/darwin/darwindb/ddbref.go new file mode 100644 index 0000000000..026f50ad67 --- /dev/null +++ b/go/trains/darwin/darwindb/ddbref.go @@ -0,0 +1,103 @@ +package darwindb + +import ( + "context" + "fmt" + + pgx "github.com/jackc/pgx/v4" + "hg.lukegb.com/lukegb/depot/go/trains/darwin" +) + +// ProcessReferenceData updates the database by processing a PportReferenceData message in the given transaction. +func ProcessReferenceData(ctx context.Context, tx pgx.Tx, pprd *darwin.PushPortReferenceData, a *Affected) error { + crsToTIPLOC := make(map[string]string) + for _, loc := range pprd.Locations { + if loc.CRS != nil { + crsToTIPLOC[*loc.CRS] = loc.TIPLOC + } + _, err := tx.Exec(ctx, ` +INSERT INTO ref_locations +(tiploc, name, crs, toc) +VALUES +($1, $2, $3, $4) +ON CONFLICT (tiploc) DO UPDATE +SET name=COALESCE(ref_locations.override_name, $2), crs=$3, toc=$4 +`, loc.TIPLOC, loc.Name, loc.CRS, loc.TOC) + if err != nil { + return fmt.Errorf("updating location %q: %w", loc.TIPLOC, err) + } + a.TIPLOC(loc.TIPLOC) + } + for _, toc := range pprd.TOCs { + _, err := tx.Exec(ctx, ` +INSERT INTO ref_tocs +(toc, name, url) +VALUES +($1, $2, $3) +ON CONFLICT (toc) DO UPDATE +SET name=$2, url=$3 +`, toc.TOC, toc.Name, toc.URL) + if err != nil { + return fmt.Errorf("updating TOC %q: %w", toc.TOC, err) + } + } + for _, r := range pprd.LateRunningReasons { + _, err := tx.Exec(ctx, ` +INSERT INTO ref_late_running_reasons +(code, text) +VALUES +($1, $2) +ON CONFLICT (code) DO UPDATE +SET text=$2 +`, r.Code, r.Text) + if err != nil { + return fmt.Errorf("updating LateRunningReason %q: %w", r.Code, err) + } + } + for _, r := range pprd.CancellationReasons { + _, err := tx.Exec(ctx, ` +INSERT INTO ref_cancel_reasons +(code, text) +VALUES +($1, $2) +ON CONFLICT (code) DO UPDATE +SET text=$2 +`, r.Code, r.Text) + if err != nil { + return fmt.Errorf("updating CancellationReason %q: %w", r.Code, err) + } + } + for _, v := range pprd.Via { + _, err := tx.Exec(ctx, ` +INSERT INTO ref_via +(at_crs, dest_tiploc, loc1_tiploc, loc2_tiploc, text) +VALUES +($1, $2, $3, $4, $5) +ON CONFLICT (at_crs, dest_tiploc, loc1_tiploc, loc2_tiploc) DO UPDATE +SET text=$5 +RETURNING at_crs +`, v.AtCRS, v.DestTIPLOC, v.Loc1TIPLOC, v.Loc2TIPLOC, v.Text) + if err != nil { + return fmt.Errorf("updating Via %v/%v/%v/%v: %w", v.AtCRS, v.DestTIPLOC, v.Loc1TIPLOC, v.Loc2TIPLOC, err) + } + tiploc, ok := crsToTIPLOC[v.AtCRS] + if ok { + a.TIPLOC(tiploc) + } + } + for _, cis := range pprd.CISSource { + _, err := tx.Exec(ctx, ` +INSERT INTO ref_cis +(code, name) +VALUES +($1, $2) +ON CONFLICT (code) DO UPDATE +SET name=$2 +`, cis.CISCode, cis.Name) + if err != nil { + return fmt.Errorf("updating CISSource %v: %w", cis.CISCode, err) + } + } + + return nil +} diff --git a/go/trains/darwin/darwindb/ddbschedule.go b/go/trains/darwin/darwindb/ddbschedule.go new file mode 100644 index 0000000000..0695e00702 --- /dev/null +++ b/go/trains/darwin/darwindb/ddbschedule.go @@ -0,0 +1,112 @@ +package darwindb + +import ( + "context" + "encoding/json" + "fmt" + "log" + + pgx "github.com/jackc/pgx/v4" + "hg.lukegb.com/lukegb/depot/go/trains/darwin" +) + +type scheduleMode int + +const ( + scheduleModeOverwrite scheduleMode = iota + scheduleModeIgnoreExisting +) + +// handleSchedule processes a Darwin Schedule. +func handleSchedule(ctx context.Context, tx pgx.Tx, s *darwin.Schedule, a *Affected, sm scheduleMode) error { + startTS, err := startTime(s) + if err != nil { + return fmt.Errorf("finding start time for RID %v: %w", s.RID, err) + } + + var cancelCode, cancelTIPLOC *string + var cancelTIPLOCNear *bool + if s.CancellationReason != nil { + cancelCode = &s.CancellationReason.Code + cancelTIPLOC = &s.CancellationReason.TIPLOC + cancelTIPLOCNear = &s.CancellationReason.Near + } + sql := ` +INSERT INTO train_services +(rid, uid, rsid, headcode, scheduled_start_date, effective_start_time, + train_operator, status, train_category, is_passenger_svc, is_charter, is_q_train, + cancellation_reason_code, cancellation_reason_tiploc, cancellation_reason_tiploc_near, + active, deleted, cancelled) +VALUES +($1, $2, $3, $4, $5, $6, + $7, COALESCE($8, 'P'), COALESCE($9, 'OO'), COALESCE($10, true), COALESCE($11, false), COALESCE($12, false), + $13, $14, $15, + COALESCE($16, true), $17, COALESCE($18, false)) +ON CONFLICT (rid)` + switch sm { + case scheduleModeOverwrite: + sql += ` DO UPDATE SET +uid=$2, rsid=$3, headcode=$4, scheduled_start_date=$5, effective_start_time=$6, +train_operator=$7, status=COALESCE($8, 'P'), train_category=COALESCE($9, 'OO'), is_passenger_svc=COALESCE($10, true), is_charter=COALESCE($11, false), is_q_train=COALESCE($12, train_services.is_q_train), +cancellation_reason_code=$13, cancellation_reason_tiploc=$14, cancellation_reason_tiploc_near=$15, +active=COALESCE($16, 'true'), deleted=$17, cancelled=COALESCE($18, 'false') +` + case scheduleModeIgnoreExisting: + sql += ` DO NOTHING +` + } + sql += `RETURNING id` + row := tx.QueryRow(ctx, sql, + s.RID, s.UID, s.RSID, s.TrainID, s.SSD, startTS, + s.TOC, s.Status, s.TrainCat, s.IsPassengerSvc, s.IsCharter, s.IsQTrain, + cancelCode, cancelTIPLOC, cancelTIPLOCNear, + s.IsActive, s.Deleted, s.Cancelled) + var serviceID int + if err := row.Scan(&serviceID); err == pgx.ErrNoRows && sm == scheduleModeIgnoreExisting { + // The row existed already. + return nil + } else if err != nil { + log.Printf("inserting schedule for RID %v: %v", s.RID, err) + return fmt.Errorf("inserting schedule for RID %v: %w", s.RID, err) + } + a.RID(s.RID) + a.TSID(serviceID) + + // Insert/update all the locations. + var activeLocations []int + for _, cp := range s.CallingPoints { + var locid int + row := tx.QueryRow(ctx, ` +SELECT train_locations_upsert_from_schedule( +p_tsid => $1, p_rid => $2, p_tiploc => $3, p_calling_point => $4, +p_schedule_public_arrival => $5, p_schedule_working_arrival => $6, +p_schedule_public_departure => $7, p_schedule_working_departure => $8, +p_schedule_working_pass => $9, +p_schedule_route_delay => $10, p_schedule_activity => $11, p_schedule_planned_activity => $12, p_schedule_cancelled => $13, +p_schedule_false_destination_tiploc => $14, p_schedule_platform => $15) +`, + serviceID, s.RID, cp.TIPLOC, cp.XMLName.Local, + timeToTimestamp(cp.PTA, startTS), timeToTimestamp(cp.WTA, startTS), + timeToTimestamp(cp.PTD, startTS), timeToTimestamp(cp.WTD, startTS), + timeToTimestamp(cp.WTP, startTS), + cp.RDelay, cp.Act, cp.PlanAct, cp.Cancelled, + cp.FD, cp.Platform, + ) + if err := row.Scan(&locid); err != nil { + log.Printf("updating location %v from RID %v/my ID %v: %v", cp.TIPLOC, s.RID, serviceID, err) + zz, _ := json.MarshalIndent(s, "", " ") + log.Println(string(zz)) + zz, _ = json.MarshalIndent(cp, "", " ") + log.Println(string(zz)) + return fmt.Errorf("updating location %v from RID %v/my ID %v: %w", cp.TIPLOC, s.RID, serviceID, err) + } else { + activeLocations = append(activeLocations, locid) + } + a.TIPLOC(cp.TIPLOC) + } + if _, err := tx.Exec(ctx, "UPDATE train_locations SET active_in_schedule=false WHERE tsid=$1 AND NOT (id = ANY ($2))", serviceID, activeLocations); err != nil { + return fmt.Errorf("marking old locations as removed from schedule for RID %v/my ID %v (active locations: %v): %w", s.RID, serviceID, activeLocations, err) + } + fmt.Printf("s") + return nil +} diff --git a/go/trains/darwin/darwindb/ddbtrainstatus.go b/go/trains/darwin/darwindb/ddbtrainstatus.go new file mode 100644 index 0000000000..9e3297d803 --- /dev/null +++ b/go/trains/darwin/darwindb/ddbtrainstatus.go @@ -0,0 +1,130 @@ +package darwindb + +import ( + "context" + "fmt" + "log" + "strings" + "time" + + pgx "github.com/jackc/pgx/v4" + "hg.lukegb.com/lukegb/depot/go/trains/darwin" +) + +// handleTrainStatus handles a Darwin "TS" (train status) message. +func handleTrainStatus(ctx context.Context, tx pgx.Tx, ts *darwin.TrainStatus, a *Affected) error { + var tsid int + var tsEST time.Time + if ts.LateReason != nil { + row := tx.QueryRow(ctx, ` +UPDATE train_services +SET delay_reason_code=$2, delay_reason_tiploc=$3, delay_reason_tiploc_near=$4 +WHERE rid=$1 +RETURNING id, effective_start_time +`, ts.RID, ts.LateReason.Code, ts.LateReason.TIPLOC, ts.LateReason.Near) + if err := row.Scan(&tsid, &tsEST); err == pgx.ErrNoRows { + log.Printf("got TrainStatus (delayed) for RID %v which isn't known", ts.RID) + return nil + } else if err != nil { + return fmt.Errorf("updating delay reason: %w", err) + } + } else { + row := tx.QueryRow(ctx, "SELECT id, effective_start_time FROM train_services WHERE rid=$1", ts.RID) + if err := row.Scan(&tsid, &tsEST); err == pgx.ErrNoRows { + log.Printf("got TrainStatus for RID %v which isn't known", ts.RID) + return nil + } else if err != nil { + return fmt.Errorf("fetching train_services ID: %w", err) + } + } + a.RID(ts.RID) + a.TSID(tsid) + + for _, l := range ts.Location { + // OK, this is a bit hairy. We only get updates for things which have _changed_. + var els []interface{} + addEl := func(x interface{}) int { + els = append(els, x) + return len(els) + } + addSet := func(s string, x interface{}) string { + return fmt.Sprintf("%s=$%d", s, addEl(x)) + } + addTSTimeData := func(prefix string, td *darwin.TSTimeData) []string { + if td == nil { + return nil + } + x := []interface{}{ + "estimated_%s", timeToTimestamp(td.ET, tsEST), + "working_estimated_%s", timeToTimestamp(td.WET, tsEST), + "actual_%s", timeToTimestamp(td.AT, tsEST), + "%s_data", td, + } + out := make([]string, len(x)/2) + for n := 0; n < len(out); n++ { + out[n] = fmt.Sprintf(x[n*2].(string)+"=$%d", prefix, addEl(x[n*2+1])) + } + return out + } + var setClause []string + setClause = append(setClause, addTSTimeData("arrival", l.Arr)...) + setClause = append(setClause, addTSTimeData("departure", l.Dep)...) + setClause = append(setClause, addTSTimeData("pass", l.Pass)...) + if l.Plat != nil { + setClause = append(setClause, addSet("platform", l.Plat.Platform)) + setClause = append(setClause, addSet("platform_suppressed", l.Plat.PlatSup)) + setClause = append(setClause, addSet("cis_platform_suppression", l.Plat.CisPlatSup)) + platsrc := map[string]string{ + "P": "planned", + "A": "automatic", + "M": "manual", + "": "planned", + }[l.Plat.PlatSrc] + setClause = append(setClause, addSet("platform_source", platsrc)) + setClause = append(setClause, addSet("platform_confirmed", l.Plat.Conf)) + } + if l.Suppr != nil { + setClause = append(setClause, addSet("service_suppressed", *l.Suppr)) + } + if l.Length != nil { + setClause = append(setClause, addSet("train_length", *l.Length)) + } + if len(setClause) == 0 { + // Nothing to set for this location. + continue + } + + var whereClause []string + for columnName, tsValue := range map[string]string{ + "schedule_public_arrival": l.PTA, + "schedule_working_arrival": l.WTA, + "schedule_public_departure": l.PTD, + "schedule_working_departure": l.WTD, + "schedule_working_pass": l.WTP, + } { + if tsValue == "" { + continue + } + whereClause = append(whereClause, addSet(columnName, timeToTimestamp(tsValue, tsEST))) + } + if len(whereClause) == 0 { + return fmt.Errorf("RID %v/my ID %v at TIPLOC %v: ended up with no CircularTimes-based WHERE", ts.RID, tsid, l.TIPLOC) + } + whereClause = append(whereClause, addSet("tiploc", l.TIPLOC)) + whereClause = append(whereClause, addSet("tsid", tsid)) + + query := fmt.Sprintf("UPDATE train_locations SET %s WHERE active_in_schedule AND %s", strings.Join(setClause, ", "), strings.Join(whereClause, " AND ")) + cmd, err := tx.Exec(ctx, query, els...) + if err != nil { + log.Printf("RID %v/my ID %v at TIPLOC %v: failed to update with query %q: %v", ts.RID, tsid, l.TIPLOC, query, err) + return fmt.Errorf("RID %v/my ID %v at TIPLOC %v: failed to update with query %q: %w", ts.RID, tsid, l.TIPLOC, query, err) + } + a.TIPLOC(l.TIPLOC) + if cmd.RowsAffected() != 1 { + return fmt.Errorf("RID %v/my ID %v at TIPLOC %v: query %q: wanted to update 1 row, updated %d", ts.RID, tsid, l.TIPLOC, query, cmd.RowsAffected()) + } + } + fmt.Printf("t") + + return nil +} diff --git a/go/trains/darwin/darwindb/default.nix b/go/trains/darwin/darwindb/default.nix new file mode 100644 index 0000000000..67de60a6f1 --- /dev/null +++ b/go/trains/darwin/darwindb/default.nix @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: 2020 Luke Granger-Brown +# +# SPDX-License-Identifier: Apache-2.0 + +{ depot, ... }@args: +depot.third_party.buildGo.package { + name = "darwindb"; + srcs = [ + ./affected.go + ./darwindb.go + ./ddbref.go + ./ddbschedule.go + ./ddbtrainstatus.go + ./tsutil.go + ]; + path = "hg.lukegb.com/lukegb/depot/go/trains/darwin/darwindb"; + deps = with depot.third_party; [ + gopkgs."github.com".jackc.pgx.v4 + depot.go.trains.darwin + ]; +} diff --git a/go/trains/darwin/darwindb/tsutil.go b/go/trains/darwin/darwindb/tsutil.go new file mode 100644 index 0000000000..3a1b786efb --- /dev/null +++ b/go/trains/darwin/darwindb/tsutil.go @@ -0,0 +1,74 @@ +package darwindb + +import ( + "fmt" + "time" + + "hg.lukegb.com/lukegb/depot/go/trains/darwin" +) + +// secondsify adds :00 onto the end of a string if it's HH:MM formatted, otherwise does nothing. +func secondsify(ts string) string { + if len(ts) == 5 { // xx:xx (no seconds) + return ts + ":00" + } + return ts +} + +// startTime takes a train and ascertains its "start time". +func startTime(s *darwin.Schedule) (time.Time, error) { + // We assume these are sensibly sorted by Darwin. + if len(s.CallingPoints) == 0 { + return time.Time{}, fmt.Errorf("no calling points") + } + cp := s.CallingPoints[0] + + var ts string + tsCandidates := []string{cp.WTD, cp.PTD, cp.WTP, cp.WTA, cp.PTA} + for _, tsc := range tsCandidates { + if tsc != "" { + ts = tsc + } + } + if ts == "" { + return time.Time{}, fmt.Errorf("failed to find a suitable timestamp in first calling point") + } + + ts = fmt.Sprintf("%v %v", s.SSD, secondsify(ts)) + + t, err := time.ParseInLocation("2006-01-02 15:04:05", ts, darwin.London) + if err != nil { + return time.Time{}, fmt.Errorf("parsing start time %q: %w", ts, err) + } + return t, nil +} + +// timeToTimestamp takes a string HH:MM and places it appropriately given the approximate 'start time' of the train. +func timeToTimestamp(ts string, startTime time.Time) *time.Time { + if ts == "" { + return nil + } + ts = secondsify(ts) + + t, err := time.ParseInLocation("2006-01-02 15:04:05", startTime.Format("2006-01-02 ")+ts, darwin.London) + if err != nil { + return nil + } + + // Follow Darwin rules for determining time skips + // https://wiki.openraildata.com/index.php/Darwin:Schedule_Element#Ordering + difference := t.Sub(startTime) + switch { + case difference > -6*time.Hour && difference < 18*time.Hour: + return &t + case difference <= -6*time.Hour: + // e.g. 08:00 start time, 01:00 time "now" (-7 hrs) -- add a day + t = t.AddDate(0, 0, 1) + return &t + case difference >= 18*time.Hour: + // e.g. 01:00 start time, 22:00 time "now" (+18 hrs) -- sub a day + t = t.AddDate(0, 0, -1) + return &t + } + panic(fmt.Sprintf("unhandled difference %v", difference)) +} diff --git a/go/trains/darwin/darwingest/darwingest.go b/go/trains/darwin/darwingest/darwingest.go new file mode 100644 index 0000000000..70553803c4 --- /dev/null +++ b/go/trains/darwin/darwingest/darwingest.go @@ -0,0 +1,310 @@ +package darwingest + +import ( + "bytes" + "compress/gzip" + "context" + "encoding/xml" + "fmt" + "io" + "log" + "strings" + "time" + + "github.com/cenkalti/backoff/v4" + "github.com/go-stomp/stomp/v3" + "github.com/jlaffaye/ftp" + "gocloud.dev/blob" + "hg.lukegb.com/lukegb/depot/go/trains/darwin" + "hg.lukegb.com/lukegb/depot/go/trains/darwin/darwingest/darwingestftp" + "hg.lukegb.com/lukegb/depot/go/trains/darwin/darwingest/darwingests3" +) + +func degzip(b []byte) ([]byte, error) { + gzr, err := gzip.NewReader(bytes.NewBuffer(b)) + if err != nil { + return nil, fmt.Errorf("gzip.NewReader: %w", err) + } + d, err := io.ReadAll(gzr) + if err != nil { + return nil, fmt.Errorf("io.ReadAll: %w", err) + } + return d, nil +} + +func unmarshalGzipPushPort(m []byte) (*darwin.PushPort, error) { + d, err := degzip(m) + if err != nil { + return nil, fmt.Errorf("degzip: %w", err) + } + + var pp darwin.PushPort + if err := xml.Unmarshal(d, &pp); err != nil { + return nil, fmt.Errorf("xml.Unmarshal(%q): %w", string(d), err) + } + return &pp, nil +} + +func ridToDate(rid string) (time.Time, error) { + t, err := time.ParseInLocation("20060102", rid[0:8], darwin.London) + if err != nil { + return time.Time{}, err + } + if t.Hour() < 2 || (t.Hour() == 2 && t.Minute() < 45) { + t = t.AddDate(0, 0, -1) + } + return t, nil +} + +type Config struct { + Verbose bool + + DialStomp func(ctx context.Context) (*stomp.Conn, error) + StompSubscribe func(ctx context.Context, sc *stomp.Conn) (*stomp.Subscription, error) + DialFTP func(ctx context.Context) (*ftp.ServerConn, error) + Message func(pp *darwin.PushPort) error + MessageCatchUp func(pp *darwin.PushPort) error + Timetable func(ppt *darwin.PushPortTimetable) error + ReferenceData func(pprd *darwin.PushPortReferenceData) error + CatchUpComplete func(ctx context.Context) error +} + +func Start(ctx context.Context, bucket *blob.Bucket, cfg Config) error { + return run(ctx, bucket, cfg, false) +} + +func CatchUpAndStart(ctx context.Context, bucket *blob.Bucket, cfg Config) error { + return run(ctx, bucket, cfg, true) +} + +func run(ctx context.Context, bucket *blob.Bucket, cfg Config, runCatchup bool) error { + handleMsg := func(cb func(*darwin.PushPort) error) func(*darwin.PushPort) error { + return func(pp *darwin.PushPort) error { + for _, ttid := range pp.TimeTableID { + fn := strings.TrimSpace(ttid.Filename) + if fn != "" { + log.Printf("informed that there's a new timetable at %q", fn) + ppt, err := darwingests3.LoadTimetable(ctx, bucket, fn) + if err == darwingests3.ErrBadVersion { + continue + } + if err != nil { + return fmt.Errorf("LoadTimetable(%q): %w", fn, err) + } + if err := cfg.Timetable(ppt); err != nil { + return fmt.Errorf("Timetable(%q): %w", fn, err) + } + } + + fn = strings.TrimSpace(ttid.ReferenceFilename) + if fn != "" { + log.Printf("informed that there's a new refsheet at %q", fn) + rd, err := darwingests3.LoadReferenceData(ctx, bucket, fn) + if err == darwingests3.ErrBadVersion { + continue + } + if err != nil { + return fmt.Errorf("LoadReferenceData(%q): %w", fn, err) + } + if err := cfg.ReferenceData(rd); err != nil { + return fmt.Errorf("cfg.ReferenceData(%q): %w", fn, err) + } + } + } + return cb(pp) + } + } + + if runCatchup { + // Load the latest reference data. + rd, err := darwingests3.LoadLatestReferenceData(ctx, bucket) + if err != nil { + return fmt.Errorf("LoadLatestReferenceData: %w", err) + } + if err := cfg.ReferenceData(rd); err != nil { + return fmt.Errorf("cfg.ReferenceData: %w", err) + } + } + + stompBackoff := backoff.NewExponentialBackOff() + stompBackoff.MaxElapsedTime = 24 * time.Hour + + stompConn, err := cfg.DialStomp(ctx) + if err != nil { + return fmt.Errorf("DialStomp: %w", err) + } + defer stompConn.Disconnect() + + sub, err := cfg.StompSubscribe(ctx, stompConn) + if err != nil { + return fmt.Errorf("StompSubscribe: %w", err) + } + defer sub.Unsubscribe() + + drainConnection := func() { + if sub == nil { + return + } + go sub.Unsubscribe() + for m := range sub.C { + stompConn.Nack(m) + } + } + + if runCatchup { + if err := func() error { + log.Println("awaiting first message to find replay timestamp") + + // Find the start timestamp we need to replay up to. + m := <-sub.C + defer stompConn.Nack(m) + d, err := unmarshalGzipPushPort(m.Body) + if err != nil { + return fmt.Errorf("unmarshalGzipPushPort: %w", err) + } + log.Printf("first message is at %v", d.Time) + // Don't handle the message just yet. + + rids := d.AffectedRIDs() + var timetableDate time.Time + if len(rids) == 0 { + log.Printf("no affected RIDs in first message! falling back to time.Now...") + timetableDate = time.Now() + } else { + timetableDate, err = ridToDate(rids[0]) + if err != nil { + log.Printf("couldn't parse RID %q to date, falling back to time.Now: %v", rids[0], err) + timetableDate = time.Now() + } + } + + // Load the timetable first. + log.Println("loading initial timetable") + ppt, err := darwingests3.LoadTimetableForDate(ctx, bucket, timetableDate) + if err != nil { + return fmt.Errorf("LoadTimetableForDate: %w", err) + } + + log.Println("feeding timetable to database") + if err := cfg.Timetable(ppt); err != nil { + return fmt.Errorf("Timetable: %w", err) + } + + log.Println("connecting to FTP") + sc, err := cfg.DialFTP(ctx) + if err != nil { + return fmt.Errorf("DialFTP: %w", err) + } + defer sc.Quit() + + // Grab the full snapshot dump + log.Println("loading snapshot") + lw, _, err := darwingestftp.LoadSnapshot(ctx, sc, handleMsg(cfg.MessageCatchUp)) + if err != nil { + return fmt.Errorf("LoadSnapshot: %w", err) + } + + // Now try to "catch it up" using the push-port logs to the start timestamp of the message... + log.Println("loading logs") + if err := darwingestftp.LoadLogs(ctx, sc, lw, d.Time, handleMsg(cfg.MessageCatchUp)); err != nil { + return fmt.Errorf("LoadLogs: %w", err) + } + + log.Println("marking catchup complete") + if err := cfg.CatchUpComplete(ctx); err != nil { + return fmt.Errorf("CatchUpComplete callback: %w", err) + } + + log.Println("we think we've caught up! let's go...") + if err := handleMsg(cfg.Message)(d); err != nil { + return fmt.Errorf("replaying first pushport message: %w", err) + } + if err := stompConn.Ack(m); err != nil { + return fmt.Errorf("acking first pushport message: %w", err) + } + return nil + }(); err != nil { + drainConnection() + return err + } + } else { + if err := cfg.CatchUpComplete(ctx); err != nil { + return fmt.Errorf("CatchUpComplete callback: %w", err) + } + } + + mustReconnect := false + for { + if sub == nil { + mustReconnect = true + } + if mustReconnect { + duration := stompBackoff.NextBackOff() + if duration == backoff.Stop { + drainConnection() + return fmt.Errorf("exceeded stomp reconnection retries") + } + log.Printf("subscription appears to have closed, retrying after %v...", duration) + if err := darwin.Sleep(ctx, duration); err != nil { + drainConnection() + return err + } + + var err error + log.Printf("resubscribing...") + sub, err = cfg.StompSubscribe(ctx, stompConn) + if err != nil { + log.Printf("StompSubscribe while resubscribing: %v", err) + // Force close the server connection and dial again. + stompConn.MustDisconnect() + stompConn, err = cfg.DialStomp(ctx) + if err != nil { + log.Printf("DialStomp failed as well :(") + // Try again later. + continue + } + defer stompConn.Disconnect() + + sub, err = cfg.StompSubscribe(ctx, stompConn) + if err != nil { + log.Printf("StompSubscribe with new connection failed: %v", err) + // Try again later. + continue + } + } + defer sub.Unsubscribe() + mustReconnect = false + } + select { + case <-ctx.Done(): + return ctx.Err() + case m, ok := <-sub.C: + if !ok { + mustReconnect = true + continue + } + d, err := unmarshalGzipPushPort(m.Body) + if err != nil { + log.Printf("unmarshalGzipPushPort: %v", err) + if err := stompConn.Nack(m); err != nil { + log.Printf("attempting to nack unparsable message: %v", err) + } + continue + } + + if err := handleMsg(cfg.Message)(d); err != nil { + log.Printf("Message: %v", err) + if err := stompConn.Nack(m); err != nil { + log.Printf("attempting to nack unhandled message: %v", err) + } + continue + } + if err := stompConn.Ack(m); err != nil { + log.Printf("attempting to ack handled message: %v", err) + continue + } + + stompBackoff.Reset() + } + } +} diff --git a/go/trains/darwin/darwingest/darwingestftp/darwingestftp.go b/go/trains/darwin/darwingest/darwingestftp/darwingestftp.go new file mode 100644 index 0000000000..f5d6677131 --- /dev/null +++ b/go/trains/darwin/darwingest/darwingestftp/darwingestftp.go @@ -0,0 +1,169 @@ +package darwingestftp + +import ( + "compress/gzip" + "context" + "encoding/xml" + "errors" + "fmt" + "io" + "log" + "sort" + "time" + + "github.com/jlaffaye/ftp" + "hg.lukegb.com/lukegb/depot/go/trains/darwin" +) + +func loadGzippedFile(ctx context.Context, sc *ftp.ServerConn, fn string, cb func(pp *darwin.PushPort) error) error { + log.Printf("fetching %v", fn) + resp, err := sc.Retr(fn) + if err != nil { + return fmt.Errorf("Retr(%q): %w", fn, err) + } + defer resp.Close() + + respGz, err := gzip.NewReader(resp) + if err != nil { + return fmt.Errorf("gzip.NewReader from %q: %w", fn, err) + } + defer respGz.Close() + + xmlDec := xml.NewDecoder(respGz) + for { + if ctx.Err() != nil { + return ctx.Err() + } + + var pp darwin.PushPort + if err := xmlDec.Decode(&pp); errors.Is(err, io.EOF) { + break + } else if err != nil { + return fmt.Errorf("xmlDec.Decode from %q: %w", fn, err) + } + + if err := cb(&pp); err != nil { + return fmt.Errorf("processing %q: %w", fn, err) + } + } + return nil +} + +func LoadSnapshot(ctx context.Context, sc *ftp.ServerConn, cb func(*darwin.PushPort) error) (lowWater, highWater time.Time, err error) { + log.Println("fetching snapshot...") + err = loadGzippedFile(ctx, sc, "/snapshot/snapshot.gz", func(pp *darwin.PushPort) error { + if lowWater.IsZero() || lowWater.After(pp.Time) { + lowWater = pp.Time + } + if highWater.IsZero() || highWater.Before(pp.Time) { + highWater = pp.Time + } + return cb(pp) + }) + if err != nil { + return time.Time{}, time.Time{}, err + } + return lowWater, highWater, nil +} + +type logFTPFile struct { + Filename string + EndAt time.Time +} + +func LoadLogs(ctx context.Context, sc *ftp.ServerConn, startAt, endAt time.Time, cb func(*darwin.PushPort) error) error { + firstIt := true + for { + if ctx.Err() != nil { + return ctx.Err() + } + + log.Println("fetching logs starting at", startAt) + entries, err := sc.List("/pushport") + if err != nil { + return fmt.Errorf("listing /pushport: %w", err) + } + + var fs []logFTPFile + for _, e := range entries { + if e.Type != ftp.EntryTypeFile { + continue + } + + t, err := time.ParseInLocation("pPortData.log.2006-01-02_1504.gz", e.Name, darwin.London) + if err != nil { + log.Println("failed to parse filename %q: %v", err) + continue + } + + fs = append(fs, logFTPFile{ + Filename: e.Name, + EndAt: t, + }) + } + + sort.Slice(fs, func(i, j int) bool { + return fs[i].EndAt.Before(fs[j].EndAt) + }) + + startReadingFrom := -1 + endReadingAt := len(fs) - 1 + for n, f := range fs { + if f.EndAt.After(startAt) && startReadingFrom == -1 { + startReadingFrom = n + } + if f.EndAt.After(endAt) && !endAt.IsZero() { + endReadingAt = n + break + } + } + + var lowWater, highWater time.Time + if startReadingFrom != -1 { + for n := startReadingFrom; n <= endReadingAt; n++ { + fmt.Printf("%v: ", fs[n].Filename) + err := loadGzippedFile(ctx, sc, fmt.Sprintf("/pushport/%v", fs[n].Filename), func(pp *darwin.PushPort) error { + if lowWater.IsZero() || lowWater.After(pp.Time) { + lowWater = pp.Time + } + if highWater.IsZero() || highWater.Before(pp.Time) { + highWater = pp.Time + } + if !pp.Time.After(startAt) { + fmt.Printf("x") + return nil + } else if !endAt.IsZero() && (endAt.Before(pp.Time) || endAt.Equal(pp.Time)) { + fmt.Printf("_") + return nil + } + return cb(pp) + }) + if err != nil { + return err + } + fmt.Print("\n") + } + + if !lowWater.Before(startAt) { + // Don't believe that we saw everything :/ + log.Printf("low water was %v; start at was %v; we might not have seen everything!", lowWater, startAt) + } + } + + if !highWater.After(endAt) { + if !firstIt { + log.Printf("not yet caught up: high water %v; end at %v; waiting 1 minute...", highWater, endAt) + if err := darwin.Sleep(ctx, 1*time.Minute); err != nil { + return err + } + } + firstIt = false + if !highWater.IsZero() { + startAt = highWater + } + continue + } + + return nil + } +} diff --git a/go/trains/darwin/darwingest/darwingestftp/default.nix b/go/trains/darwin/darwingest/darwingestftp/default.nix new file mode 100644 index 0000000000..3134a2ee2f --- /dev/null +++ b/go/trains/darwin/darwingest/darwingestftp/default.nix @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2020 Luke Granger-Brown +# +# SPDX-License-Identifier: Apache-2.0 + +{ depot, ... }@args: +depot.third_party.buildGo.package { + name = "darwingestftp"; + srcs = [ + ./darwingestftp.go + ]; + path = "hg.lukegb.com/lukegb/depot/go/trains/darwin/darwingest/darwingestftp"; + deps = with depot.third_party; [ + gopkgs."github.com".jlaffaye.ftp + + depot.go.trains.darwin + ]; +} diff --git a/go/trains/darwin/darwingest/darwingests3/darwingests3.go b/go/trains/darwin/darwingest/darwingests3/darwingests3.go new file mode 100644 index 0000000000..bd3f114b2c --- /dev/null +++ b/go/trains/darwin/darwingest/darwingests3/darwingests3.go @@ -0,0 +1,214 @@ +package darwingests3 + +import ( + "compress/gzip" + "context" + "encoding/xml" + "errors" + "fmt" + "io" + "log" + "regexp" + "sort" + "strconv" + "time" + + "gocloud.dev/blob" + "hg.lukegb.com/lukegb/depot/go/trains/darwin" +) + +var ( + blobRegex = regexp.MustCompile(`^(?P[0-9]{14})(_ref)?_v(?P[0-9]+)\.xml\.gz$`) +) + +const ( + understoodVersionTimetable = 8 + understoodVersionReference = 3 +) + +type fileType int + +const ( + fileTypeWantAny fileType = iota + fileTypeTimetable + fileTypeReference +) + +func (ft fileType) IsA(wantFileType fileType) bool { + switch wantFileType { + case fileTypeWantAny: + return true + case ft: + return true + default: + return false + } +} + +type fileInfo struct { + filename string + fileType fileType + version int + filenameDate time.Time +} + +func parseFilename(fn string) (fileInfo, error) { + m := blobRegex.FindStringSubmatch(fn) + if m == nil { + return fileInfo{}, fmt.Errorf("couldn't parse filename %q: regex didn't match", fn) + } + + filenameDateStr := m[1] + filenameDate, err := time.ParseInLocation("20060102150405", filenameDateStr, darwin.London) + if err != nil { + return fileInfo{}, fmt.Errorf("couldn't parse blob filename %q: datetime part %q unparsable: %v", fn, filenameDateStr, err) + } + + versionStr := m[3] + version, err := strconv.Atoi(versionStr) + if err != nil { + return fileInfo{}, fmt.Errorf("couldn't parse blob filename %q: version part %q unparsable: %v", fn, versionStr, err) + } + + fileType := fileTypeTimetable + if m[2] != "" { + fileType = fileTypeReference + } + + return fileInfo{ + filename: fn, + fileType: fileType, + version: version, + filenameDate: filenameDate, + }, nil +} + +func filesInBucket(ctx context.Context, bucket *blob.Bucket, wantFileType fileType) ([]fileInfo, error) { + var fis []fileInfo + + iter := bucket.List(nil) + for { + obj, err := iter.Next(ctx) + if err == io.EOF { + break + } + if err != nil { + return nil, fmt.Errorf("listing objects in bucket: %w", err) + } + + fi, err := parseFilename(obj.Key) + if err != nil { + log.Print(err) + continue + } + + if !fi.fileType.IsA(wantFileType) { + continue + } + + fis = append(fis, fi) + } + + return fis, nil +} + +func LoadTimetableForDate(ctx context.Context, bucket *blob.Bucket, ts time.Time) (*darwin.PushPortTimetable, error) { + // Find the latest filename that covers "today". + // This is a little tricky, and probably wrong, but we'll try and be good enough. + fis, err := filesInBucket(ctx, bucket, fileTypeTimetable) + if err != nil { + return nil, fmt.Errorf("filesInBucket: %w", err) + } + + // OK, find one where the date matches... + wanty, wantm, wantd := ts.Date() + var got *fileInfo + for _, fi := range fis { + if fi.version != understoodVersionTimetable { + continue + } + fiy, fim, fid := fi.filenameDate.Date() + if wanty == fiy && wantm == fim && wantd == fid { + fi := fi + got = &fi + break + } + } + if got == nil { + return nil, fmt.Errorf("unable to find matching timetable for %v") + } + + return LoadTimetable(ctx, bucket, got.filename) +} + +func decodeFromBucket(ctx context.Context, bucket *blob.Bucket, filename string, out interface{}) error { + r, err := bucket.NewReader(ctx, filename, nil) + if err != nil { + return fmt.Errorf("NewReader(%q): %w", filename, err) + } + defer r.Close() + + gzr, err := gzip.NewReader(r) + if err != nil { + return fmt.Errorf("gzip.NewReader(fn=%q): %w", filename, err) + } + defer gzr.Close() + + if err := xml.NewDecoder(gzr).Decode(out); err != nil { + return fmt.Errorf("decoding XML from %q: %w", filename, err) + } + + return nil +} + +var ErrBadVersion = errors.New("darwingests3: bad version, ignoring request") + +func LoadTimetable(ctx context.Context, bucket *blob.Bucket, filename string) (*darwin.PushPortTimetable, error) { + log.Printf("loading timetable from S3 file %q", filename) + fi, err := parseFilename(filename) + if err != nil { + return nil, err + } + if fi.version != understoodVersionTimetable { + return nil, ErrBadVersion + } + var ppt darwin.PushPortTimetable + if err := decodeFromBucket(ctx, bucket, filename, &ppt); err != nil { + return nil, err + } + return &ppt, nil +} + +func LoadLatestReferenceData(ctx context.Context, bucket *blob.Bucket) (*darwin.PushPortReferenceData, error) { + fis, err := filesInBucket(ctx, bucket, fileTypeReference) + if err != nil { + return nil, fmt.Errorf("filesInBucket: %w", err) + } + + sort.Slice(fis, func(i, j int) bool { + if fis[i].version == understoodVersionReference && fis[j].version != understoodVersionReference { + return true + } else if fis[i].version != understoodVersionReference && fis[j].version == understoodVersionReference { + return false + } + return fis[i].filenameDate.Before(fis[j].filenameDate) + }) + + return LoadReferenceData(ctx, bucket, fis[0].filename) +} + +func LoadReferenceData(ctx context.Context, bucket *blob.Bucket, filename string) (*darwin.PushPortReferenceData, error) { + log.Printf("loading reference data from S3 file %q", filename) + fi, err := parseFilename(filename) + if err != nil { + return nil, err + } + if fi.version != understoodVersionReference { + return nil, ErrBadVersion + } + var pprd darwin.PushPortReferenceData + if err := decodeFromBucket(ctx, bucket, filename, &pprd); err != nil { + return nil, err + } + return &pprd, nil +} diff --git a/go/trains/darwin/darwingest/darwingests3/default.nix b/go/trains/darwin/darwingest/darwingests3/default.nix new file mode 100644 index 0000000000..b5fe56b049 --- /dev/null +++ b/go/trains/darwin/darwingest/darwingests3/default.nix @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2020 Luke Granger-Brown +# +# SPDX-License-Identifier: Apache-2.0 + +{ depot, ... }@args: +depot.third_party.buildGo.package { + name = "darwingests3"; + srcs = [ + ./darwingests3.go + ]; + path = "hg.lukegb.com/lukegb/depot/go/trains/darwin/darwingest/darwingests3"; + deps = with depot.third_party; [ + gopkgs."gocloud.dev".blob + + depot.go.trains.darwin + ]; +} diff --git a/go/trains/darwin/darwingest/darwingeststomp/darwingeststomp.go b/go/trains/darwin/darwingest/darwingeststomp/darwingeststomp.go new file mode 100644 index 0000000000..2b6174668b --- /dev/null +++ b/go/trains/darwin/darwingest/darwingeststomp/darwingeststomp.go @@ -0,0 +1,34 @@ +package darwingeststomp + +import ( + "context" + "time" + + stomp "github.com/go-stomp/stomp/v3" +) + +type Config struct { + Hostname string + Username, Password string + ClientID string +} + +func Dial(ctx context.Context, cfg Config) (*stomp.Conn, error) { + const maxExpectedQPS = 60 + var maxExpectedDowntime = int((10 * time.Minute).Seconds()) + return stomp.Dial( + "tcp", cfg.Hostname, + stomp.ConnOpt.Login(cfg.Username, cfg.Password), + stomp.ConnOpt.Header("client-id", cfg.ClientID), + stomp.ConnOpt.ReadChannelCapacity(maxExpectedQPS*maxExpectedDowntime), + stomp.ConnOpt.HeartBeat(7*time.Minute, 1*time.Minute), + stomp.ConnOpt.HeartBeatGracePeriodMultiplier(3), + ) +} + +func Subscribe(c *stomp.Conn, subName string) (*stomp.Subscription, error) { + return c.Subscribe( + "/topic/darwin.pushport-v16", stomp.AckClientIndividual, + stomp.SubscribeOpt.Header("durable-subscription-name", subName), + ) +} diff --git a/go/trains/darwin/darwingest/darwingeststomp/default.nix b/go/trains/darwin/darwingest/darwingeststomp/default.nix new file mode 100644 index 0000000000..afdbd0b5b4 --- /dev/null +++ b/go/trains/darwin/darwingest/darwingeststomp/default.nix @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2020 Luke Granger-Brown +# +# SPDX-License-Identifier: Apache-2.0 + +{ depot, ... }@args: +depot.third_party.buildGo.package { + name = "darwingeststomp"; + srcs = [ + ./darwingeststomp.go + ]; + path = "hg.lukegb.com/lukegb/depot/go/trains/darwin/darwingest/darwingeststomp"; + deps = with depot.third_party; [ + gopkgs."github.com".go-stomp.stomp.v3 + + depot.go.trains.darwin + ]; +} diff --git a/go/trains/darwin/darwingest/default.nix b/go/trains/darwin/darwingest/default.nix new file mode 100644 index 0000000000..53258525b3 --- /dev/null +++ b/go/trains/darwin/darwingest/default.nix @@ -0,0 +1,27 @@ +# SPDX-FileCopyrightText: 2020 Luke Granger-Brown +# +# SPDX-License-Identifier: Apache-2.0 + +{ depot, ... }@args: +depot.third_party.buildGo.package { + name = "darwingest"; + srcs = [ + ./darwingest.go + ]; + path = "hg.lukegb.com/lukegb/depot/go/trains/darwin/darwingest"; + deps = with depot.third_party; [ + gopkgs."github.com".cenkalti.backoff.v4 + gopkgs."github.com".go-stomp.stomp.v3 + gopkgs."github.com".jlaffaye.ftp + gopkgs."gocloud.dev".blob + + depot.go.trains.darwin + depot.go.trains.darwin.darwingest.darwingestftp + depot.go.trains.darwin.darwingest.darwingests3 + depot.go.trains.darwin.darwingest.darwingeststomp + ]; +} // { + darwingestftp = import ./darwingestftp args; + darwingests3 = import ./darwingests3 args; + darwingeststomp = import ./darwingeststomp args; +} diff --git a/go/trains/darwin/default.nix b/go/trains/darwin/default.nix new file mode 100644 index 0000000000..7e7dc7013f --- /dev/null +++ b/go/trains/darwin/default.nix @@ -0,0 +1,18 @@ +# SPDX-FileCopyrightText: 2020 Luke Granger-Brown +# +# SPDX-License-Identifier: Apache-2.0 + +{ depot, ... }@args: +depot.third_party.buildGo.package { + name = "darwin"; + srcs = [ + ./msg.go + ./refdata.go + ./timezone.go + ./util.go + ]; + path = "hg.lukegb.com/lukegb/depot/go/trains/darwin"; +} // { + darwindb = import ./darwindb args; + darwingest = import ./darwingest args; +} diff --git a/go/trains/darwin/msg.go b/go/trains/darwin/msg.go new file mode 100644 index 0000000000..ddf1d5f22d --- /dev/null +++ b/go/trains/darwin/msg.go @@ -0,0 +1,436 @@ +package darwin + +import ( + "encoding/xml" + "time" +) + +type QueryTimetable struct { + //XMLName xml.Name `xml:"QueryTimetable"` +} + +type TimetableID struct { + //XMLName xml.Name `xml:"TimeTableId"` + ID string `xml:",chardata"` + Filename string `xml:"ttfile,attr"` + ReferenceFilename string `xml:"ttreffile,attr"` +} + +type GetSnapshotReq struct { + //XMLName xml.Name `xml:"GetSnapshotReq"` + ViaFTP bool `xml:"viaftp,attr"` +} + +type GetFullSnapshotReq struct { + //XMLName xml.Name `xml:"GetFullSnapshotReq"` + ViaFTP bool `xml:"viaftp,attr"` +} + +type SnapshotID struct { + //XMLName xml.Name `xml:"SnapshotId"` + ID string `xml:",chardata"` +} + +type StartUpdateReq struct { + //XMLName xml.Name `xml:"StartUpdateReq"` +} + +type StopUpdateReq struct { + //XMLName xml.Name `xml:"StopUpdateReq"` +} + +type FailureResp struct { + //XMLName xml.Name `xml:"FailureResp"` + ErrorMessage string `xml:",chardata"` + Code string `xml:"code,attr"` + RequestSource string `xml:"requestSource,attr"` + RequestID string `xml:"requestID,attr"` +} + +type ScheduleCallingPoint struct { + XMLName xml.Name + + TIPLOC string `xml:"tpl,attr"` + Act string `xml:"act,attr"` + PlanAct string `xml:"planAct,attr"` + Platform string `xml:"plat,attr"` + Cancelled bool `xml:"can,attr"` + FID string `xml:"fid,attr"` + + PTA string `xml:"pta,attr"` + PTD string `xml:"ptd,attr"` + AvgLoading int `xml:"avgLoading,attr"` + + WTA string `xml:"wta,attr"` + WTD string `xml:"wtd,attr"` + WTP string `xml:"wtp,attr"` + FD string `xml:"fd,attr"` + + RDelay int `xml:"rdelay,attr"` +} + +type Schedule struct { + //XMLName xml.Name `xml:"schedule"` + + CallingPoints []ScheduleCallingPoint `xml:",any"` + CancellationReason *DisruptionReason `xml:"cancelReason" json:",omitempty"` + + RID string `xml:"rid,attr"` + UID string `xml:"uid,attr"` + TrainID string `xml:"trainId,attr"` + RSID string `xml:"rsid,attr"` + SSD string `xml:"ssd,attr"` // RTTIDateType + TOC string `xml:"toc,attr"` + Status *string `xml:"status,attr"` + TrainCat *string `xml:"trainCat,attr"` + IsPassengerSvc *bool `xml:"isPassengerSvc,attr"` + IsActive *bool `xml:"isActive,attr"` + Deleted bool `xml:"deleted,attr"` + IsCharter *bool `xml:"isCharter,attr"` + IsQTrain *bool `xml:"qtrain,attr"` + Cancelled *bool `xml:"can,attr"` +} + +func (s Schedule) AffectedRIDs() []string { + return []string{s.RID} +} + +type DeactivatedSchedule struct { + //XMLName xml.Name `xml:"deactivated"` + + RID string `xml:"rid,attr"` +} + +func (ds DeactivatedSchedule) AffectedRIDs() []string { + return []string{ds.RID} +} + +type CircularTimes struct { + WTA string `xml:"wta,attr" json:",omitempty"` + WTD string `xml:"wtd,attr" json:",omitempty"` + WTP string `xml:"wtp,attr" json:",omitempty"` + PTA string `xml:"pta,attr" json:",omitempty"` + PTD string `xml:"ptd,attr" json:",omitempty"` +} + +type AssocService struct { + RID string `xml:"rid,attr"` + CircularTimes +} + +type Association struct { + //XMLName xml.Name `xml:"association"` + + Main AssocService `xml:"main"` + Assoc AssocService `xml:"assoc"` + + TIPLOC string `xml:"tiploc,attr"` + Category string `xml:"category,attr"` + IsCancelled bool `xml:"isCancelled,attr"` + IsDeleted bool `xml:"isDeleted,attr"` +} + +func (a Association) AffectedRIDs() []string { + return []string{a.Main.RID, a.Assoc.RID} +} + +type ToiletAvailability struct { + Type string `xml:",chardata"` + Status string `xml:"status,attr"` +} + +type Coach struct { + Toilet []ToiletAvailability `xml:"toilet"` + + CoachNumber string `xml:"coachNumber,attr"` + CoachClass string `xml:"coachClass,attr"` +} + +type Formation struct { + //XMLName xml.Name `xml:"formation"` + + Coaches []Coach `xml:"coaches>coach"` + + FID string `xml:"fid,attr"` + Src string `xml:"src,attr"` + SrcInst string `xml:"srcInst,attr"` +} + +type ScheduleFormation struct { + //XMLName xml.Name `xml:"scheduleFormations"` + + Formation []Formation `xml:"formation"` + + RID string `xml:"rid,attr"` +} + +func (sf ScheduleFormation) AffectedRIDs() []string { + return []string{sf.RID} +} + +type DisruptionReason struct { + Code string `xml:",chardata"` + TIPLOC string `xml:"tiploc,attr"` + Near bool `xml:"near,attr"` +} + +type TSTimeData struct { + ET string `xml:"et,attr" json:",omitempty"` + WET string `xml:"wet,attr" json:",omitempty"` + AT string `xml:"at,attr" json:",omitempty"` + ATRemoved bool `xml:"atRemoved,attr" json:",omitempty"` + ETMin string `xml:"etmin,attr" json:",omitempty"` + ETUnknown bool `xml:"etUnknown,attr" json:",omitempty"` + Delayed bool `xml:"delayed,attr" json:",omitempty"` + Src string `xml:"src,attr" json:",omitempty"` + SrcInst string `xml:"srcInst,attr" json:",omitempty"` +} + +type PlatformData struct { + Platform string `xml:",chardata"` + PlatSup bool `xml:"platsup,attr"` + CisPlatSup bool `xml:"cisPlatsup,attr"` + PlatSrc string `xml:"platsrc,attr"` + Conf bool `xml:"conf,attr"` +} + +type TSLocation struct { + TIPLOC string `xml:"tpl,attr"` + CircularTimes + + Arr *TSTimeData `xml:"arr" json:",omitempty"` + Dep *TSTimeData `xml:"dep" json:",omitempty"` + Pass *TSTimeData `xml:"pass" json:",omitempty"` + Plat *PlatformData `xml:"plat" json:",omitempty"` + Suppr *bool `xml:"suppr" json:",omitempty"` + Length *int `xml:"length" json:",omitempty"` + DetachFront *bool `xml:"detachFront" json:",omitempty"` +} + +type TrainStatus struct { + //XMLName xml.Name `xml:"TS"` + + LateReason *DisruptionReason `xml:"LateReason" json:",omitempty"` + Location []TSLocation `xml:"Location"` + + RID string `xml:"rid,attr"` + UID string `xml:"uid,attr"` + SSD string `xml:"ssd,attr"` + IsReverseFormation bool `xml:"isReverseFormation,attr"` +} + +func (ts TrainStatus) AffectedRIDs() []string { + return []string{ts.RID} +} + +type CoachLoadingData struct { + Value string `xml:",chardata"` + + CoachNumber string `xml:"coachNumber,attr"` + Src string `xml:"src,attr"` + SrcInst string `xml:"srcInst,attr"` +} + +type FormationLoading struct { + //XMLName xml.Name `xml:"formationLoading"` + + Loading []CoachLoadingData `xml:"loading"` + + FID string `xml:"fid,attr"` + RID string `xml:"rid,attr"` + TIPLOC string `xml:"tpl,attr"` + CircularTimes +} + +func (fl FormationLoading) AffectedRIDs() []string { + return []string{fl.RID} +} + +type StationMessageStation struct { + CRS string `xml:"crs,attr"` +} + +type StationMessageMessage struct { + InnerXML string `xml:",innerxml"` +} + +type StationMessage struct { + Station []StationMessageStation `xml:"Station"` + Msg StationMessageMessage `xml:"Msg"` + + ID int `xml:"id,attr"` + Cat string `xml:"cat,attr"` + Sev string `xml:"sev,attr"` + Suppress bool `xml:"bool,attr"` +} + +type AlertService struct { + RID string `xml:"RID,attr"` + UID string `xml:"UID,attr"` + SSD string `xml:"SSD,attr"` + + Location []string `xml:"Location"` +} + +type TrainAlert struct { + AlertID int `xml:"AlertID"` + AlertServices []AlertService `xml:"AlertServices"` + SendAlertBySMS bool `xml:"SendAlertBySMS"` + SendAlertByEmail bool `xml:"SendAlertByEmail"` + SendAlertByTwitter bool `xml:"SendAlertByTwitter"` + Source string `xml:"Source"` + AlertText string `xml:"AlertText"` + Audience string `xml:"Audience"` + AlertType string `xml:"AlertType"` +} + +func (ta TrainAlert) AffectedRIDs() []string { + rids := make([]string, len(ta.AlertServices)) + for n, as := range ta.AlertServices { + rids[n] = as.RID + } + return rids +} + +type TrainOrderRID struct { + RID string `xml:",chardata"` + CircularTimes +} + +type TrainOrderItem struct { + RID *TrainOrderRID `xml:"rid"` + TrainID *string `xml:"trainID"` +} + +type TrainOrderData struct { + First TrainOrderItem `xml:"first"` + Second *TrainOrderItem `xml:"second"` + Third *TrainOrderItem `xml:"third"` +} + +type TrainOrder struct { + Set *TrainOrderData `xml:"set"` + Clear struct{} `xml:"clear"` + + TIPLOC string `xml:"tiploc,attr"` + CRS string `xml:"crs,attr"` + Platform string `xml:"platform,attr"` +} + +type FullTDBerthID struct { + Berth string `xml:",chardata"` + + Area string `xml:"area,attr"` +} + +type TrackingID struct { + Berth FullTDBerthID `xml:"berth"` + IncorrectTrainID string `xml:"incorrectTrainID"` + CorrectTrainID string `xml:"correctTrainID"` +} + +type RTTIAlarmData struct { + TDAreaFail *string `xml:"tdAreaFail"` + TDFeedFail *string `xml:"tdFeedFail"` + TyrellFeedFail *string `xml:"tyrellFeedFail"` + + ID string `xml:"id,attr"` +} + +type RTTIAlarm struct { + Set *RTTIAlarmData `xml:"set"` + Clear *string `xml:"clear"` +} + +type DataResponse struct { + Schedule []Schedule `xml:"schedule" json:",omitempty"` + Deactivated []DeactivatedSchedule `xml:"deactivated" json:",omitempty"` + Association []Association `xml:"association" json:",omitempty"` + ScheduleFormations []ScheduleFormation `xml:"scheduleFormations" json:",omitempty"` + TrainStatus []TrainStatus `xml:"TS" json:",omitempty"` + FormationLoading []FormationLoading `xml:"formationLoading" json:",omitempty"` + OW []StationMessage `xml:"OW" json:",omitempty"` + TrainAlert []TrainAlert `xml:"trainAlert" json:",omitempty"` + TrainOrder []TrainOrder `xml:"trainOrder" json:",omitempty"` + TrackingID []TrackingID `xml:"trackingID" json:",omitempty"` + Alarm []RTTIAlarm `xml:"alarm" json:",omitempty"` +} + +func (r DataResponse) AffectedRIDs() []string { + var rids []string + for _, s := range r.Schedule { + rids = append(rids, s.AffectedRIDs()...) + } + for _, ds := range r.Deactivated { + rids = append(rids, ds.AffectedRIDs()...) + } + for _, a := range r.Association { + rids = append(rids, a.AffectedRIDs()...) + } + for _, sf := range r.ScheduleFormations { + rids = append(rids, sf.AffectedRIDs()...) + } + for _, ts := range r.TrainStatus { + rids = append(rids, ts.AffectedRIDs()...) + } + for _, fl := range r.FormationLoading { + rids = append(rids, fl.AffectedRIDs()...) + } + // OW doesn't affect RIDs directly. + for _, ta := range r.TrainAlert { + rids = append(rids, ta.AffectedRIDs()...) + } + // TrainOrder needs extra processing (clear doesn't specify anything so we have to know ahead of time...) + // TrackingID doesn't contain RIDs. + // Alarm doesn't affect RIDs directly. + return rids +} + +type UpdateResp struct { + UpdateOrigin string `xml:"updateOrigin,attr"` + RequestSource string `xml:"requestSource,attr" json:",omitempty"` + RequestID string `xml:"requestID,attr" json:",omitempty"` + DataResponse +} + +type SnapshotResp struct { + DataResponse +} + +type PushPort struct { + //XMLName xml.Name `xml:"Pport"` + + // Subelements + QueryTimetable []QueryTimetable `xml:"QueryTimetable" json:",omitempty"` + TimeTableID []TimetableID `xml:"TimeTableId" json:",omitempty"` + GetSnapshotReq []GetSnapshotReq `xml:"GetSnapshotReq" json:",omitempty"` + GetFullSnapshotReq []GetFullSnapshotReq `xml:"GetFullSnapshotReq" json:",omitempty"` + SnapshotID []SnapshotID `xml:"SnapshotId" json:",omitempty"` + StartUpdateReq []StartUpdateReq `xml:"StartUpdateReq" json:",omitempty"` + StopUpdateReq []StopUpdateReq `xml:"StopUpdateReq" json:",omitempty"` + FailureResp []FailureResp `xml:"FailureResp" json:",omitempty"` + UpdateResp []UpdateResp `xml:"uR" json:",omitempty"` + SnapshotResp []SnapshotResp `xml:"sR" json:",omitempty"` + + // Attrs + Time time.Time `xml:"ts,attr"` +} + +func (pp PushPort) AffectedRIDs() []string { + var rids []string + for _, r := range pp.UpdateResp { + rids = append(rids, r.AffectedRIDs()...) + } + for _, r := range pp.SnapshotResp { + rids = append(rids, r.AffectedRIDs()...) + } + return rids +} + +type PushPortTimetable struct { + Journey []Schedule `xml:"Journey"` + Association []Association `xml:"Association"` + + // Attrs + TimetableID string `xml:"timetableID,attr"` +} diff --git a/go/trains/darwin/refdata.go b/go/trains/darwin/refdata.go new file mode 100644 index 0000000000..551700d9c8 --- /dev/null +++ b/go/trains/darwin/refdata.go @@ -0,0 +1,45 @@ +package darwin + +import "encoding/xml" + +type RefLocationRef struct { + TIPLOC string `xml:"tpl,attr"` + Name string `xml:"locname,attr"` + CRS *string `xml:"crs,attr"` + TOC *string `xml:"toc,attr"` +} + +type RefTOCRef struct { + TOC string `xml:"toc,attr"` + Name string `xml:"tocname,attr"` + URL *string `xml:"url,attr"` +} + +type RefReason struct { + Code string `xml:"code,attr"` + Text string `xml:"reasontext,attr"` +} + +type RefVia struct { + AtCRS string `xml:"at,attr"` + DestTIPLOC string `xml:"dest,attr"` + Loc1TIPLOC string `xml:"loc1,attr"` + Loc2TIPLOC string `xml:"loc2,attr"` + Text string `xml:"viatext,attr"` +} + +type RefCISSource struct { + CISCode string `xml:"code,attr"` + Name string `xml:"name,attr"` +} + +type PushPortReferenceData struct { + XMLName xml.Name `xml:"PportTimetableRef"` + + Locations []RefLocationRef `xml:"LocationRef"` + TOCs []RefTOCRef `xml:"TocRef"` + LateRunningReasons []RefReason `xml:"LateRunningReasons>Reason"` + CancellationReasons []RefReason `xml:"CancellationReasons>Reason"` + Via []RefVia `xml:"Via"` + CISSource []RefCISSource `xml:"CISSource"` +} diff --git a/go/trains/darwin/timezone.go b/go/trains/darwin/timezone.go new file mode 100644 index 0000000000..2ded0f3c37 --- /dev/null +++ b/go/trains/darwin/timezone.go @@ -0,0 +1,15 @@ +package darwin + +import ( + "time" +) + +var ( + London = func() *time.Location { + tz, err := time.LoadLocation("Europe/London") + if err != nil { + panic(err) + } + return tz + }() +) diff --git a/go/trains/darwin/util.go b/go/trains/darwin/util.go new file mode 100644 index 0000000000..5f60e36378 --- /dev/null +++ b/go/trains/darwin/util.go @@ -0,0 +1,18 @@ +package darwin + +import ( + "context" + "time" +) + +func Sleep(ctx context.Context, d time.Duration) error { + t := time.NewTicker(d) + defer t.Stop() + + select { + case <-t.C: + return nil + case <-ctx.Done(): + return ctx.Err() + } +} diff --git a/go/trains/default.nix b/go/trains/default.nix new file mode 100644 index 0000000000..82bdd24dad --- /dev/null +++ b/go/trains/default.nix @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: 2020 Luke Granger-Brown +# +# SPDX-License-Identifier: Apache-2.0 + +{ depot, ... }@args: + +{ + cmd = import ./cmd args; + webapi = import ./webapi args; + darwin = import ./darwin args; +} diff --git a/go/trains/go.mod b/go/trains/go.mod new file mode 100644 index 0000000000..701de573a8 --- /dev/null +++ b/go/trains/go.mod @@ -0,0 +1,38 @@ +module hg.lukegb.com/lukegb/depot/go/trains + +go 1.16 + +require ( + git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999 // indirect + github.com/cenkalti/backoff/v4 v4.1.1 + github.com/coreos/bbolt v1.3.1-coreos.6 // indirect + github.com/coreos/etcd v3.3.10+incompatible // indirect + github.com/coreos/go-semver v0.2.0 // indirect + github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f // indirect + github.com/dnaeon/go-vcr v1.0.1 // indirect + github.com/go-ini/ini v1.39.0 // indirect + github.com/go-stomp/stomp/v3 v3.0.3 + github.com/golang/lint v0.0.0-20180702182130-06c8688daad7 // indirect + github.com/googleapis/gax-go v2.0.0+incompatible // indirect + github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect + github.com/gorilla/context v1.1.1 // indirect + github.com/gorilla/mux v1.6.2 // indirect + github.com/grpc-ecosystem/go-grpc-middleware v1.0.0 // indirect + github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect + github.com/hooklift/gowsdl v0.5.0 // indirect + github.com/jackc/pgx/v4 v4.13.0 + github.com/jlaffaye/ftp v0.0.0-20211029032751-b1140299f4df + github.com/jonboulle/clockwork v0.1.0 // indirect + github.com/jtolds/gls v4.2.1+incompatible // indirect + github.com/openzipkin/zipkin-go v0.1.1 // indirect + github.com/prometheus/client_golang v1.11.0 + github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect + github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a // indirect + github.com/soheilhy/cmux v0.1.4 // indirect + github.com/streadway/amqp v0.0.0-20181107104731-27835f1a64e9 // indirect + github.com/tmc/grpc-websocket-proxy v0.0.0-20171017195756-830351dc03c6 // indirect + github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18 // indirect + gocloud.dev v0.24.0 // indirect + gopkg.in/ini.v1 v1.39.0 // indirect + gopkg.in/pipe.v2 v2.0.0-20140414041502-3c2ca4d52544 // indirect +) diff --git a/go/trains/go.sum b/go/trains/go.sum new file mode 100644 index 0000000000..3918a3b10d --- /dev/null +++ b/go/trains/go.sum @@ -0,0 +1,959 @@ +bazil.org/fuse v0.0.0-20180421153158-65cc252bf669/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.82.0/go.mod h1:vlKccHJGuFBFufnAnuB08dfEH9Y3H7dzDzRECFdC2TA= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.88.0/go.mod h1:dnKwfYbP9hQhefiUvpbcAyoGSHUrOxR20JVElLiUvEY= +cloud.google.com/go v0.89.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.92.2/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.92.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.0/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.5.0/go.mod h1:c4nNYR1qdq7eaZ+jSc5fonrQN2k3M7sWATcYTiakjEo= +cloud.google.com/go/kms v0.1.0/go.mod h1:8Qp8PCAypHg4FdmlyW1QRAv09BGQ9Uzh7JnmIZxPk+c= +cloud.google.com/go/monitoring v0.1.0/go.mod h1:Hpm3XfzJv+UTiXzCG5Ffp0wijzHTC7Cv4eR7o3x/fEE= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/pubsub v1.16.0/go.mod h1:6A8EfoWZ/lUvCWStKGwAWauJZSiuV0Mkmu6WilK/TxQ= +cloud.google.com/go/secretmanager v0.1.0/go.mod h1:3nGKHvnzDUVit7U0S9KAKJ4aOsO1xtwRG+7ey5LK1bM= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.16.1/go.mod h1:LaNorbty3ehnU3rEjXSNV/NRgQA0O8Y+uh6bPe5UOk4= +cloud.google.com/go/trace v0.1.0/go.mod h1:wxEwsoeRVPbeSkt7ZC9nWCgmoKQRAoySN7XHW2AmI7g= +contrib.go.opencensus.io/exporter/aws v0.0.0-20200617204711-c478e41e60e9/go.mod h1:uu1P0UCM/6RbsMrgPa98ll8ZcHM858i/AD06a9aLRCA= +contrib.go.opencensus.io/exporter/stackdriver v0.13.8/go.mod h1:huNtlWx75MwO7qMs0KrMxPZXzNNWebav1Sq/pm02JdQ= +contrib.go.opencensus.io/integrations/ocsql v0.1.7/go.mod h1:8DsSdjz3F+APR+0z0WkU1aRorQCFfRxvqjUUPMbF3fE= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= +github.com/Azure/azure-amqp-common-go/v3 v3.1.0/go.mod h1:PBIGdzcO1teYoufTKMcGibdKaYZv4avS+O6LNIp8bq0= +github.com/Azure/azure-amqp-common-go/v3 v3.1.1/go.mod h1:YsDaPfaO9Ub2XeSKdIy2DfwuiQlHQCauHJwSqtrkECI= +github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k= +github.com/Azure/azure-sdk-for-go v51.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go v57.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-service-bus-go v0.10.16/go.mod h1:MlkLwGGf1ewcx5jZadn0gUEty+tTg0RaElr6bPf+QhI= +github.com/Azure/azure-storage-blob-go v0.14.0/go.mod h1:SMqIBi+SuiQH32bvyjngEewEeXoPfKMgWlBDaYf6fck= +github.com/Azure/go-amqp v0.13.0/go.mod h1:qj+o8xPCz9tMSbQ83Vp8boHahuRDl5mkNHyt1xlxUTs= +github.com/Azure/go-amqp v0.13.11/go.mod h1:D5ZrjQqB1dyp1A+G73xeL/kNn7D5qHJIIsNNps7YNmk= +github.com/Azure/go-amqp v0.13.12/go.mod h1:D5ZrjQqB1dyp1A+G73xeL/kNn7D5qHJIIsNNps7YNmk= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest v0.11.3/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= +github.com/Azure/go-autorest/autorest v0.11.17/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw= +github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= +github.com/Azure/go-autorest/autorest v0.11.20/go.mod h1:o3tqFY+QR40VOlk+pV4d77mORO64jOXSgEnPQgLK6JY= +github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= +github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= +github.com/Azure/go-autorest/autorest/adal v0.9.11/go.mod h1:nBKAnTomx8gDtl+3ZCJv2v0KACFHWTB2drffI1B68Pk= +github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= +github.com/Azure/go-autorest/autorest/adal v0.9.14/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= +github.com/Azure/go-autorest/autorest/adal v0.9.15/go.mod h1:tGMin8I49Yij6AQ+rvV+Xa/zwxYQB5hmsd6DkfAx2+A= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.8/go.mod h1:kxyKZTSfKh8OVFWPAgOgQ/frrJgeYQJPyR5fLFmXko4= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.2/go.mod h1:7qkJkT+j6b+hIpzMOwPChJhTqS8VbsqqgULzMNRugoM= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.3/go.mod h1:yAQ2b6eP/CmLPnmLvxtT1ALIY3OR1oFcCqVBi8vHiTc= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE= +github.com/Azure/go-autorest/autorest/validation v0.3.1/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E= +github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/GoogleCloudPlatform/cloudsql-proxy v1.24.0/go.mod h1:3tx938GhY4FC+E1KT/jNjDw7Z5qxAEtIiERJ2sXjnII= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= +github.com/aws/aws-sdk-go v1.37.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= +github.com/aws/aws-sdk-go v1.40.34 h1:SBYmodndE2d4AYucuuJnOXk4MD1SFbucoIdpwKVKeSA= +github.com/aws/aws-sdk-go v1.40.34/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= +github.com/aws/aws-sdk-go-v2 v1.9.0 h1:+S+dSqQCN3MSU5vJRu1HqHrq00cJn6heIMU7X9hcsoo= +github.com/aws/aws-sdk-go-v2 v1.9.0/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= +github.com/aws/aws-sdk-go-v2/config v1.7.0 h1:J2cZ7qe+3IpqBEXnHUrFrOjoB9BlsXg7j53vxcl5IVg= +github.com/aws/aws-sdk-go-v2/config v1.7.0/go.mod h1:w9+nMZ7soXCe5nT46Ri354SNhXDQ6v+V5wqDjnZE+GY= +github.com/aws/aws-sdk-go-v2/credentials v1.4.0 h1:kmvesfjY861FzlCU9mvAfe01D9aeXcG2ZuC+k9F2YLM= +github.com/aws/aws-sdk-go-v2/credentials v1.4.0/go.mod h1:dgGR+Qq7Wjcd4AOAW5Rf5Tnv3+x7ed6kETXyS9WCuAY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.5.0 h1:OxTAgH8Y4BXHD6PGCJ8DHx2kaZPCQfSTqmDsdRZFezE= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.5.0/go.mod h1:CpNzHK9VEFUCknu50kkB8z58AH2B5DvPP7ea1LHve/Y= +github.com/aws/aws-sdk-go-v2/internal/ini v1.2.2 h1:d95cddM3yTm4qffj3P6EnP+TzX1SSkWaQypXSgT/hpA= +github.com/aws/aws-sdk-go-v2/internal/ini v1.2.2/go.mod h1:BQV0agm+JEhqR+2RT5e1XTFIDcAAV0eW6z2trp+iduw= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.0 h1:VNJ5NLBteVXEwE2F1zEXVmyIH58mZ6kIQGJoC7C+vkg= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.0/go.mod h1:R1KK+vY8AfalhG1AOu5e35pOD2SdoPKQCFLTvnxiohk= +github.com/aws/aws-sdk-go-v2/service/kms v1.5.0/go.mod h1:w7JuP9Oq1IKMFQPkNe3V6s9rOssXzOVEMNEqK1L1bao= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.6.0/go.mod h1:B+7C5UKdVq1ylkI/A6O8wcurFtaux0R1njePNPtKwoA= +github.com/aws/aws-sdk-go-v2/service/ssm v1.10.0/go.mod h1:4dXS5YNqI3SNbetQ7X7vfsMlX6ZnboJA2dulBwJx7+g= +github.com/aws/aws-sdk-go-v2/service/sso v1.4.0 h1:sHXMIKYS6YiLPzmKSvDpPmOpJDHxmAUgbiF49YNVztg= +github.com/aws/aws-sdk-go-v2/service/sso v1.4.0/go.mod h1:+1fpWnL96DL23aXPpMGbsmKe8jLTEfbjuQoA4WS1VaA= +github.com/aws/aws-sdk-go-v2/service/sts v1.7.0 h1:1at4e5P+lvHNl2nUktdM2/v+rpICg/QSEr9TO/uW9vU= +github.com/aws/aws-sdk-go-v2/service/sts v1.7.0/go.mod h1:0qcSMCyASQPN2sk/1KQLQ2Fh6yq8wm0HSDAimPhzCoM= +github.com/aws/smithy-go v1.8.0 h1:AEwwwXQZtUwP5Mz506FeXXrKBe0jA8gVM+1gEcSRooc= +github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cenkalti/backoff/v4 v4.1.1 h1:G2HAfAmvm/GcKan2oOQpBXOd2tT2G57ZnZGWa1PxPBQ= +github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/coreos/bbolt v1.3.1-coreos.6/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/devigned/tab v0.1.1/go.mod h1:XG9mPq0dFghrYvoBF3xdRrJzSTX1b7IQrvaL9mzjeJY= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= +github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= +github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-ini/ini v1.39.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-stomp/stomp/v3 v3.0.3 h1:7YQGJCDMkbA05Rw8dS00LxwU1mhzEHS69gMlPjMZGDk= +github.com/go-stomp/stomp/v3 v3.0.3/go.mod h1:jTrybHBK20jPdM9iyh65m6GusX6aMf7atfEFZ1nIcgc= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= +github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-replayers/grpcreplay v1.1.0/go.mod h1:qzAvJ8/wi57zq7gWqaE6AwLM6miiXUQwP1S+I9icmhk= +github.com/google/go-replayers/httpreplay v1.0.0/go.mod h1:LJhKoTwS5Wy5Ld/peq8dFFG5OfJyHEz7ft+DsTUv25M= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian v2.1.1-0.20190517191504-25dcb96d9e51+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210506205249-923b5ab0fc1a/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210715191844-86eeefc3e471/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/wire v0.5.0 h1:I7ELFeVBr3yfPIcc8+MWvrjk+3VjbcSzoXm3JVa+jD8= +github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= +github.com/googleapis/gax-go v2.0.0+incompatible h1:j0GKcs05QVmm7yesiZq2+9cxHkNK9YM6zKx4D2qucQU= +github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.1.0 h1:6DWmvNpomjL1+3liNSZbVns3zsYzzCjm6pRBO1tLeso= +github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hooklift/gowsdl v0.5.0 h1:DE8RevqhGPLchumV/V7OwbCzfJ8lcozFg1uWC/ESCBQ= +github.com/hooklift/gowsdl v0.5.0/go.mod h1:9kRc402w9Ci/Mek5a1DNgTmU14yPY8fMumxNVvxhis4= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= +github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= +github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= +github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= +github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= +github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= +github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= +github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= +github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= +github.com/jackc/pgconn v1.10.0 h1:4EYhlDVEMsJ30nNj0mmgwIUXoq7e9sMJrVC2ED6QlCU= +github.com/jackc/pgconn v1.10.0/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= +github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= +github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= +github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= +github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= +github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.1.1 h1:7PQ/4gLoqnl87ZxL7xjO0DR5gYuviDCZxQJsUlFW1eI= +github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= +github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= +github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= +github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= +github.com/jackc/pgtype v1.8.1 h1:9k0IXtdJXHJbyAWQgbWr1lU+MEhPXZz6RIXxfR5oxXs= +github.com/jackc/pgtype v1.8.1/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= +github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= +github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= +github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= +github.com/jackc/pgx/v4 v4.13.0 h1:JCjhT5vmhMAf/YwBHLvrBn4OGdIQBiFG6ym8Zmdx570= +github.com/jackc/pgx/v4 v4.13.0/go.mod h1:9P4X524sErlaxj0XSGZk7s+LD0eOyu1ZDUrrpznYDF0= +github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.3 h1:JnPg/5Q9xVJGfjsO5CPUOjnJps1JaRUm8I9FXVCFK94= +github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jlaffaye/ftp v0.0.0-20211029032751-b1140299f4df h1:nsRFf9ZkcalB12ZJZYAD35TE2r+g/i088w9ytnaUvUo= +github.com/jlaffaye/ftp v0.0.0-20211029032751-b1140299f4df/go.mod h1:2lmrmq866uF2tnje75wQHzmPXhmSWUt7Gyx2vgK1RCU= +github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= +github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/streadway/amqp v0.0.0-20181107104731-27835f1a64e9/go.mod h1:1WNBiOZtZQLpVAyu0iTduoJL9hEsMloAK5XWrtW0xdY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tmc/grpc-websocket-proxy v0.0.0-20171017195756-830351dc03c6/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +go.opencensus.io v0.15.0/go.mod h1:UffZAU+4sDEINUGP/B7UfBBkq4fqLu9zXAX7ke6CHW0= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.22.6/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +gocloud.dev v0.24.0 h1:cNtHD07zQQiv02OiwwDyVMuHmR7iQt2RLkzoAgz7wBs= +gocloud.dev v0.24.0/go.mod h1:uA+als++iBX5ShuG4upQo/3Zoz49iIPlYUWHV5mM8w8= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191112182307-2180aed22343/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210825183410-e898025ed96a h1:bRuuGXV8wwSdGTB+CtJf+FjgO1APK1CoO39T4BN/XBw= +golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210126194326-f9ce19ea3013/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210427180440-81ed05c6b58c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191112214154-59a1497f0cea/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200828194041-157a740278f4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210223095934-7937bea0104d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210503080704-8803ae5d1324/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e h1:XMgFehsDnnLGtjvjOfqWSUzt0alpTR1RSEuznObga2c= +golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.37.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.46.0/go.mod h1:ceL4oozhkAiTID8XMmJBsIxID/9wMXJVVFXPg4ylg3I= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.52.0/go.mod h1:Him/adpjt0sxtkWViy0b6xyKW/SD71CwdJ7HqJo7SrU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.56.0 h1:08F9XVYTLOGeSQb3xI9C0gXMuQanhdGed0cWFhDozbI= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210126160654-44e461bb6506/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210429181445-86c259c2b4ab/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210517163617-5e0236093d7a/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210721163202-f1cecdd8b78a/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210722135532-667f2b7c528f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210825212027-de86158e7fda/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2 h1:NHN4wOCScVzKhPenJ2dt+BTs3X/XkBVI/Rh4iDt55T8= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0 h1:AGJ0Ih4mHjSeibYkFGh1dD9KJ/eOtZ93I6hoHhukQ5Q= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1 h1:7QnIQpGRHE5RnLKnESfDoxm2dTapTZua5a0kS0A+VXQ= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= +gopkg.in/ini.v1 v1.39.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/pipe.v2 v2.0.0-20140414041502-3c2ca4d52544/go.mod h1:UhTeH/yXCK/KY7TX24mqPkaQ7gZeqmWd/8SSS8B3aHw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= +nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/go/trains/webapi/default.nix b/go/trains/webapi/default.nix new file mode 100644 index 0000000000..856d3a2dbf --- /dev/null +++ b/go/trains/webapi/default.nix @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: 2020 Luke Granger-Brown +# +# SPDX-License-Identifier: Apache-2.0 + +{ depot, ... }: +depot.third_party.buildGo.package { + name = "webapi"; + srcs = [ + ./structs.go + ]; + path = "hg.lukegb.com/lukegb/depot/go/trains/webapi"; +} diff --git a/go/trains/webapi/structs.go b/go/trains/webapi/structs.go new file mode 100644 index 0000000000..d9182a1baa --- /dev/null +++ b/go/trains/webapi/structs.go @@ -0,0 +1,197 @@ +package webapi + +import ( + "time" +) + +type TrainOperator struct { + Code string `json:"code"` + + Name *string `json:"name"` + URL *string `json:"url"` +} + +func (toc *TrainOperator) IsEmpty() bool { + return toc == nil || toc.Code == "" +} + +func (toc *TrainOperator) Canonicalize() { + if toc == nil { + return + } + if *toc.Name == "" { + toc.Name = nil + } + if *toc.URL == "" { + toc.URL = nil + } +} + +type Location struct { + TIPLOC string `json:"tiploc"` + Name string `json:"name"` + + CRS *string `json:"crs"` + TOC *TrainOperator `json:"operator"` +} + +func (loc *Location) IsEmpty() bool { + return loc == nil || loc.TIPLOC == "" +} + +func (loc *Location) Canonicalize() { + if loc == nil { + return + } + if loc.CRS == nil || *loc.CRS == "" { + loc.CRS = nil + } + if loc.TOC.IsEmpty() { + loc.TOC = nil + } + loc.TOC.Canonicalize() +} + +type DisruptionReason struct { + Code int `json:"code"` + Text string `json:"text"` + + Location *Location `json:"location"` + NearLocation bool `json:"near_location"` +} + +func (dr *DisruptionReason) IsEmpty() bool { + return dr == nil || dr.Code == 0 +} + +func (dr *DisruptionReason) Canonicalize() { + if dr == nil { + return + } + if dr.Location.IsEmpty() { + dr.Location = nil + } + dr.Location.Canonicalize() +} + +type ServiceData struct { + ID int `json:"id"` + RID string `json:"rid"` + UID string `json:"uid"` + RSID string `json:"rsid"` + Headcode string `json:"headcode"` + StartDate string `json:"scheduled_start_date"` + + TrainOperator TrainOperator `json:"operator"` + + DelayReason *DisruptionReason `json:"delay_reason"` + CancelReason *DisruptionReason `json:"cancel_reason"` + + Active bool `json:"is_active"` + Deleted bool `json:"is_deleted"` + Cancelled bool `json:"is_cancelled"` + + Locations []ServiceLocation `json:"locations"` +} + +func (sd *ServiceData) Canonicalize() { + if sd.DelayReason.IsEmpty() { + sd.DelayReason = nil + } + sd.DelayReason.Canonicalize() + if sd.CancelReason.IsEmpty() { + sd.CancelReason = nil + } + sd.CancelReason.Canonicalize() +} + +type TimingData struct { + PublicScheduled *time.Time `json:"public_scheduled"` + WorkingScheduled *time.Time `json:"working_scheduled"` + + PublicEstimated *time.Time `json:"public_estimated"` + WorkingEstimated *time.Time `json:"working_estimated"` + + Actual *time.Time `json:"actual"` +} + +func (td *TimingData) IsEmpty() bool { + if td == nil { + return true + } + return td.PublicScheduled == nil && td.WorkingScheduled == nil && td.PublicEstimated == nil && td.WorkingEstimated == nil && td.Actual == nil +} + +func (td *TimingData) Canonicalize() { + if td == nil { + return + } + ts := []**time.Time{&td.PublicScheduled, &td.WorkingScheduled, &td.PublicEstimated, &td.WorkingEstimated, &td.Actual} + for _, t := range ts { + if *t == nil || (*t).IsZero() { + *t = nil + } + } +} + +type PlatformData struct { + Scheduled string `json:"scheduled"` + + Live string `json:"live"` + Confirmed bool `json:"confirmed"` + + Suppressed bool `json:"platform_suppressed"` +} + +func (pd *PlatformData) IsEmpty() bool { + return pd.Scheduled == "" && pd.Live == "" +} + +func (pd *PlatformData) Canonicalize() {} + +type ServiceLocation struct { + ID int `json:"id"` + Location *Location `json:"location"` + CallingPointType string `json:"calling_point_type"` + + Length *int `json:"train_length"` + Suppressed bool `json:"service_suppressed"` + Platform *PlatformData `json:"platform"` + + Cancelled bool `json:"cancelled"` + FalseDestination *Location `json:"false_destination"` + + ArrivalTiming *TimingData `json:"arrival_timing"` + DepartureTiming *TimingData `json:"departure_timing"` + PassTiming *TimingData `json:"pass_timing"` +} + +func (sl *ServiceLocation) Canonicalize() { + if sl == nil { + return + } + sl.Location.Canonicalize() + if sl.Length == nil || *sl.Length == 0 { + sl.Length = nil + } + if sl.Platform.IsEmpty() { + sl.Platform = nil + } + sl.Platform.Canonicalize() + if sl.FalseDestination.IsEmpty() { + sl.FalseDestination = nil + } + sl.FalseDestination.Canonicalize() + if sl.ArrivalTiming.IsEmpty() { + sl.ArrivalTiming = nil + } + sl.ArrivalTiming.Canonicalize() + if sl.DepartureTiming.IsEmpty() { + sl.DepartureTiming = nil + } + sl.DepartureTiming.Canonicalize() + if sl.PassTiming.IsEmpty() { + sl.PassTiming = nil + } + sl.PassTiming.Canonicalize() +} diff --git a/third_party/gopkgs/github.com/cenkalti/backoff/v4/default.nix b/third_party/gopkgs/github.com/cenkalti/backoff/v4/default.nix new file mode 100644 index 0000000000..781d87d8cf --- /dev/null +++ b/third_party/gopkgs/github.com/cenkalti/backoff/v4/default.nix @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: 2020 Luke Granger-Brown +# +# SPDX-License-Identifier: Apache-2.0 + +{ depot, ... }: +depot.third_party.buildGo.external { + path = "github.com/cenkalti/backoff/v4"; + src = depot.third_party.nixpkgs.fetchFromGitHub { + owner = "cenkalti"; + repo = "backoff"; + rev = "v4.1.2"; + hash = "sha256:08c28226q612i1pv83161y57qh16631vpc51ai9f76qfrzsy946z"; + }; +} diff --git a/third_party/gopkgs/github.com/go-stomp/stomp/v3/default.nix b/third_party/gopkgs/github.com/go-stomp/stomp/v3/default.nix new file mode 100644 index 0000000000..07684691be --- /dev/null +++ b/third_party/gopkgs/github.com/go-stomp/stomp/v3/default.nix @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: 2020 Luke Granger-Brown +# +# SPDX-License-Identifier: Apache-2.0 + +{ depot, ... }: +depot.third_party.buildGo.external { + path = "github.com/go-stomp/stomp/v3"; + src = depot.third_party.nixpkgs.fetchFromGitHub { + owner = "go-stomp"; + repo = "stomp"; + rev = "v3.0.3"; + hash = "sha256:0plm1p5nibqaaqbs4qxaxc3vr9h5xwx5jp3z2fv3nip07slg7rb6"; + }; +} diff --git a/third_party/gopkgs/github.com/jlaffaye/ftp/default.nix b/third_party/gopkgs/github.com/jlaffaye/ftp/default.nix new file mode 100644 index 0000000000..1aeefe3d26 --- /dev/null +++ b/third_party/gopkgs/github.com/jlaffaye/ftp/default.nix @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: 2020 Luke Granger-Brown +# +# SPDX-License-Identifier: Apache-2.0 + +{ depot, ... }: +depot.third_party.buildGo.external { + path = "github.com/jlaffaye/ftp"; + src = depot.third_party.nixpkgs.fetchFromGitHub { + owner = "jlaffaye"; + repo = "ftp"; + rev = "11820403398b118877718a169d6368edc6e3ef9a"; + hash = "sha256:18v142q292wp5y8flnq15iyfb33801xz1sg4c9pn2aibi546j011"; + }; +}