Skip to content

Commit 0f30acf

Browse files
committed
add convert flights action and add flightnumber endpoint
1 parent 2d93691 commit 0f30acf

14 files changed

+670
-72
lines changed

cdk/lib/constructs/sfn-construct.ts

+18-1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,22 @@ export class SfnConstruct extends Construct {
6060
resultPath: '$.convertSchedulesResponse',
6161
retryOnServiceExceptions: true,
6262
}))
63+
.next(new LambdaInvoke(this, 'ConvertFlightsTask', {
64+
lambdaFunction: props.cronLambda,
65+
payload: TaskInput.fromObject({
66+
'action': 'convert_flights',
67+
'params': {
68+
'inputBucket': props.dataBucket.bucketName,
69+
'inputPrefix': 'processed/flights/',
70+
'outputBucket': props.dataBucket.bucketName,
71+
'outputPrefix': 'processed/flight_numbers/',
72+
'dateRanges': JsonPath.objectAt('$.convertSchedulesResponse.dateRanges'),
73+
},
74+
}),
75+
payloadResponseOnly: true,
76+
resultPath: '$.convertNumbersResponse',
77+
retryOnServiceExceptions: true,
78+
}))
6379
.toSingleState('ConvertTry', { outputPath: '$[0]' })
6480
.addCatch(
6581
this.sendWebhookTask(
@@ -75,9 +91,10 @@ export class SfnConstruct extends Construct {
7591
props.cronLambda,
7692
props.webhookUrl,
7793
JsonPath.format(
78-
'FlightSchedules Cron {} succeeded:\n```json\n{}\n```',
94+
'FlightSchedules Cron {} succeeded:\nQueried:\n```json\n{}\n```\nTouched:\n```json\n{}\n```',
7995
JsonPath.stringAt('$.time'),
8096
JsonPath.jsonToString(JsonPath.objectAt('$.loadSchedulesResponse.loadFlightSchedules.input.dateRanges')),
97+
JsonPath.jsonToString(JsonPath.objectAt('$.convertSchedulesResponse.dateRanges')),
8198
),
8299
))
83100
.next(success);

go/api/data/handler.go

+24
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"context"
66
"encoding/csv"
77
"errors"
8+
"fmt"
89
"github.com/aws/aws-sdk-go-v2/aws"
910
"github.com/aws/aws-sdk-go-v2/service/s3"
1011
"github.com/explore-flights/monorepo/go/common"
@@ -255,6 +256,29 @@ func (h *Handler) Aircraft(ctx context.Context) ([]Aircraft, error) {
255256
return result, err
256257
}
257258

259+
func (h *Handler) FlightSchedule(ctx context.Context, fnRaw string) (*common.FlightSchedule, error) {
260+
fn, err := common.ParseFlightNumber(fnRaw)
261+
if err != nil {
262+
return nil, err
263+
}
264+
265+
fs, err := loadJson[*common.FlightSchedule](
266+
ctx,
267+
h,
268+
"processed/flight_numbers/"+fmt.Sprintf("%s/%d%s.json", fn.Airline, fn.Number, fn.Suffix),
269+
)
270+
271+
if err != nil {
272+
if adapt.IsS3NotFound(err) {
273+
return nil, nil
274+
} else {
275+
return nil, err
276+
}
277+
}
278+
279+
return fs, nil
280+
}
281+
258282
func (h *Handler) FlightNumber(ctx context.Context, fnRaw, airport string, d common.LocalDate) (*common.Flight, error) {
259283
flights, err := loadJson[[]*common.Flight](
260284
ctx,

go/api/main.go

+5-3
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ func main() {
4747

4848
jsonConnEdp := web.NewConnectionsEndpoint(connHandler, "json")
4949
pngConnEdp := web.NewConnectionsEndpoint(connHandler, "png")
50+
fnEdp := web.NewFlightNumberEndpoint(dataHandler)
5051

5152
e.POST("/api/connections/json", jsonConnEdp)
5253
e.GET("/api/connections/json/:payload", jsonConnEdp)
@@ -61,9 +62,10 @@ func main() {
6162
e.GET("/auth/oauth2/login/:issuer", authHandler.Login)
6263
e.GET("/auth/oauth2/code/:issuer", authHandler.Code)
6364

64-
e.GET("/data/airports.json", web.NewAirportsHandler(dataHandler))
65-
e.GET("/data/aircraft.json", web.NewAircraftHandler(dataHandler))
66-
e.GET("/data/flight/:fn/:airport/:date", web.NewFlightNumberHandler(dataHandler))
65+
e.GET("/data/airports.json", web.NewAirportsEndpoint(dataHandler))
66+
e.GET("/data/aircraft.json", web.NewAircraftEndpoint(dataHandler))
67+
e.GET("/data/flight/:fn", fnEdp)
68+
e.GET("/data/flight/:fn/:airport/:date", fnEdp)
6769

6870
if err := run(ctx, e); err != nil {
6971
panic(err)

go/api/web/data.go

+30-39
Original file line numberDiff line numberDiff line change
@@ -13,64 +13,55 @@ func noCache(c echo.Context) {
1313
c.Response().Header().Set(echo.HeaderCacheControl, "private, no-cache, no-store, max-age=0, must-revalidate")
1414
}
1515

16-
func NewAirportsHandler(dh *data.Handler) echo.HandlerFunc {
16+
func NewAirportsEndpoint(dh *data.Handler) echo.HandlerFunc {
1717
return func(c echo.Context) error {
1818
airports, err := dh.Airports(c.Request().Context())
19-
if err != nil {
20-
noCache(c)
21-
22-
if errors.Is(err, context.DeadlineExceeded) {
23-
return echo.NewHTTPError(http.StatusRequestTimeout, err)
24-
}
25-
26-
return echo.NewHTTPError(http.StatusInternalServerError)
27-
}
28-
29-
return c.JSON(http.StatusOK, airports)
19+
return jsonResponse(c, airports, err, func(v data.AirportsResponse) bool { return false })
3020
}
3121
}
3222

33-
func NewAircraftHandler(dh *data.Handler) echo.HandlerFunc {
23+
func NewAircraftEndpoint(dh *data.Handler) echo.HandlerFunc {
3424
return func(c echo.Context) error {
3525
aircraft, err := dh.Aircraft(c.Request().Context())
36-
if err != nil {
37-
noCache(c)
38-
39-
if errors.Is(err, context.DeadlineExceeded) {
40-
return echo.NewHTTPError(http.StatusRequestTimeout, err)
41-
}
42-
43-
return echo.NewHTTPError(http.StatusInternalServerError)
44-
}
45-
46-
return c.JSON(http.StatusOK, aircraft)
26+
return jsonResponse(c, aircraft, err, func(v []data.Aircraft) bool { return false })
4727
}
4828
}
4929

50-
func NewFlightNumberHandler(dh *data.Handler) echo.HandlerFunc {
30+
func NewFlightNumberEndpoint(dh *data.Handler) echo.HandlerFunc {
5131
return func(c echo.Context) error {
5232
fn := c.Param("fn")
5333
airport := c.Param("airport")
54-
d, err := common.ParseLocalDate(c.Param("date"))
55-
if err != nil {
56-
return echo.NewHTTPError(http.StatusBadRequest, err)
57-
}
58-
59-
flight, err := dh.FlightNumber(c.Request().Context(), fn, airport, d)
60-
if err != nil {
61-
noCache(c)
34+
dateRaw := c.Param("date")
6235

63-
if errors.Is(err, context.DeadlineExceeded) {
64-
return echo.NewHTTPError(http.StatusRequestTimeout, err)
36+
if airport != "" && dateRaw != "" {
37+
d, err := common.ParseLocalDate(dateRaw)
38+
if err != nil {
39+
return echo.NewHTTPError(http.StatusBadRequest, err)
6540
}
6641

67-
return echo.NewHTTPError(http.StatusInternalServerError)
42+
flight, err := dh.FlightNumber(c.Request().Context(), fn, airport, d)
43+
return jsonResponse(c, flight, err, func(v *common.Flight) bool { return v == nil })
44+
} else {
45+
fs, err := dh.FlightSchedule(c.Request().Context(), fn)
46+
return jsonResponse(c, fs, err, func(v *common.FlightSchedule) bool { return v == nil })
6847
}
48+
}
49+
}
6950

70-
if flight == nil {
71-
return echo.NewHTTPError(http.StatusNotFound)
51+
func jsonResponse[T any](c echo.Context, v T, err error, isEmpty func(T) bool) error {
52+
if err != nil {
53+
noCache(c)
54+
55+
if errors.Is(err, context.DeadlineExceeded) {
56+
return echo.NewHTTPError(http.StatusRequestTimeout, err)
7257
}
7358

74-
return c.JSON(http.StatusOK, flight)
59+
return echo.NewHTTPError(http.StatusInternalServerError)
60+
}
61+
62+
if isEmpty(v) {
63+
return echo.NewHTTPError(http.StatusNotFound)
7564
}
65+
66+
return c.JSON(http.StatusOK, v)
7667
}

go/common/flight_schedule.go

+43-12
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
package common
22

33
import (
4-
"maps"
4+
"slices"
5+
"time"
56
)
67

78
type FlightScheduleData struct {
@@ -13,8 +14,6 @@ type FlightScheduleData struct {
1314
AircraftOwner AirlineIdentifier `json:"aircraftOwner"`
1415
AircraftType string `json:"aircraftType"`
1516
AircraftConfigurationVersion string `json:"aircraftConfigurationVersion"`
16-
Registration string `json:"registration"`
17-
DataElements map[int]string `json:"dataElements"`
1817
CodeShares []FlightNumber `json:"codeShares"`
1918
}
2019

@@ -27,18 +26,27 @@ func (fsd FlightScheduleData) Equal(other FlightScheduleData) bool {
2726
fsd.AircraftOwner == other.AircraftOwner &&
2827
fsd.AircraftType == other.AircraftType &&
2928
fsd.AircraftConfigurationVersion == other.AircraftConfigurationVersion &&
30-
fsd.Registration == other.Registration &&
31-
maps.Equal(fsd.DataElements, other.DataElements) &&
3229
SliceEqualContent(fsd.CodeShares, other.CodeShares)
3330
}
3431

32+
type FlightScheduleAlias struct {
33+
FlightNumber FlightNumber `json:"flightNumber"`
34+
DepartureTime OffsetTime `json:"departureTime"`
35+
DepartureAirport string `json:"departureAirport"`
36+
}
37+
3538
type FlightScheduleVariant struct {
36-
Ranges []LocalDateRange `json:"ranges"`
37-
Data FlightScheduleData `json:"data"`
39+
Ranges LocalDateRanges `json:"ranges"`
40+
Data *FlightScheduleData `json:"data,omitempty"`
41+
Alias *FlightScheduleAlias `json:"alias,omitempty"`
3842
}
3943

40-
func (fsv *FlightScheduleVariant) Expand(d LocalDate) {
44+
func (fsv *FlightScheduleVariant) DepartureTime() OffsetTime {
45+
if fsv.Data != nil {
46+
return fsv.Data.DepartureTime
47+
}
4148

49+
return fsv.Alias.DepartureTime
4250
}
4351

4452
type FlightSchedule struct {
@@ -48,12 +56,35 @@ type FlightSchedule struct {
4856
Variants []*FlightScheduleVariant `json:"variants"`
4957
}
5058

51-
func (fs *FlightSchedule) DataVariant(fsd FlightScheduleData) *FlightScheduleVariant {
59+
func (fs *FlightSchedule) RemoveVariants(start, end time.Time) {
60+
fs.Variants = slices.DeleteFunc(fs.Variants, func(variant *FlightScheduleVariant) bool {
61+
for d := range variant.Ranges.Iter() {
62+
t := variant.DepartureTime().Time(d)
63+
if t.Compare(start) >= 0 && t.Compare(end) <= 0 {
64+
variant.Ranges = variant.Ranges.Remove(d)
65+
}
66+
}
67+
68+
return len(variant.Ranges) < 1
69+
})
70+
}
71+
72+
func (fs *FlightSchedule) DataVariant(fsd FlightScheduleData) (*FlightScheduleVariant, bool) {
73+
for _, variant := range fs.Variants {
74+
if variant.Data != nil && variant.Data.Equal(fsd) {
75+
return variant, true
76+
}
77+
}
78+
79+
return nil, false
80+
}
81+
82+
func (fs *FlightSchedule) AliasVariant(fsa FlightScheduleAlias) (*FlightScheduleVariant, bool) {
5283
for _, variant := range fs.Variants {
53-
if variant.Data.Equal(fsd) {
54-
return variant
84+
if variant.Alias != nil && *variant.Alias == fsa {
85+
return variant, true
5586
}
5687
}
5788

58-
return nil
89+
return nil, false
5990
}

go/common/go.mod

+4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ require (
77
github.com/aws/aws-sdk-go-v2/service/s3 v1.60.1
88
github.com/go-jose/go-jose/v4 v4.0.4
99
github.com/golang-jwt/jwt/v5 v5.2.1
10+
github.com/stretchr/testify v1.9.0
1011
golang.org/x/time v0.6.0
1112
)
1213

@@ -20,5 +21,8 @@ require (
2021
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18 // indirect
2122
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.16 // indirect
2223
github.com/aws/smithy-go v1.20.4 // indirect
24+
github.com/davecgh/go-spew v1.1.1 // indirect
25+
github.com/pmezard/go-difflib v1.0.0 // indirect
2326
golang.org/x/crypto v0.26.0 // indirect
27+
gopkg.in/yaml.v3 v3.0.1 // indirect
2428
)

go/common/go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,7 @@ golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
3636
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
3737
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
3838
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
39+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
40+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
3941
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
4042
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

0 commit comments

Comments
 (0)