Skip to content

Commit

Permalink
add allegris schedules overview
Browse files Browse the repository at this point in the history
  • Loading branch information
its-felix committed Nov 3, 2024
1 parent 0f6dc63 commit 7fa64e0
Show file tree
Hide file tree
Showing 11 changed files with 355 additions and 40 deletions.
133 changes: 94 additions & 39 deletions go/api/data/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/explore-flights/monorepo/go/common/lufthansa"
"github.com/explore-flights/monorepo/go/common/xtime"
"io"
"iter"
"log/slog"
"net/http"
"slices"
Expand Down Expand Up @@ -158,6 +159,12 @@ type Aircraft struct {
Name string `json:"name"`
}

type RouteAndRanges struct {
DepartureAirport string `json:"departureAirport"`
ArrivalAirport string `json:"arrivalAirport"`
Ranges xtime.LocalDateRanges `json:"ranges"`
}

type MinimalS3Client interface {
adapt.S3Getter
adapt.S3Lister
Expand Down Expand Up @@ -338,48 +345,25 @@ func (h *Handler) Aircraft(ctx context.Context) ([]Aircraft, error) {
}

func (h *Handler) FlightSchedule(ctx context.Context, fn common.FlightNumber) (*common.FlightSchedule, error) {
resp, err := h.s3c.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(h.bucket),
Key: aws.String(fmt.Sprintf("processed/schedules/%s.json.gz", fn.Airline)),
})

if err != nil {
if adapt.IsS3NotFound(err) {
return nil, nil
} else {
return nil, err
}
}

defer resp.Body.Close()

r, err := gzip.NewReader(resp.Body)
if err != nil {
return nil, err
}

decoder := jstream.NewDecoder(r, 1).EmitKV()
for mv := range decoder.Stream() {
if kv := mv.Value.(jstream.KV); kv.Key == fn.String() {
b, err := json.Marshal(kv.Value)
if err != nil {
return nil, err
}

var fs *common.FlightSchedule
if err = json.Unmarshal(b, &fs); err != nil {
return nil, err
var fs *common.FlightSchedule
return fs, h.flightSchedules(ctx, fn.Airline, func(seq iter.Seq[jstream.KV]) error {
for kv := range seq {
if kv.Key == fn.String() {
b, err := json.Marshal(kv.Value)
if err != nil {
return err
}

if err = json.Unmarshal(b, &fs); err != nil {
return err
}

return nil
}

return fs, nil
}
}

if err = decoder.Err(); err != nil {
return nil, err
}

return nil, nil
return nil
})
}

func (h *Handler) FlightNumbers(ctx context.Context, prefix string, limit int) ([]common.FlightNumber, error) {
Expand Down Expand Up @@ -500,6 +484,77 @@ func (h *Handler) seatMapS3Key(fn common.FlightNumber, departureAirport, arrival
return fmt.Sprintf("tmp/seatmap/%s/%s/%s/%s/%s.json", fn.String(), departureAirport, arrivalAirport, departureDate.String(), cabinClass)
}

func (h *Handler) QuerySchedules(ctx context.Context, airline common.AirlineIdentifier, aircraftType, aircraftConfigurationVersion string) (map[common.FlightNumber][]RouteAndRanges, error) {
result := make(map[common.FlightNumber][]RouteAndRanges)
return result, h.flightSchedules(ctx, airline, func(seq iter.Seq[jstream.KV]) error {
for kv := range seq {
b, err := json.Marshal(kv.Value)
if err != nil {
return err
}

var fs *common.FlightSchedule
if err = json.Unmarshal(b, &fs); err != nil {
return err
}

for _, variant := range fs.Variants {
if variant.Data.ServiceType == "J" && variant.Data.AircraftType == aircraftType && variant.Data.AircraftConfigurationVersion == aircraftConfigurationVersion {
fn := fs.Number()
result[fn] = append(result[fn], RouteAndRanges{
DepartureAirport: variant.Data.DepartureAirport,
ArrivalAirport: variant.Data.ArrivalAirport,
Ranges: variant.Ranges,
})
}
}
}

return nil
})
}

func (h *Handler) flightSchedules(ctx context.Context, airline common.AirlineIdentifier, fn func(seq iter.Seq[jstream.KV]) error) error {
resp, err := h.s3c.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(h.bucket),
Key: aws.String(fmt.Sprintf("processed/schedules/%s.json.gz", airline)),
})

if err != nil {
if adapt.IsS3NotFound(err) {
return nil
} else {
return err
}
}

defer resp.Body.Close()

r, err := gzip.NewReader(resp.Body)
if err != nil {
return err
}

decoder := jstream.NewDecoder(r, 1).EmitKV()
err = fn(func(yield func(jstream.KV) bool) {
for mv := range decoder.Stream() {
if !yield(mv.Value.(jstream.KV)) {
return
}
}
})

if err != nil {
return err
}

if err = decoder.Err(); err != nil {
return err
}

return nil
}

func (h *Handler) loadCsv(ctx context.Context, name string) (*csvReader, error) {
resp, err := h.s3c.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(h.bucket),
Expand Down
1 change: 1 addition & 0 deletions go/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ func main() {
e.GET("/data/aircraft.json", web.NewAircraftEndpoint(dataHandler))
e.GET("/data/flight/:fn", web.NewFlightNumberEndpoint(dataHandler))
e.GET("/data/flight/:fn/seatmap/:departure/:arrival/:date/:aircraft", web.NewSeatMapEndpoint(dataHandler))
e.GET("/data/:airline/schedule/:aircraftType/:aircraftConfigurationVersion", web.NewQueryFlightSchedulesEndpoint(dataHandler))

if err := run(ctx, e); err != nil {
panic(err)
Expand Down
23 changes: 23 additions & 0 deletions go/api/web/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,29 @@ func NewSeatMapEndpoint(dh *data.Handler) echo.HandlerFunc {
}
}

func NewQueryFlightSchedulesEndpoint(dh *data.Handler) echo.HandlerFunc {
return func(c echo.Context) error {
airline := strings.ToUpper(c.Param("airline"))
aircraftType := strings.ToUpper(c.Param("aircraftType"))
aircraftConfigurationVersion := strings.ToUpper(c.Param("aircraftConfigurationVersion"))

result, err := dh.QuerySchedules(
c.Request().Context(),
common.AirlineIdentifier(airline),
aircraftType,
aircraftConfigurationVersion,
)

if err != nil {
noCache(c)
return echo.NewHTTPError(http.StatusInternalServerError)
}

addExpirationHeaders(c, time.Now(), time.Hour*3)
return c.JSON(http.StatusOK, result)
}
}

func jsonResponse[T any](c echo.Context, v T, err error, isEmpty func(T) bool) error {
if err != nil {
noCache(c)
Expand Down
3 changes: 3 additions & 0 deletions ui/public/sitemap_static.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
<url>
<loc>https://explore.flights/privacy-policy</loc>
</url>
<url>
<loc>https://explore.flights/allegris</loc>
</url>
<url>
<loc>https://explore.flights/tools/mm-quick-search</loc>
</url>
Expand Down
6 changes: 6 additions & 0 deletions ui/src/components/sidenav/sidenav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ export function SideNav() {
return (
<SideNavigation
items={[
{
type: 'link',
text: 'Allegris',
href: useHref('/allegris'),
},
{ type: 'divider' },
{
type: 'section-group',
title: 'Tools',
Expand Down
20 changes: 20 additions & 0 deletions ui/src/components/util/state/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,26 @@ export function useSeatMap(flightNumber: string,
});
}

export function useQueryFlightSchedules(airline: string, aircraftType: string, aircraftConfigurationVersion: string) {
const { apiClient } = useHttpClient();
return useQuery({
queryKey: ['query_flight_schedules', airline, aircraftType, aircraftConfigurationVersion],
queryFn: async () => {
const { body } = expectSuccess(await apiClient.queryFlightSchedules(airline, aircraftType, aircraftConfigurationVersion));
return body;
},
retry: (count, e) => {
if (count > 3) {
return false;
} else if (e instanceof ApiError && (e.response.status === 400 || e.response.status === 404)) {
return false;
}

return true;
},
});
}

export function useSearch(query: string, enabled: boolean) {
const { apiClient } = useHttpClient();
return useQuery({
Expand Down
5 changes: 5 additions & 0 deletions ui/src/components/util/state/use-route-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ const ROUTES = [{
},
],
},
{
path: 'allegris',
title: 'Allegris',
breadcrumb: 'Allegris',
},
{
path: 'tools',
children: [
Expand Down
8 changes: 8 additions & 0 deletions ui/src/lib/api/api.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,3 +234,11 @@ export enum ComponentFeature {
STAIRS = 'ST',
TABLE = 'TA',
}

export type QueryScheduleResponse = Record<string, ReadonlyArray<RouteAndRanges>>;

export interface RouteAndRanges {
departureAirport: string;
arrivalAirport: string;
ranges: ReadonlyArray<[string, string]>;
}
6 changes: 5 additions & 1 deletion ui/src/lib/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
ConnectionsSearchRequest,
ConnectionsSearchResponseWithSearch,
ConnectionsSearchResponse,
FlightSchedule, SeatMap
FlightSchedule, SeatMap, QueryScheduleResponse
} from './api.model';
import { ConcurrencyLimit } from './concurrency-limit';
import { DateTime } from 'luxon';
Expand Down Expand Up @@ -122,6 +122,10 @@ export class ApiClient {
return transform(this.httpClient.fetch(url));
}

queryFlightSchedules(airline: string, aircraftType: string, aircraftConfigurationVersion: string): Promise<ApiResponse<QueryScheduleResponse>> {
return transform(this.httpClient.fetch(`/data/${encodeURIComponent(airline)}/schedule/${encodeURIComponent(aircraftType)}/${aircraftConfigurationVersion}`));
}

search(query: string): Promise<ApiResponse<ReadonlyArray<string>>> {
const params = new URLSearchParams();
params.set('q', query);
Expand Down
Loading

0 comments on commit 7fa64e0

Please sign in to comment.