From 1ffc9c42054e208a01d3e70e6b6f3e1781e798f8 Mon Sep 17 00:00:00 2001 From: Julien Dessaux Date: Mon, 5 Apr 2021 17:52:31 +0200 Subject: Moved code around to conform best practices --- config/config.go | 55 - config/config_test.go | 54 - config/error.go | 92 - config/error_test.go | 20 - config/test_data/invalid.yaml | 1 - config/test_data/invalid_address.yaml | 3 - config/test_data/invalid_address_unresolvable.yaml | 3 - config/test_data/invalid_port.yaml | 3 - config/test_data/invalid_token.yaml | 3 - config/test_data/minimal.yaml | 3 - config/test_data/minimal_with_hostname.yaml | 3 - navitia_api_client/client.go | 31 - navitia_api_client/client_test.go | 38 - navitia_api_client/departures.go | 73 - navitia_api_client/departures_test.go | 57 - navitia_api_client/test_data/invalid.json | 1 - navitia_api_client/test_data/normal-crepieux.json | 2135 -------------------- pkg/config/config.go | 55 + pkg/config/config_test.go | 54 + pkg/config/error.go | 92 + pkg/config/error_test.go | 20 + pkg/config/test_data/invalid.yaml | 1 + pkg/config/test_data/invalid_address.yaml | 3 + .../test_data/invalid_address_unresolvable.yaml | 3 + pkg/config/test_data/invalid_port.yaml | 3 + pkg/config/test_data/invalid_token.yaml | 3 + pkg/config/test_data/minimal.yaml | 3 + pkg/config/test_data/minimal_with_hostname.yaml | 3 + pkg/navitia_api_client/client.go | 31 + pkg/navitia_api_client/client_test.go | 38 + pkg/navitia_api_client/departures.go | 73 + pkg/navitia_api_client/departures_test.go | 57 + pkg/navitia_api_client/test_data/invalid.json | 1 + .../test_data/normal-crepieux.json | 2135 ++++++++++++++++++++ 34 files changed, 2575 insertions(+), 2575 deletions(-) delete mode 100644 config/config.go delete mode 100644 config/config_test.go delete mode 100644 config/error.go delete mode 100644 config/error_test.go delete mode 100644 config/test_data/invalid.yaml delete mode 100644 config/test_data/invalid_address.yaml delete mode 100644 config/test_data/invalid_address_unresolvable.yaml delete mode 100644 config/test_data/invalid_port.yaml delete mode 100644 config/test_data/invalid_token.yaml delete mode 100644 config/test_data/minimal.yaml delete mode 100644 config/test_data/minimal_with_hostname.yaml delete mode 100644 navitia_api_client/client.go delete mode 100644 navitia_api_client/client_test.go delete mode 100644 navitia_api_client/departures.go delete mode 100644 navitia_api_client/departures_test.go delete mode 100644 navitia_api_client/test_data/invalid.json delete mode 100644 navitia_api_client/test_data/normal-crepieux.json create mode 100644 pkg/config/config.go create mode 100644 pkg/config/config_test.go create mode 100644 pkg/config/error.go create mode 100644 pkg/config/error_test.go create mode 100644 pkg/config/test_data/invalid.yaml create mode 100644 pkg/config/test_data/invalid_address.yaml create mode 100644 pkg/config/test_data/invalid_address_unresolvable.yaml create mode 100644 pkg/config/test_data/invalid_port.yaml create mode 100644 pkg/config/test_data/invalid_token.yaml create mode 100644 pkg/config/test_data/minimal.yaml create mode 100644 pkg/config/test_data/minimal_with_hostname.yaml create mode 100644 pkg/navitia_api_client/client.go create mode 100644 pkg/navitia_api_client/client_test.go create mode 100644 pkg/navitia_api_client/departures.go create mode 100644 pkg/navitia_api_client/departures_test.go create mode 100644 pkg/navitia_api_client/test_data/invalid.json create mode 100644 pkg/navitia_api_client/test_data/normal-crepieux.json diff --git a/config/config.go b/config/config.go deleted file mode 100644 index f97467a..0000000 --- a/config/config.go +++ /dev/null @@ -1,55 +0,0 @@ -package config - -import ( - "net" - "os" - "regexp" - - "gopkg.in/yaml.v3" -) - -var validToken = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`) - -type Config struct { - // Address is the hostname or ip the web server will listen to - Address string `yaml:"address",default:"127.0.0.1"` - Port string `yaml:"port",default:"8080"` - // Token is the sncf api token - Token string `yaml:"token"` -} - -func (c *Config) validate() error { - // address - if ip := net.ParseIP(c.Address); ip == nil { - if _, err := net.LookupIP(c.Address); err != nil { - return newInvalidAddressError(c.Address, err) - } - } - // port - if _, err := net.LookupPort("tcp", c.Port); err != nil { - return newInvalidPortError(c.Port, err) - } - // token - if ok := validToken.MatchString(c.Token); !ok { - return newInvalidTokenError(c.Token) - } - return nil -} - -// LoadFile loads the c from a given file -func LoadFile(path string) (*Config, error) { - var c *Config - f, errOpen := os.Open(path) - if errOpen != nil { - return nil, newOpenError(path, errOpen) - } - defer f.Close() - decoder := yaml.NewDecoder(f) - if err := decoder.Decode(&c); err != nil { - return nil, newDecodeError(path, err) - } - if err := c.validate(); err != nil { - return nil, err - } - return c, nil -} diff --git a/config/config_test.go b/config/config_test.go deleted file mode 100644 index 3904b5d..0000000 --- a/config/config_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package config - -import ( - "reflect" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestLoadFile(t *testing.T) { - // Minimal yaml file - minimalConfig := Config{ - Address: "127.0.0.2", - Port: "8082", - Token: "12345678-9abc-def0-1234-56789abcdef0", - } - - // Minimal yaml file with hostname resolving - minimalConfigWithResolving := Config{ - Address: "localhost", - Port: "8082", - Token: "12345678-9abc-def0-1234-56789abcdef0", - } - - // Test cases - testCases := []struct { - name string - input string - expected *Config - expectedError interface{} - }{ - {"Non existant file", "test_data/non-existant", nil, &OpenError{}}, - {"Invalid file content", "test_data/invalid.yaml", nil, &DecodeError{}}, - {"Invalid address should fail to load", "test_data/invalid_address.yaml", nil, &InvalidAddressError{}}, - {"Unresolvable address should fail to load", "test_data/invalid_address_unresolvable.yaml", nil, &InvalidAddressError{}}, - {"Invalid port should fail to load", "test_data/invalid_port.yaml", nil, &InvalidPortError{}}, - {"Invalid token should fail to load", "test_data/invalid_token.yaml", nil, &InvalidTokenError{}}, - {"Minimal config", "test_data/minimal.yaml", &minimalConfig, nil}, - {"Minimal config with resolving", "test_data/minimal_with_hostname.yaml", &minimalConfigWithResolving, nil}, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - valid, err := LoadFile(tc.input) - if tc.expectedError != nil { - require.Error(t, err) - assert.Equalf(t, reflect.TypeOf(err), reflect.TypeOf(tc.expectedError), "Invalid error type. Got %s but expected %s", reflect.TypeOf(err), reflect.TypeOf(tc.expectedError)) - } else { - require.NoError(t, err) - } - assert.Equal(t, tc.expected, valid, "Invalid value") - }) - } -} diff --git a/config/error.go b/config/error.go deleted file mode 100644 index c49b6a9..0000000 --- a/config/error.go +++ /dev/null @@ -1,92 +0,0 @@ -package config - -import "fmt" - -type ErrorType int - -// file open configuration file error -type OpenError struct { - path string - err error -} - -func (e *OpenError) Error() string { - return fmt.Sprintf("Failed to open configuration file : %s", e.path) -} -func (e *OpenError) Unwrap() error { return e.err } - -func newOpenError(path string, err error) error { - return &OpenError{ - path: path, - err: err, - } -} - -// Yaml configuration file decoding error -type DecodeError struct { - path string - err error -} - -func (e *DecodeError) Error() string { - return fmt.Sprintf("Failed to decode configuration file : %s", e.path) -} -func (e *DecodeError) Unwrap() error { return e.err } - -func newDecodeError(path string, err error) error { - return &DecodeError{ - path: path, - err: err, - } -} - -// Invalid address field error -type InvalidAddressError struct { - address string - err error -} - -func (e *InvalidAddressError) Error() string { - return fmt.Sprintf("Invalid address %s : it must be a valid ipv4 address, ipv6 address, or resolvable name", e.address) -} -func (e *InvalidAddressError) Unwrap() error { return e.err } - -func newInvalidAddressError(address string, err error) error { - return &InvalidAddressError{ - address: address, - err: err, - } -} - -// Invalid port field error -type InvalidPortError struct { - port string - err error -} - -func (e *InvalidPortError) Error() string { - return fmt.Sprintf("Invalid port %s : it must be a valid port number or tcp service name", e.port) -} -func (e *InvalidPortError) Unwrap() error { return e.err } - -func newInvalidPortError(port string, err error) error { - return &InvalidPortError{ - port: port, - err: err, - } -} - -// Invalid token field error -type InvalidTokenError struct { - token string -} - -func (e *InvalidTokenError) Error() string { - return fmt.Sprintf("Invalid token %s : it must be an hexadecimal string that lookslike 12345678-9abc-def0-1234-56789abcdef0", e.token) -} - -func newInvalidTokenError(token string) error { - return &InvalidTokenError{ - token: token, - } -} diff --git a/config/error_test.go b/config/error_test.go deleted file mode 100644 index f9807c1..0000000 --- a/config/error_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package config - -import "testing" - -func TestErrorsCoverage(t *testing.T) { - openErr := OpenError{} - _ = openErr.Error() - _ = openErr.Unwrap() - decodeErr := DecodeError{} - _ = decodeErr.Error() - _ = decodeErr.Unwrap() - invalidAddressErr := InvalidAddressError{} - _ = invalidAddressErr.Error() - _ = invalidAddressErr.Unwrap() - invalidPortErr := InvalidPortError{} - _ = invalidPortErr.Error() - _ = invalidPortErr.Unwrap() - invalidTokenErr := InvalidTokenError{} - _ = invalidTokenErr.Error() -} diff --git a/config/test_data/invalid.yaml b/config/test_data/invalid.yaml deleted file mode 100644 index db1ddad..0000000 --- a/config/test_data/invalid.yaml +++ /dev/null @@ -1 +0,0 @@ -blargh(ads) diff --git a/config/test_data/invalid_address.yaml b/config/test_data/invalid_address.yaml deleted file mode 100644 index e3682b2..0000000 --- a/config/test_data/invalid_address.yaml +++ /dev/null @@ -1,3 +0,0 @@ -address: "0.0.0.0.0" -port: 8082 -token: 12345678-9abc-def0-1234-56789abcdef0 diff --git a/config/test_data/invalid_address_unresolvable.yaml b/config/test_data/invalid_address_unresolvable.yaml deleted file mode 100644 index d476093..0000000 --- a/config/test_data/invalid_address_unresolvable.yaml +++ /dev/null @@ -1,3 +0,0 @@ -address: "invalid" -port: 8082 -token: 12345678-9abc-def0-1234-56789abcdef0 diff --git a/config/test_data/invalid_port.yaml b/config/test_data/invalid_port.yaml deleted file mode 100644 index 2790b3f..0000000 --- a/config/test_data/invalid_port.yaml +++ /dev/null @@ -1,3 +0,0 @@ -address: localhost -port: invalid -token: 12345678-9abc-def0-1234-56789abcdef0 diff --git a/config/test_data/invalid_token.yaml b/config/test_data/invalid_token.yaml deleted file mode 100644 index 6f9e297..0000000 --- a/config/test_data/invalid_token.yaml +++ /dev/null @@ -1,3 +0,0 @@ -address: 127.0.0.1 -port: 8080 -token: invalid diff --git a/config/test_data/minimal.yaml b/config/test_data/minimal.yaml deleted file mode 100644 index 2944645..0000000 --- a/config/test_data/minimal.yaml +++ /dev/null @@ -1,3 +0,0 @@ -address: 127.0.0.2 -port: 8082 -token: 12345678-9abc-def0-1234-56789abcdef0 diff --git a/config/test_data/minimal_with_hostname.yaml b/config/test_data/minimal_with_hostname.yaml deleted file mode 100644 index 716b2d5..0000000 --- a/config/test_data/minimal_with_hostname.yaml +++ /dev/null @@ -1,3 +0,0 @@ -address: localhost -port: 8082 -token: 12345678-9abc-def0-1234-56789abcdef0 diff --git a/navitia_api_client/client.go b/navitia_api_client/client.go deleted file mode 100644 index aef8d0e..0000000 --- a/navitia_api_client/client.go +++ /dev/null @@ -1,31 +0,0 @@ -package navitia_api_client - -import ( - "fmt" - "net/http" - "sync" - "time" -) - -type Client struct { - baseURL string - httpClient *http.Client - - mutex sync.Mutex - cache map[string]cachedResult -} - -type cachedResult struct { - ts time.Time - result interface{} -} - -func NewClient(token string) *Client { - return &Client{ - baseURL: fmt.Sprintf("https://%s@api.sncf.com/v1", token), - httpClient: &http.Client{ - Timeout: time.Minute, - }, - cache: make(map[string]cachedResult), - } -} diff --git a/navitia_api_client/client_test.go b/navitia_api_client/client_test.go deleted file mode 100644 index 332b222..0000000 --- a/navitia_api_client/client_test.go +++ /dev/null @@ -1,38 +0,0 @@ -package navitia_api_client - -import ( - "fmt" - "io/ioutil" - "net/http" - "net/http/httptest" - "testing" -) - -// package utilities -func NewTestClient(ts *httptest.Server) *Client { - return &Client{ - baseURL: fmt.Sprintf(ts.URL), - httpClient: ts.Client(), - cache: make(map[string]cachedResult), - } -} - -func NewTestClientFromFilename(t *testing.T, filename string) (*Client, *httptest.Server) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - page, err := ioutil.ReadFile(filename) - if err != nil { - t.Fatalf("Could not open test file : %s", err) - } - w.Write(page) - })) - return NewTestClient(ts), ts -} - -// tests -func TestNewClient(t *testing.T) { - client := NewClient("test") - want := "https://test@api.sncf.com/v1" - if client.baseURL != want { - t.Fatal("Invalid new client") - } -} diff --git a/navitia_api_client/departures.go b/navitia_api_client/departures.go deleted file mode 100644 index 7738940..0000000 --- a/navitia_api_client/departures.go +++ /dev/null @@ -1,73 +0,0 @@ -package navitia_api_client - -import ( - "encoding/json" - "fmt" - "net/http" - "time" -) - -type DeparturesResponse struct { - Disruptions []interface{} `json:"disruptions"` - Notes []interface{} `json:"notes"` - Departures []struct { - DisplayInformations struct { - Direction string `json:"direction"` - Code string `json:"code"` - Network string `json:"network"` - Links []interface{} `json:"links"` - Color string `json:"color"` - Name string `json:"name"` - PhysicalMode string `json:"physical_mode"` - Headsign string `json:"headsign"` - Label string `json:"label"` - Equipments []interface{} `json:"equipments"` - TextColor string `json:"text_color"` - TripShortName string `json:"trip_short_name"` - CommercialMode string `json:"commercial_mode"` - Description string `json:"description"` - } `json:"display_informations"` - StopDateTime struct { - Links []interface{} `json:"links"` - ArrivalDateTime string `json:"arrival_date_time"` - AdditionalInformations []interface{} `json:"additional_informations"` - DepartureDateTime string `json:"departure_date_time"` - BaseArrivalDateTime string `json:"base_arrival_date_time"` - BaseDepartureDateTime string `json:"base_departure_date_time"` - DataFreshness string `json:"data_freshness"` - } `json:"stop_date_time"` - } `json:"departures"` - Context struct { - Timezone string `json:"timezone"` - CurrentDatetime string `json:"current_datetime"` - } `json:"context"` -} - -func (c *Client) GetDepartures() (departures *DeparturesResponse, err error) { - request := fmt.Sprintf("%s/coverage/sncf/stop_areas/stop_area:SNCF:87723502/departures", c.baseURL) - start := time.Now() - c.mutex.Lock() - defer c.mutex.Unlock() - if cachedResult, ok := c.cache[request]; ok { - if start.Sub(cachedResult.ts) < 60*1000*1000*1000 { - return cachedResult.result.(*DeparturesResponse), nil - } - } - req, err := http.NewRequest("GET", request, nil) - if err != nil { - return nil, err - } - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - if err = json.NewDecoder(resp.Body).Decode(&departures); err != nil { - return nil, err - } - c.cache[request] = cachedResult{ - ts: start, - result: departures, - } - return -} diff --git a/navitia_api_client/departures_test.go b/navitia_api_client/departures_test.go deleted file mode 100644 index a1658d2..0000000 --- a/navitia_api_client/departures_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package navitia_api_client - -import ( - "net/http" - "net/http/httptest" - "testing" -) - -func TestGetDepartures(t *testing.T) { - // invalid characters in token - client := NewClient("}") - _, err := client.GetDepartures() - if err == nil { - t.Fatalf("invalid characters in token should raise an error") - } - // unreachable server - client = NewClient("https://") - _, err = client.GetDepartures() - if err == nil { - t.Fatalf("unreachable server should raise an error") - } - // invalid json - client, ts := NewTestClientFromFilename(t, "test_data/invalid.json") - defer ts.Close() - _, err = client.GetDepartures() - if err == nil { - t.Fatalf("invalid json should raise an error") - } - // http error - ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusNotFound) - })) - client = NewTestClient(ts) - _, err = client.GetDepartures() - if err == nil { - t.Fatalf("404 should raise an error") - } - // normal working request - client, ts = NewTestClientFromFilename(t, "test_data/normal-crepieux.json") - defer ts.Close() - departures, err := client.GetDepartures() - if err != nil { - t.Fatalf("could not get normal-crepieux departures : %s", err) - } - if len(departures.Departures) != 10 { - t.Fatalf("did not decode normal-crepieux departures properly, got %d departures when expected 10", len(departures.Departures)) - } - // test the cache (assuming the test takes less than 60 seconds (and it really should) it will be accurate) - ts.Close() - departures, err = client.GetDepartures() - if err != nil { - t.Fatalf("could not get normal-crepieux departures : %s", err) - } - if len(departures.Departures) != 10 { - t.Fatalf("did not decode normal-crepieux departures properly, got %d departures when expected 10", len(departures.Departures)) - } -} diff --git a/navitia_api_client/test_data/invalid.json b/navitia_api_client/test_data/invalid.json deleted file mode 100644 index 98232c6..0000000 --- a/navitia_api_client/test_data/invalid.json +++ /dev/null @@ -1 +0,0 @@ -{ diff --git a/navitia_api_client/test_data/normal-crepieux.json b/navitia_api_client/test_data/normal-crepieux.json deleted file mode 100644 index 19a635a..0000000 --- a/navitia_api_client/test_data/normal-crepieux.json +++ /dev/null @@ -1,2135 +0,0 @@ -{ - "pagination": { - "start_page": 0, - "items_on_page": 10, - "items_per_page": 10, - "total_result": 10 - }, - "links": [ - { - "href": "https://api.sncf.com/v1/coverage/sncf/stop_points/{stop_point.id}", - "type": "stop_point", - "rel": "stop_points", - "templated": true - }, - { - "href": "https://api.sncf.com/v1/coverage/sncf/commercial_modes/{commercial_modes.id}", - "type": "commercial_modes", - "rel": "commercial_modes", - "templated": true - }, - { - "href": "https://api.sncf.com/v1/coverage/sncf/stop_areas/{stop_area.id}", - "type": "stop_area", - "rel": "stop_areas", - "templated": true - }, - { - "href": "https://api.sncf.com/v1/coverage/sncf/physical_modes/{physical_modes.id}", - "type": "physical_modes", - "rel": "physical_modes", - "templated": true - }, - { - "href": "https://api.sncf.com/v1/coverage/sncf/routes/{route.id}", - "type": "route", - "rel": "routes", - "templated": true - }, - { - "href": "https://api.sncf.com/v1/coverage/sncf/commercial_modes/{commercial_mode.id}", - "type": "commercial_mode", - "rel": "commercial_modes", - "templated": true - }, - { - "href": "https://api.sncf.com/v1/coverage/sncf/vehicle_journeys/{vehicle_journey.id}", - "type": "vehicle_journey", - "rel": "vehicle_journeys", - "templated": true - }, - { - "href": "https://api.sncf.com/v1/coverage/sncf/lines/{line.id}", - "type": "line", - "rel": "lines", - "templated": true - }, - { - "href": "https://api.sncf.com/v1/coverage/sncf/physical_modes/{physical_mode.id}", - "type": "physical_mode", - "rel": "physical_modes", - "templated": true - }, - { - "href": "https://api.sncf.com/v1/coverage/sncf/networks/{network.id}", - "type": "network", - "rel": "networks", - "templated": true - }, - { - "href": "https://api.sncf.com/v1/coverage/sncf/stop_areas/stop_area:OCE:SA:87723502/departures", - "type": "first", - "templated": false - } - ], - "disruptions": [], - "notes": [], - "feed_publishers": [], - "departures": [ - { - "display_informations": { - "direction": "Ambérieu-en-Bugey (Ambérieu-en-Bugey)", - "code": "", - "network": "SNCF", - "links": [], - "color": "000000", - "name": "St-Etienne - Lyon - Ambérieu", - "physical_mode": "Train régional / TER", - "headsign": "886823", - "label": "St-Etienne - Lyon - Ambérieu", - "equipments": [], - "text_color": "FFFFFF", - "trip_short_name": "886823", - "commercial_mode": "TER", - "description": "" - }, - "stop_point": { - "commercial_modes": [ - { - "id": "commercial_mode:ter", - "name": "TER" - } - ], - "name": "Crépieux-la-Pape", - "links": [], - "physical_modes": [ - { - "id": "physical_mode:LocalTrain", - "name": "Train régional / TER" - } - ], - "coord": { - "lat": "45.803921", - "lon": "4.892737" - }, - "label": "Crépieux-la-Pape (Rillieux-la-Pape)", - "equipments": [], - "administrative_regions": [ - { - "insee": "69286", - "name": "Rillieux-la-Pape", - "level": 8, - "coord": { - "lat": "45.823514", - "lon": "4.8994366" - }, - "label": "Rillieux-la-Pape (69140)", - "id": "admin:fr:69286", - "zip_code": "69140" - } - ], - "fare_zone": { - "name": "0" - }, - "id": "stop_point:OCE:SP:TrainTER-87723502", - "stop_area": { - "codes": [ - { - "type": "CR-CI-CH", - "value": "0087-723502-00" - }, - { - "type": "UIC8", - "value": "87723502" - }, - { - "type": "external_code", - "value": "OCE87723502" - } - ], - "name": "Crépieux-la-Pape", - "links": [], - "coord": { - "lat": "45.803921", - "lon": "4.892737" - }, - "label": "Crépieux-la-Pape (Rillieux-la-Pape)", - "administrative_regions": [ - { - "insee": "69286", - "name": "Rillieux-la-Pape", - "level": 8, - "coord": { - "lat": "45.823514", - "lon": "4.8994366" - }, - "label": "Rillieux-la-Pape (69140)", - "id": "admin:fr:69286", - "zip_code": "69140" - } - ], - "timezone": "Europe/Paris", - "id": "stop_area:OCE:SA:87723502" - } - }, - "route": { - "direction": { - "embedded_type": "stop_area", - "stop_area": { - "codes": [ - { - "type": "CR-CI-CH", - "value": "0087-743716-BV" - }, - { - "type": "UIC8", - "value": "87743716" - }, - { - "type": "external_code", - "value": "OCE87743716" - } - ], - "name": "Ambérieu-en-Bugey", - "links": [], - "coord": { - "lat": "45.954008", - "lon": "5.342313" - }, - "label": "Ambérieu-en-Bugey (Ambérieu-en-Bugey)", - "timezone": "Europe/Paris", - "id": "stop_area:OCE:SA:87743716" - }, - "quality": 0, - "name": "Ambérieu-en-Bugey (Ambérieu-en-Bugey)", - "id": "stop_area:OCE:SA:87743716" - }, - "name": "St-Etienne-Châteaucreux vers Ambérieu-en-Bugey (Train TER)", - "links": [], - "physical_modes": [ - { - "id": "physical_mode:LocalTrain", - "name": "Train régional / TER" - } - ], - "is_frequence": "False", - "geojson": { - "type": "MultiLineString", - "coordinates": [] - }, - "direction_type": "forward", - "line": { - "code": "", - "name": "St-Etienne - Lyon - Ambérieu", - "links": [], - "color": "000000", - "geojson": { - "type": "MultiLineString", - "coordinates": [] - }, - "text_color": "FFFFFF", - "physical_modes": [ - { - "id": "physical_mode:LocalTrain", - "name": "Train régional / TER" - } - ], - "codes": [], - "closing_time": "221200", - "opening_time": "053500", - "commercial_mode": { - "id": "commercial_mode:ter", - "name": "TER" - }, - "id": "line:OCE:199" - }, - "id": "route:OCE:199-TrainTER-87726000-87743716" - }, - "links": [ - { - "type": "line", - "id": "line:OCE:199" - }, - { - "type": "vehicle_journey", - "id": "vehicle_journey:OCE:SN886823F29029_dst_1" - }, - { - "type": "route", - "id": "route:OCE:199-TrainTER-87726000-87743716" - }, - { - "type": "commercial_mode", - "id": "commercial_mode:ter" - }, - { - "type": "physical_mode", - "id": "physical_mode:LocalTrain" - }, - { - "type": "network", - "id": "network:sncf" - } - ], - "stop_date_time": { - "links": [], - "arrival_date_time": "20210218T131800", - "additional_informations": [], - "departure_date_time": "20210218T131800", - "base_arrival_date_time": "20210218T131800", - "base_departure_date_time": "20210218T131800", - "data_freshness": "base_schedule" - } - }, - { - "display_informations": { - "direction": "St-Etienne-Châteaucreux (Saint-Étienne)", - "code": "", - "network": "SNCF", - "links": [], - "color": "000000", - "name": "St-Etienne - Lyon - Ambérieu", - "physical_mode": "Train régional / TER", - "headsign": "886726", - "label": "St-Etienne - Lyon - Ambérieu", - "equipments": [], - "text_color": "FFFFFF", - "trip_short_name": "886726", - "commercial_mode": "TER", - "description": "" - }, - "stop_point": { - "commercial_modes": [ - { - "id": "commercial_mode:ter", - "name": "TER" - } - ], - "name": "Crépieux-la-Pape", - "links": [], - "physical_modes": [ - { - "id": "physical_mode:LocalTrain", - "name": "Train régional / TER" - } - ], - "coord": { - "lat": "45.803921", - "lon": "4.892737" - }, - "label": "Crépieux-la-Pape (Rillieux-la-Pape)", - "equipments": [], - "administrative_regions": [ - { - "insee": "69286", - "name": "Rillieux-la-Pape", - "level": 8, - "coord": { - "lat": "45.823514", - "lon": "4.8994366" - }, - "label": "Rillieux-la-Pape (69140)", - "id": "admin:fr:69286", - "zip_code": "69140" - } - ], - "fare_zone": { - "name": "0" - }, - "id": "stop_point:OCE:SP:TrainTER-87723502", - "stop_area": { - "codes": [ - { - "type": "CR-CI-CH", - "value": "0087-723502-00" - }, - { - "type": "UIC8", - "value": "87723502" - }, - { - "type": "external_code", - "value": "OCE87723502" - } - ], - "name": "Crépieux-la-Pape", - "links": [], - "coord": { - "lat": "45.803921", - "lon": "4.892737" - }, - "label": "Crépieux-la-Pape (Rillieux-la-Pape)", - "administrative_regions": [ - { - "insee": "69286", - "name": "Rillieux-la-Pape", - "level": 8, - "coord": { - "lat": "45.823514", - "lon": "4.8994366" - }, - "label": "Rillieux-la-Pape (69140)", - "id": "admin:fr:69286", - "zip_code": "69140" - } - ], - "timezone": "Europe/Paris", - "id": "stop_area:OCE:SA:87723502" - } - }, - "route": { - "direction": { - "embedded_type": "stop_area", - "stop_area": { - "codes": [ - { - "type": "CR-CI-CH", - "value": "0087-726000-BV" - }, - { - "type": "UIC8", - "value": "87726000" - }, - { - "type": "external_code", - "value": "OCE87726000" - } - ], - "name": "St-Etienne-Châteaucreux", - "links": [], - "coord": { - "lat": "45.443382", - "lon": "4.399996" - }, - "label": "St-Etienne-Châteaucreux (Saint-Étienne)", - "timezone": "Europe/Paris", - "id": "stop_area:OCE:SA:87726000" - }, - "quality": 0, - "name": "St-Etienne-Châteaucreux (Saint-Étienne)", - "id": "stop_area:OCE:SA:87726000" - }, - "name": "Ambérieu-en-Bugey vers St-Etienne-Châteaucreux (Train TER)", - "links": [], - "physical_modes": [ - { - "id": "physical_mode:LocalTrain", - "name": "Train régional / TER" - } - ], - "is_frequence": "False", - "geojson": { - "type": "MultiLineString", - "coordinates": [] - }, - "direction_type": "backward", - "line": { - "code": "", - "name": "St-Etienne - Lyon - Ambérieu", - "links": [], - "color": "000000", - "geojson": { - "type": "MultiLineString", - "coordinates": [] - }, - "text_color": "FFFFFF", - "physical_modes": [ - { - "id": "physical_mode:LocalTrain", - "name": "Train régional / TER" - } - ], - "codes": [], - "closing_time": "221200", - "opening_time": "053500", - "commercial_mode": { - "id": "commercial_mode:ter", - "name": "TER" - }, - "id": "line:OCE:199" - }, - "id": "route:OCE:199-TrainTER-87743716-87726000" - }, - "links": [ - { - "type": "line", - "id": "line:OCE:199" - }, - { - "type": "vehicle_journey", - "id": "vehicle_journey:OCE:SN886726F35035_dst_1" - }, - { - "type": "route", - "id": "route:OCE:199-TrainTER-87743716-87726000" - }, - { - "type": "commercial_mode", - "id": "commercial_mode:ter" - }, - { - "type": "physical_mode", - "id": "physical_mode:LocalTrain" - }, - { - "type": "network", - "id": "network:sncf" - } - ], - "stop_date_time": { - "links": [], - "arrival_date_time": "20210218T134100", - "additional_informations": [], - "departure_date_time": "20210218T134100", - "base_arrival_date_time": "20210218T134100", - "base_departure_date_time": "20210218T134100", - "data_freshness": "base_schedule" - } - }, - { - "display_informations": { - "direction": "Ambérieu-en-Bugey (Ambérieu-en-Bugey)", - "code": "", - "network": "SNCF", - "links": [], - "color": "000000", - "name": "St-Etienne - Lyon - Ambérieu", - "physical_mode": "Train régional / TER", - "headsign": "886827", - "label": "St-Etienne - Lyon - Ambérieu", - "equipments": [], - "text_color": "FFFFFF", - "trip_short_name": "886827", - "commercial_mode": "TER", - "description": "" - }, - "stop_point": { - "commercial_modes": [ - { - "id": "commercial_mode:ter", - "name": "TER" - } - ], - "name": "Crépieux-la-Pape", - "links": [], - "physical_modes": [ - { - "id": "physical_mode:LocalTrain", - "name": "Train régional / TER" - } - ], - "coord": { - "lat": "45.803921", - "lon": "4.892737" - }, - "label": "Crépieux-la-Pape (Rillieux-la-Pape)", - "equipments": [], - "administrative_regions": [ - { - "insee": "69286", - "name": "Rillieux-la-Pape", - "level": 8, - "coord": { - "lat": "45.823514", - "lon": "4.8994366" - }, - "label": "Rillieux-la-Pape (69140)", - "id": "admin:fr:69286", - "zip_code": "69140" - } - ], - "fare_zone": { - "name": "0" - }, - "id": "stop_point:OCE:SP:TrainTER-87723502", - "stop_area": { - "codes": [ - { - "type": "CR-CI-CH", - "value": "0087-723502-00" - }, - { - "type": "UIC8", - "value": "87723502" - }, - { - "type": "external_code", - "value": "OCE87723502" - } - ], - "name": "Crépieux-la-Pape", - "links": [], - "coord": { - "lat": "45.803921", - "lon": "4.892737" - }, - "label": "Crépieux-la-Pape (Rillieux-la-Pape)", - "administrative_regions": [ - { - "insee": "69286", - "name": "Rillieux-la-Pape", - "level": 8, - "coord": { - "lat": "45.823514", - "lon": "4.8994366" - }, - "label": "Rillieux-la-Pape (69140)", - "id": "admin:fr:69286", - "zip_code": "69140" - } - ], - "timezone": "Europe/Paris", - "id": "stop_area:OCE:SA:87723502" - } - }, - "route": { - "direction": { - "embedded_type": "stop_area", - "stop_area": { - "codes": [ - { - "type": "CR-CI-CH", - "value": "0087-743716-BV" - }, - { - "type": "UIC8", - "value": "87743716" - }, - { - "type": "external_code", - "value": "OCE87743716" - } - ], - "name": "Ambérieu-en-Bugey", - "links": [], - "coord": { - "lat": "45.954008", - "lon": "5.342313" - }, - "label": "Ambérieu-en-Bugey (Ambérieu-en-Bugey)", - "timezone": "Europe/Paris", - "id": "stop_area:OCE:SA:87743716" - }, - "quality": 0, - "name": "Ambérieu-en-Bugey (Ambérieu-en-Bugey)", - "id": "stop_area:OCE:SA:87743716" - }, - "name": "St-Etienne-Châteaucreux vers Ambérieu-en-Bugey (Train TER)", - "links": [], - "physical_modes": [ - { - "id": "physical_mode:LocalTrain", - "name": "Train régional / TER" - } - ], - "is_frequence": "False", - "geojson": { - "type": "MultiLineString", - "coordinates": [] - }, - "direction_type": "forward", - "line": { - "code": "", - "name": "St-Etienne - Lyon - Ambérieu", - "links": [], - "color": "000000", - "geojson": { - "type": "MultiLineString", - "coordinates": [] - }, - "text_color": "FFFFFF", - "physical_modes": [ - { - "id": "physical_mode:LocalTrain", - "name": "Train régional / TER" - } - ], - "codes": [], - "closing_time": "221200", - "opening_time": "053500", - "commercial_mode": { - "id": "commercial_mode:ter", - "name": "TER" - }, - "id": "line:OCE:199" - }, - "id": "route:OCE:199-TrainTER-87726000-87743716" - }, - "links": [ - { - "type": "line", - "id": "line:OCE:199" - }, - { - "type": "vehicle_journey", - "id": "vehicle_journey:OCE:SN886827F22022_dst_1" - }, - { - "type": "route", - "id": "route:OCE:199-TrainTER-87726000-87743716" - }, - { - "type": "commercial_mode", - "id": "commercial_mode:ter" - }, - { - "type": "physical_mode", - "id": "physical_mode:LocalTrain" - }, - { - "type": "network", - "id": "network:sncf" - } - ], - "stop_date_time": { - "links": [], - "arrival_date_time": "20210218T141800", - "additional_informations": [], - "departure_date_time": "20210218T141800", - "base_arrival_date_time": "20210218T141800", - "base_departure_date_time": "20210218T141800", - "data_freshness": "base_schedule" - } - }, - { - "display_informations": { - "direction": "St-Etienne-Châteaucreux (Saint-Étienne)", - "code": "", - "network": "SNCF", - "links": [], - "color": "000000", - "name": "St-Etienne - Lyon - Ambérieu", - "physical_mode": "Train régional / TER", - "headsign": "886728", - "label": "St-Etienne - Lyon - Ambérieu", - "equipments": [], - "text_color": "FFFFFF", - "trip_short_name": "886728", - "commercial_mode": "TER", - "description": "" - }, - "stop_point": { - "commercial_modes": [ - { - "id": "commercial_mode:ter", - "name": "TER" - } - ], - "name": "Crépieux-la-Pape", - "links": [], - "physical_modes": [ - { - "id": "physical_mode:LocalTrain", - "name": "Train régional / TER" - } - ], - "coord": { - "lat": "45.803921", - "lon": "4.892737" - }, - "label": "Crépieux-la-Pape (Rillieux-la-Pape)", - "equipments": [], - "administrative_regions": [ - { - "insee": "69286", - "name": "Rillieux-la-Pape", - "level": 8, - "coord": { - "lat": "45.823514", - "lon": "4.8994366" - }, - "label": "Rillieux-la-Pape (69140)", - "id": "admin:fr:69286", - "zip_code": "69140" - } - ], - "fare_zone": { - "name": "0" - }, - "id": "stop_point:OCE:SP:TrainTER-87723502", - "stop_area": { - "codes": [ - { - "type": "CR-CI-CH", - "value": "0087-723502-00" - }, - { - "type": "UIC8", - "value": "87723502" - }, - { - "type": "external_code", - "value": "OCE87723502" - } - ], - "name": "Crépieux-la-Pape", - "links": [], - "coord": { - "lat": "45.803921", - "lon": "4.892737" - }, - "label": "Crépieux-la-Pape (Rillieux-la-Pape)", - "administrative_regions": [ - { - "insee": "69286", - "name": "Rillieux-la-Pape", - "level": 8, - "coord": { - "lat": "45.823514", - "lon": "4.8994366" - }, - "label": "Rillieux-la-Pape (69140)", - "id": "admin:fr:69286", - "zip_code": "69140" - } - ], - "timezone": "Europe/Paris", - "id": "stop_area:OCE:SA:87723502" - } - }, - "route": { - "direction": { - "embedded_type": "stop_area", - "stop_area": { - "codes": [ - { - "type": "CR-CI-CH", - "value": "0087-726000-BV" - }, - { - "type": "UIC8", - "value": "87726000" - }, - { - "type": "external_code", - "value": "OCE87726000" - } - ], - "name": "St-Etienne-Châteaucreux", - "links": [], - "coord": { - "lat": "45.443382", - "lon": "4.399996" - }, - "label": "St-Etienne-Châteaucreux (Saint-Étienne)", - "timezone": "Europe/Paris", - "id": "stop_area:OCE:SA:87726000" - }, - "quality": 0, - "name": "St-Etienne-Châteaucreux (Saint-Étienne)", - "id": "stop_area:OCE:SA:87726000" - }, - "name": "Ambérieu-en-Bugey vers St-Etienne-Châteaucreux (Train TER)", - "links": [], - "physical_modes": [ - { - "id": "physical_mode:LocalTrain", - "name": "Train régional / TER" - } - ], - "is_frequence": "False", - "geojson": { - "type": "MultiLineString", - "coordinates": [] - }, - "direction_type": "backward", - "line": { - "code": "", - "name": "St-Etienne - Lyon - Ambérieu", - "links": [], - "color": "000000", - "geojson": { - "type": "MultiLineString", - "coordinates": [] - }, - "text_color": "FFFFFF", - "physical_modes": [ - { - "id": "physical_mode:LocalTrain", - "name": "Train régional / TER" - } - ], - "codes": [], - "closing_time": "221200", - "opening_time": "053500", - "commercial_mode": { - "id": "commercial_mode:ter", - "name": "TER" - }, - "id": "line:OCE:199" - }, - "id": "route:OCE:199-TrainTER-87743716-87726000" - }, - "links": [ - { - "type": "line", - "id": "line:OCE:199" - }, - { - "type": "vehicle_journey", - "id": "vehicle_journey:OCE:SN886728F16016_dst_1" - }, - { - "type": "route", - "id": "route:OCE:199-TrainTER-87743716-87726000" - }, - { - "type": "commercial_mode", - "id": "commercial_mode:ter" - }, - { - "type": "physical_mode", - "id": "physical_mode:LocalTrain" - }, - { - "type": "network", - "id": "network:sncf" - } - ], - "stop_date_time": { - "links": [], - "arrival_date_time": "20210218T144100", - "additional_informations": [], - "departure_date_time": "20210218T144100", - "base_arrival_date_time": "20210218T144100", - "base_departure_date_time": "20210218T144100", - "data_freshness": "base_schedule" - } - }, - { - "display_informations": { - "direction": "Ambérieu-en-Bugey (Ambérieu-en-Bugey)", - "code": "", - "network": "SNCF", - "links": [], - "color": "000000", - "name": "St-Etienne - Lyon - Ambérieu", - "physical_mode": "Train régional / TER", - "headsign": "886871", - "label": "St-Etienne - Lyon - Ambérieu", - "equipments": [], - "text_color": "FFFFFF", - "trip_short_name": "886871", - "commercial_mode": "TER", - "description": "" - }, - "stop_point": { - "commercial_modes": [ - { - "id": "commercial_mode:ter", - "name": "TER" - } - ], - "name": "Crépieux-la-Pape", - "links": [], - "physical_modes": [ - { - "id": "physical_mode:LocalTrain", - "name": "Train régional / TER" - } - ], - "coord": { - "lat": "45.803921", - "lon": "4.892737" - }, - "label": "Crépieux-la-Pape (Rillieux-la-Pape)", - "equipments": [], - "administrative_regions": [ - { - "insee": "69286", - "name": "Rillieux-la-Pape", - "level": 8, - "coord": { - "lat": "45.823514", - "lon": "4.8994366" - }, - "label": "Rillieux-la-Pape (69140)", - "id": "admin:fr:69286", - "zip_code": "69140" - } - ], - "fare_zone": { - "name": "0" - }, - "id": "stop_point:OCE:SP:TrainTER-87723502", - "stop_area": { - "codes": [ - { - "type": "CR-CI-CH", - "value": "0087-723502-00" - }, - { - "type": "UIC8", - "value": "87723502" - }, - { - "type": "external_code", - "value": "OCE87723502" - } - ], - "name": "Crépieux-la-Pape", - "links": [], - "coord": { - "lat": "45.803921", - "lon": "4.892737" - }, - "label": "Crépieux-la-Pape (Rillieux-la-Pape)", - "administrative_regions": [ - { - "insee": "69286", - "name": "Rillieux-la-Pape", - "level": 8, - "coord": { - "lat": "45.823514", - "lon": "4.8994366" - }, - "label": "Rillieux-la-Pape (69140)", - "id": "admin:fr:69286", - "zip_code": "69140" - } - ], - "timezone": "Europe/Paris", - "id": "stop_area:OCE:SA:87723502" - } - }, - "route": { - "direction": { - "embedded_type": "stop_area", - "stop_area": { - "codes": [ - { - "type": "CR-CI-CH", - "value": "0087-743716-BV" - }, - { - "type": "UIC8", - "value": "87743716" - }, - { - "type": "external_code", - "value": "OCE87743716" - } - ], - "name": "Ambérieu-en-Bugey", - "links": [], - "coord": { - "lat": "45.954008", - "lon": "5.342313" - }, - "label": "Ambérieu-en-Bugey (Ambérieu-en-Bugey)", - "timezone": "Europe/Paris", - "id": "stop_area:OCE:SA:87743716" - }, - "quality": 0, - "name": "Ambérieu-en-Bugey (Ambérieu-en-Bugey)", - "id": "stop_area:OCE:SA:87743716" - }, - "name": "St-Etienne-Châteaucreux vers Ambérieu-en-Bugey (Train TER)", - "links": [], - "physical_modes": [ - { - "id": "physical_mode:LocalTrain", - "name": "Train régional / TER" - } - ], - "is_frequence": "False", - "geojson": { - "type": "MultiLineString", - "coordinates": [] - }, - "direction_type": "forward", - "line": { - "code": "", - "name": "St-Etienne - Lyon - Ambérieu", - "links": [], - "color": "000000", - "geojson": { - "type": "MultiLineString", - "coordinates": [] - }, - "text_color": "FFFFFF", - "physical_modes": [ - { - "id": "physical_mode:LocalTrain", - "name": "Train régional / TER" - } - ], - "codes": [], - "closing_time": "221200", - "opening_time": "053500", - "commercial_mode": { - "id": "commercial_mode:ter", - "name": "TER" - }, - "id": "line:OCE:199" - }, - "id": "route:OCE:199-TrainTER-87726000-87743716" - }, - "links": [ - { - "type": "line", - "id": "line:OCE:199" - }, - { - "type": "vehicle_journey", - "id": "vehicle_journey:OCE:SN886871F27027_dst_1" - }, - { - "type": "route", - "id": "route:OCE:199-TrainTER-87726000-87743716" - }, - { - "type": "commercial_mode", - "id": "commercial_mode:ter" - }, - { - "type": "physical_mode", - "id": "physical_mode:LocalTrain" - }, - { - "type": "network", - "id": "network:sncf" - } - ], - "stop_date_time": { - "links": [], - "arrival_date_time": "20210218T151800", - "additional_informations": [], - "departure_date_time": "20210218T151800", - "base_arrival_date_time": "20210218T151800", - "base_departure_date_time": "20210218T151800", - "data_freshness": "base_schedule" - } - }, - { - "display_informations": { - "direction": "Ambérieu-en-Bugey (Ambérieu-en-Bugey)", - "code": "", - "network": "SNCF", - "links": [], - "color": "000000", - "name": "St-Etienne - Lyon - Ambérieu", - "physical_mode": "Train régional / TER", - "headsign": "886837", - "label": "St-Etienne - Lyon - Ambérieu", - "equipments": [], - "text_color": "FFFFFF", - "trip_short_name": "886837", - "commercial_mode": "TER", - "description": "" - }, - "stop_point": { - "commercial_modes": [ - { - "id": "commercial_mode:ter", - "name": "TER" - } - ], - "name": "Crépieux-la-Pape", - "links": [], - "physical_modes": [ - { - "id": "physical_mode:LocalTrain", - "name": "Train régional / TER" - } - ], - "coord": { - "lat": "45.803921", - "lon": "4.892737" - }, - "label": "Crépieux-la-Pape (Rillieux-la-Pape)", - "equipments": [], - "administrative_regions": [ - { - "insee": "69286", - "name": "Rillieux-la-Pape", - "level": 8, - "coord": { - "lat": "45.823514", - "lon": "4.8994366" - }, - "label": "Rillieux-la-Pape (69140)", - "id": "admin:fr:69286", - "zip_code": "69140" - } - ], - "fare_zone": { - "name": "0" - }, - "id": "stop_point:OCE:SP:TrainTER-87723502", - "stop_area": { - "codes": [ - { - "type": "CR-CI-CH", - "value": "0087-723502-00" - }, - { - "type": "UIC8", - "value": "87723502" - }, - { - "type": "external_code", - "value": "OCE87723502" - } - ], - "name": "Crépieux-la-Pape", - "links": [], - "coord": { - "lat": "45.803921", - "lon": "4.892737" - }, - "label": "Crépieux-la-Pape (Rillieux-la-Pape)", - "administrative_regions": [ - { - "insee": "69286", - "name": "Rillieux-la-Pape", - "level": 8, - "coord": { - "lat": "45.823514", - "lon": "4.8994366" - }, - "label": "Rillieux-la-Pape (69140)", - "id": "admin:fr:69286", - "zip_code": "69140" - } - ], - "timezone": "Europe/Paris", - "id": "stop_area:OCE:SA:87723502" - } - }, - "route": { - "direction": { - "embedded_type": "stop_area", - "stop_area": { - "codes": [ - { - "type": "CR-CI-CH", - "value": "0087-743716-BV" - }, - { - "type": "UIC8", - "value": "87743716" - }, - { - "type": "external_code", - "value": "OCE87743716" - } - ], - "name": "Ambérieu-en-Bugey", - "links": [], - "coord": { - "lat": "45.954008", - "lon": "5.342313" - }, - "label": "Ambérieu-en-Bugey (Ambérieu-en-Bugey)", - "timezone": "Europe/Paris", - "id": "stop_area:OCE:SA:87743716" - }, - "quality": 0, - "name": "Ambérieu-en-Bugey (Ambérieu-en-Bugey)", - "id": "stop_area:OCE:SA:87743716" - }, - "name": "St-Etienne-Châteaucreux vers Ambérieu-en-Bugey (Train TER)", - "links": [], - "physical_modes": [ - { - "id": "physical_mode:LocalTrain", - "name": "Train régional / TER" - } - ], - "is_frequence": "False", - "geojson": { - "type": "MultiLineString", - "coordinates": [] - }, - "direction_type": "forward", - "line": { - "code": "", - "name": "St-Etienne - Lyon - Ambérieu", - "links": [], - "color": "000000", - "geojson": { - "type": "MultiLineString", - "coordinates": [] - }, - "text_color": "FFFFFF", - "physical_modes": [ - { - "id": "physical_mode:LocalTrain", - "name": "Train régional / TER" - } - ], - "codes": [], - "closing_time": "221200", - "opening_time": "053500", - "commercial_mode": { - "id": "commercial_mode:ter", - "name": "TER" - }, - "id": "line:OCE:199" - }, - "id": "route:OCE:199-TrainTER-87726000-87743716" - }, - "links": [ - { - "type": "line", - "id": "line:OCE:199" - }, - { - "type": "vehicle_journey", - "id": "vehicle_journey:OCE:SN886837F18018_dst_1" - }, - { - "type": "route", - "id": "route:OCE:199-TrainTER-87726000-87743716" - }, - { - "type": "commercial_mode", - "id": "commercial_mode:ter" - }, - { - "type": "physical_mode", - "id": "physical_mode:LocalTrain" - }, - { - "type": "network", - "id": "network:sncf" - } - ], - "stop_date_time": { - "links": [], - "arrival_date_time": "20210218T161800", - "additional_informations": [], - "departure_date_time": "20210218T161800", - "base_arrival_date_time": "20210218T161800", - "base_departure_date_time": "20210218T161800", - "data_freshness": "base_schedule" - } - }, - { - "display_informations": { - "direction": "St-Etienne-Châteaucreux (Saint-Étienne)", - "code": "", - "network": "SNCF", - "links": [], - "color": "000000", - "name": "St-Etienne - Lyon - Ambérieu", - "physical_mode": "Train régional / TER", - "headsign": "886734", - "label": "St-Etienne - Lyon - Ambérieu", - "equipments": [], - "text_color": "FFFFFF", - "trip_short_name": "886734", - "commercial_mode": "TER", - "description": "" - }, - "stop_point": { - "commercial_modes": [ - { - "id": "commercial_mode:ter", - "name": "TER" - } - ], - "name": "Crépieux-la-Pape", - "links": [], - "physical_modes": [ - { - "id": "physical_mode:LocalTrain", - "name": "Train régional / TER" - } - ], - "coord": { - "lat": "45.803921", - "lon": "4.892737" - }, - "label": "Crépieux-la-Pape (Rillieux-la-Pape)", - "equipments": [], - "administrative_regions": [ - { - "insee": "69286", - "name": "Rillieux-la-Pape", - "level": 8, - "coord": { - "lat": "45.823514", - "lon": "4.8994366" - }, - "label": "Rillieux-la-Pape (69140)", - "id": "admin:fr:69286", - "zip_code": "69140" - } - ], - "fare_zone": { - "name": "0" - }, - "id": "stop_point:OCE:SP:TrainTER-87723502", - "stop_area": { - "codes": [ - { - "type": "CR-CI-CH", - "value": "0087-723502-00" - }, - { - "type": "UIC8", - "value": "87723502" - }, - { - "type": "external_code", - "value": "OCE87723502" - } - ], - "name": "Crépieux-la-Pape", - "links": [], - "coord": { - "lat": "45.803921", - "lon": "4.892737" - }, - "label": "Crépieux-la-Pape (Rillieux-la-Pape)", - "administrative_regions": [ - { - "insee": "69286", - "name": "Rillieux-la-Pape", - "level": 8, - "coord": { - "lat": "45.823514", - "lon": "4.8994366" - }, - "label": "Rillieux-la-Pape (69140)", - "id": "admin:fr:69286", - "zip_code": "69140" - } - ], - "timezone": "Europe/Paris", - "id": "stop_area:OCE:SA:87723502" - } - }, - "route": { - "direction": { - "embedded_type": "stop_area", - "stop_area": { - "codes": [ - { - "type": "CR-CI-CH", - "value": "0087-726000-BV" - }, - { - "type": "UIC8", - "value": "87726000" - }, - { - "type": "external_code", - "value": "OCE87726000" - } - ], - "name": "St-Etienne-Châteaucreux", - "links": [], - "coord": { - "lat": "45.443382", - "lon": "4.399996" - }, - "label": "St-Etienne-Châteaucreux (Saint-Étienne)", - "timezone": "Europe/Paris", - "id": "stop_area:OCE:SA:87726000" - }, - "quality": 0, - "name": "St-Etienne-Châteaucreux (Saint-Étienne)", - "id": "stop_area:OCE:SA:87726000" - }, - "name": "Ambérieu-en-Bugey vers St-Etienne-Châteaucreux (Train TER)", - "links": [], - "physical_modes": [ - { - "id": "physical_mode:LocalTrain", - "name": "Train régional / TER" - } - ], - "is_frequence": "False", - "geojson": { - "type": "MultiLineString", - "coordinates": [] - }, - "direction_type": "backward", - "line": { - "code": "", - "name": "St-Etienne - Lyon - Ambérieu", - "links": [], - "color": "000000", - "geojson": { - "type": "MultiLineString", - "coordinates": [] - }, - "text_color": "FFFFFF", - "physical_modes": [ - { - "id": "physical_mode:LocalTrain", - "name": "Train régional / TER" - } - ], - "codes": [], - "closing_time": "221200", - "opening_time": "053500", - "commercial_mode": { - "id": "commercial_mode:ter", - "name": "TER" - }, - "id": "line:OCE:199" - }, - "id": "route:OCE:199-TrainTER-87743716-87726000" - }, - "links": [ - { - "type": "line", - "id": "line:OCE:199" - }, - { - "type": "vehicle_journey", - "id": "vehicle_journey:OCE:SN886734F19019_dst_1" - }, - { - "type": "route", - "id": "route:OCE:199-TrainTER-87743716-87726000" - }, - { - "type": "commercial_mode", - "id": "commercial_mode:ter" - }, - { - "type": "physical_mode", - "id": "physical_mode:LocalTrain" - }, - { - "type": "network", - "id": "network:sncf" - } - ], - "stop_date_time": { - "links": [], - "arrival_date_time": "20210218T164100", - "additional_informations": [], - "departure_date_time": "20210218T164100", - "base_arrival_date_time": "20210218T164100", - "base_departure_date_time": "20210218T164100", - "data_freshness": "base_schedule" - } - }, - { - "display_informations": { - "direction": "Ambérieu-en-Bugey (Ambérieu-en-Bugey)", - "code": "", - "network": "SNCF", - "links": [], - "color": "000000", - "name": "St-Etienne - Lyon - Ambérieu", - "physical_mode": "Train régional / TER", - "headsign": "886839", - "label": "St-Etienne - Lyon - Ambérieu", - "equipments": [], - "text_color": "FFFFFF", - "trip_short_name": "886839", - "commercial_mode": "TER", - "description": "" - }, - "stop_point": { - "commercial_modes": [ - { - "id": "commercial_mode:ter", - "name": "TER" - } - ], - "name": "Crépieux-la-Pape", - "links": [], - "physical_modes": [ - { - "id": "physical_mode:LocalTrain", - "name": "Train régional / TER" - } - ], - "coord": { - "lat": "45.803921", - "lon": "4.892737" - }, - "label": "Crépieux-la-Pape (Rillieux-la-Pape)", - "equipments": [], - "administrative_regions": [ - { - "insee": "69286", - "name": "Rillieux-la-Pape", - "level": 8, - "coord": { - "lat": "45.823514", - "lon": "4.8994366" - }, - "label": "Rillieux-la-Pape (69140)", - "id": "admin:fr:69286", - "zip_code": "69140" - } - ], - "fare_zone": { - "name": "0" - }, - "id": "stop_point:OCE:SP:TrainTER-87723502", - "stop_area": { - "codes": [ - { - "type": "CR-CI-CH", - "value": "0087-723502-00" - }, - { - "type": "UIC8", - "value": "87723502" - }, - { - "type": "external_code", - "value": "OCE87723502" - } - ], - "name": "Crépieux-la-Pape", - "links": [], - "coord": { - "lat": "45.803921", - "lon": "4.892737" - }, - "label": "Crépieux-la-Pape (Rillieux-la-Pape)", - "administrative_regions": [ - { - "insee": "69286", - "name": "Rillieux-la-Pape", - "level": 8, - "coord": { - "lat": "45.823514", - "lon": "4.8994366" - }, - "label": "Rillieux-la-Pape (69140)", - "id": "admin:fr:69286", - "zip_code": "69140" - } - ], - "timezone": "Europe/Paris", - "id": "stop_area:OCE:SA:87723502" - } - }, - "route": { - "direction": { - "embedded_type": "stop_area", - "stop_area": { - "codes": [ - { - "type": "CR-CI-CH", - "value": "0087-743716-BV" - }, - { - "type": "UIC8", - "value": "87743716" - }, - { - "type": "external_code", - "value": "OCE87743716" - } - ], - "name": "Ambérieu-en-Bugey", - "links": [], - "coord": { - "lat": "45.954008", - "lon": "5.342313" - }, - "label": "Ambérieu-en-Bugey (Ambérieu-en-Bugey)", - "timezone": "Europe/Paris", - "id": "stop_area:OCE:SA:87743716" - }, - "quality": 0, - "name": "Ambérieu-en-Bugey (Ambérieu-en-Bugey)", - "id": "stop_area:OCE:SA:87743716" - }, - "name": "St-Etienne-Châteaucreux vers Ambérieu-en-Bugey (Train TER)", - "links": [], - "physical_modes": [ - { - "id": "physical_mode:LocalTrain", - "name": "Train régional / TER" - } - ], - "is_frequence": "False", - "geojson": { - "type": "MultiLineString", - "coordinates": [] - }, - "direction_type": "forward", - "line": { - "code": "", - "name": "St-Etienne - Lyon - Ambérieu", - "links": [], - "color": "000000", - "geojson": { - "type": "MultiLineString", - "coordinates": [] - }, - "text_color": "FFFFFF", - "physical_modes": [ - { - "id": "physical_mode:LocalTrain", - "name": "Train régional / TER" - } - ], - "codes": [], - "closing_time": "221200", - "opening_time": "053500", - "commercial_mode": { - "id": "commercial_mode:ter", - "name": "TER" - }, - "id": "line:OCE:199" - }, - "id": "route:OCE:199-TrainTER-87726000-87743716" - }, - "links": [ - { - "type": "line", - "id": "line:OCE:199" - }, - { - "type": "vehicle_journey", - "id": "vehicle_journey:OCE:SN886839F10010_dst_1" - }, - { - "type": "route", - "id": "route:OCE:199-TrainTER-87726000-87743716" - }, - { - "type": "commercial_mode", - "id": "commercial_mode:ter" - }, - { - "type": "physical_mode", - "id": "physical_mode:LocalTrain" - }, - { - "type": "network", - "id": "network:sncf" - } - ], - "stop_date_time": { - "links": [], - "arrival_date_time": "20210218T164800", - "additional_informations": [], - "departure_date_time": "20210218T164800", - "base_arrival_date_time": "20210218T164800", - "base_departure_date_time": "20210218T164800", - "data_freshness": "base_schedule" - } - }, - { - "display_informations": { - "direction": "Ambérieu-en-Bugey (Ambérieu-en-Bugey)", - "code": "", - "network": "SNCF", - "links": [], - "color": "000000", - "name": "St-Etienne - Lyon - Ambérieu", - "physical_mode": "Train régional / TER", - "headsign": "886843", - "label": "St-Etienne - Lyon - Ambérieu", - "equipments": [], - "text_color": "FFFFFF", - "trip_short_name": "886843", - "commercial_mode": "TER", - "description": "" - }, - "stop_point": { - "commercial_modes": [ - { - "id": "commercial_mode:ter", - "name": "TER" - } - ], - "name": "Crépieux-la-Pape", - "links": [], - "physical_modes": [ - { - "id": "physical_mode:LocalTrain", - "name": "Train régional / TER" - } - ], - "coord": { - "lat": "45.803921", - "lon": "4.892737" - }, - "label": "Crépieux-la-Pape (Rillieux-la-Pape)", - "equipments": [], - "administrative_regions": [ - { - "insee": "69286", - "name": "Rillieux-la-Pape", - "level": 8, - "coord": { - "lat": "45.823514", - "lon": "4.8994366" - }, - "label": "Rillieux-la-Pape (69140)", - "id": "admin:fr:69286", - "zip_code": "69140" - } - ], - "fare_zone": { - "name": "0" - }, - "id": "stop_point:OCE:SP:TrainTER-87723502", - "stop_area": { - "codes": [ - { - "type": "CR-CI-CH", - "value": "0087-723502-00" - }, - { - "type": "UIC8", - "value": "87723502" - }, - { - "type": "external_code", - "value": "OCE87723502" - } - ], - "name": "Crépieux-la-Pape", - "links": [], - "coord": { - "lat": "45.803921", - "lon": "4.892737" - }, - "label": "Crépieux-la-Pape (Rillieux-la-Pape)", - "administrative_regions": [ - { - "insee": "69286", - "name": "Rillieux-la-Pape", - "level": 8, - "coord": { - "lat": "45.823514", - "lon": "4.8994366" - }, - "label": "Rillieux-la-Pape (69140)", - "id": "admin:fr:69286", - "zip_code": "69140" - } - ], - "timezone": "Europe/Paris", - "id": "stop_area:OCE:SA:87723502" - } - }, - "route": { - "direction": { - "embedded_type": "stop_area", - "stop_area": { - "codes": [ - { - "type": "CR-CI-CH", - "value": "0087-743716-BV" - }, - { - "type": "UIC8", - "value": "87743716" - }, - { - "type": "external_code", - "value": "OCE87743716" - } - ], - "name": "Ambérieu-en-Bugey", - "links": [], - "coord": { - "lat": "45.954008", - "lon": "5.342313" - }, - "label": "Ambérieu-en-Bugey (Ambérieu-en-Bugey)", - "timezone": "Europe/Paris", - "id": "stop_area:OCE:SA:87743716" - }, - "quality": 0, - "name": "Ambérieu-en-Bugey (Ambérieu-en-Bugey)", - "id": "stop_area:OCE:SA:87743716" - }, - "name": "St-Etienne-Châteaucreux vers Ambérieu-en-Bugey (Train TER)", - "links": [], - "physical_modes": [ - { - "id": "physical_mode:LocalTrain", - "name": "Train régional / TER" - } - ], - "is_frequence": "False", - "geojson": { - "type": "MultiLineString", - "coordinates": [] - }, - "direction_type": "forward", - "line": { - "code": "", - "name": "St-Etienne - Lyon - Ambérieu", - "links": [], - "color": "000000", - "geojson": { - "type": "MultiLineString", - "coordinates": [] - }, - "text_color": "FFFFFF", - "physical_modes": [ - { - "id": "physical_mode:LocalTrain", - "name": "Train régional / TER" - } - ], - "codes": [], - "closing_time": "221200", - "opening_time": "053500", - "commercial_mode": { - "id": "commercial_mode:ter", - "name": "TER" - }, - "id": "line:OCE:199" - }, - "id": "route:OCE:199-TrainTER-87726000-87743716" - }, - "links": [ - { - "type": "line", - "id": "line:OCE:199" - }, - { - "type": "vehicle_journey", - "id": "vehicle_journey:OCE:SN886843F24024_dst_1" - }, - { - "type": "route", - "id": "route:OCE:199-TrainTER-87726000-87743716" - }, - { - "type": "commercial_mode", - "id": "commercial_mode:ter" - }, - { - "type": "physical_mode", - "id": "physical_mode:LocalTrain" - }, - { - "type": "network", - "id": "network:sncf" - } - ], - "stop_date_time": { - "links": [], - "arrival_date_time": "20210218T171800", - "additional_informations": [], - "departure_date_time": "20210218T171800", - "base_arrival_date_time": "20210218T171800", - "base_departure_date_time": "20210218T171800", - "data_freshness": "base_schedule" - } - }, - { - "display_informations": { - "direction": "St-Etienne-Châteaucreux (Saint-Étienne)", - "code": "", - "network": "SNCF", - "links": [], - "color": "000000", - "name": "St-Etienne - Lyon - Ambérieu", - "physical_mode": "Train régional / TER", - "headsign": "886738", - "label": "St-Etienne - Lyon - Ambérieu", - "equipments": [], - "text_color": "FFFFFF", - "trip_short_name": "886738", - "commercial_mode": "TER", - "description": "" - }, - "stop_point": { - "commercial_modes": [ - { - "id": "commercial_mode:ter", - "name": "TER" - } - ], - "name": "Crépieux-la-Pape", - "links": [], - "physical_modes": [ - { - "id": "physical_mode:LocalTrain", - "name": "Train régional / TER" - } - ], - "coord": { - "lat": "45.803921", - "lon": "4.892737" - }, - "label": "Crépieux-la-Pape (Rillieux-la-Pape)", - "equipments": [], - "administrative_regions": [ - { - "insee": "69286", - "name": "Rillieux-la-Pape", - "level": 8, - "coord": { - "lat": "45.823514", - "lon": "4.8994366" - }, - "label": "Rillieux-la-Pape (69140)", - "id": "admin:fr:69286", - "zip_code": "69140" - } - ], - "fare_zone": { - "name": "0" - }, - "id": "stop_point:OCE:SP:TrainTER-87723502", - "stop_area": { - "codes": [ - { - "type": "CR-CI-CH", - "value": "0087-723502-00" - }, - { - "type": "UIC8", - "value": "87723502" - }, - { - "type": "external_code", - "value": "OCE87723502" - } - ], - "name": "Crépieux-la-Pape", - "links": [], - "coord": { - "lat": "45.803921", - "lon": "4.892737" - }, - "label": "Crépieux-la-Pape (Rillieux-la-Pape)", - "administrative_regions": [ - { - "insee": "69286", - "name": "Rillieux-la-Pape", - "level": 8, - "coord": { - "lat": "45.823514", - "lon": "4.8994366" - }, - "label": "Rillieux-la-Pape (69140)", - "id": "admin:fr:69286", - "zip_code": "69140" - } - ], - "timezone": "Europe/Paris", - "id": "stop_area:OCE:SA:87723502" - } - }, - "route": { - "direction": { - "embedded_type": "stop_area", - "stop_area": { - "codes": [ - { - "type": "CR-CI-CH", - "value": "0087-726000-BV" - }, - { - "type": "UIC8", - "value": "87726000" - }, - { - "type": "external_code", - "value": "OCE87726000" - } - ], - "name": "St-Etienne-Châteaucreux", - "links": [], - "coord": { - "lat": "45.443382", - "lon": "4.399996" - }, - "label": "St-Etienne-Châteaucreux (Saint-Étienne)", - "timezone": "Europe/Paris", - "id": "stop_area:OCE:SA:87726000" - }, - "quality": 0, - "name": "St-Etienne-Châteaucreux (Saint-Étienne)", - "id": "stop_area:OCE:SA:87726000" - }, - "name": "Ambérieu-en-Bugey vers St-Etienne-Châteaucreux (Train TER)", - "links": [], - "physical_modes": [ - { - "id": "physical_mode:LocalTrain", - "name": "Train régional / TER" - } - ], - "is_frequence": "False", - "geojson": { - "type": "MultiLineString", - "coordinates": [] - }, - "direction_type": "backward", - "line": { - "code": "", - "name": "St-Etienne - Lyon - Ambérieu", - "links": [], - "color": "000000", - "geojson": { - "type": "MultiLineString", - "coordinates": [] - }, - "text_color": "FFFFFF", - "physical_modes": [ - { - "id": "physical_mode:LocalTrain", - "name": "Train régional / TER" - } - ], - "codes": [], - "closing_time": "221200", - "opening_time": "053500", - "commercial_mode": { - "id": "commercial_mode:ter", - "name": "TER" - }, - "id": "line:OCE:199" - }, - "id": "route:OCE:199-TrainTER-87743716-87726000" - }, - "links": [ - { - "type": "line", - "id": "line:OCE:199" - }, - { - "type": "vehicle_journey", - "id": "vehicle_journey:OCE:SN886738F27027_dst_1" - }, - { - "type": "route", - "id": "route:OCE:199-TrainTER-87743716-87726000" - }, - { - "type": "commercial_mode", - "id": "commercial_mode:ter" - }, - { - "type": "physical_mode", - "id": "physical_mode:LocalTrain" - }, - { - "type": "network", - "id": "network:sncf" - } - ], - "stop_date_time": { - "links": [], - "arrival_date_time": "20210218T174100", - "additional_informations": [], - "departure_date_time": "20210218T174100", - "base_arrival_date_time": "20210218T174100", - "base_departure_date_time": "20210218T174100", - "data_freshness": "base_schedule" - } - } - ], - "context": { - "timezone": "Europe/Paris", - "current_datetime": "20210218T125549" - }, - "exceptions": [] -} diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000..f97467a --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,55 @@ +package config + +import ( + "net" + "os" + "regexp" + + "gopkg.in/yaml.v3" +) + +var validToken = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`) + +type Config struct { + // Address is the hostname or ip the web server will listen to + Address string `yaml:"address",default:"127.0.0.1"` + Port string `yaml:"port",default:"8080"` + // Token is the sncf api token + Token string `yaml:"token"` +} + +func (c *Config) validate() error { + // address + if ip := net.ParseIP(c.Address); ip == nil { + if _, err := net.LookupIP(c.Address); err != nil { + return newInvalidAddressError(c.Address, err) + } + } + // port + if _, err := net.LookupPort("tcp", c.Port); err != nil { + return newInvalidPortError(c.Port, err) + } + // token + if ok := validToken.MatchString(c.Token); !ok { + return newInvalidTokenError(c.Token) + } + return nil +} + +// LoadFile loads the c from a given file +func LoadFile(path string) (*Config, error) { + var c *Config + f, errOpen := os.Open(path) + if errOpen != nil { + return nil, newOpenError(path, errOpen) + } + defer f.Close() + decoder := yaml.NewDecoder(f) + if err := decoder.Decode(&c); err != nil { + return nil, newDecodeError(path, err) + } + if err := c.validate(); err != nil { + return nil, err + } + return c, nil +} diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go new file mode 100644 index 0000000..3904b5d --- /dev/null +++ b/pkg/config/config_test.go @@ -0,0 +1,54 @@ +package config + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLoadFile(t *testing.T) { + // Minimal yaml file + minimalConfig := Config{ + Address: "127.0.0.2", + Port: "8082", + Token: "12345678-9abc-def0-1234-56789abcdef0", + } + + // Minimal yaml file with hostname resolving + minimalConfigWithResolving := Config{ + Address: "localhost", + Port: "8082", + Token: "12345678-9abc-def0-1234-56789abcdef0", + } + + // Test cases + testCases := []struct { + name string + input string + expected *Config + expectedError interface{} + }{ + {"Non existant file", "test_data/non-existant", nil, &OpenError{}}, + {"Invalid file content", "test_data/invalid.yaml", nil, &DecodeError{}}, + {"Invalid address should fail to load", "test_data/invalid_address.yaml", nil, &InvalidAddressError{}}, + {"Unresolvable address should fail to load", "test_data/invalid_address_unresolvable.yaml", nil, &InvalidAddressError{}}, + {"Invalid port should fail to load", "test_data/invalid_port.yaml", nil, &InvalidPortError{}}, + {"Invalid token should fail to load", "test_data/invalid_token.yaml", nil, &InvalidTokenError{}}, + {"Minimal config", "test_data/minimal.yaml", &minimalConfig, nil}, + {"Minimal config with resolving", "test_data/minimal_with_hostname.yaml", &minimalConfigWithResolving, nil}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + valid, err := LoadFile(tc.input) + if tc.expectedError != nil { + require.Error(t, err) + assert.Equalf(t, reflect.TypeOf(err), reflect.TypeOf(tc.expectedError), "Invalid error type. Got %s but expected %s", reflect.TypeOf(err), reflect.TypeOf(tc.expectedError)) + } else { + require.NoError(t, err) + } + assert.Equal(t, tc.expected, valid, "Invalid value") + }) + } +} diff --git a/pkg/config/error.go b/pkg/config/error.go new file mode 100644 index 0000000..c49b6a9 --- /dev/null +++ b/pkg/config/error.go @@ -0,0 +1,92 @@ +package config + +import "fmt" + +type ErrorType int + +// file open configuration file error +type OpenError struct { + path string + err error +} + +func (e *OpenError) Error() string { + return fmt.Sprintf("Failed to open configuration file : %s", e.path) +} +func (e *OpenError) Unwrap() error { return e.err } + +func newOpenError(path string, err error) error { + return &OpenError{ + path: path, + err: err, + } +} + +// Yaml configuration file decoding error +type DecodeError struct { + path string + err error +} + +func (e *DecodeError) Error() string { + return fmt.Sprintf("Failed to decode configuration file : %s", e.path) +} +func (e *DecodeError) Unwrap() error { return e.err } + +func newDecodeError(path string, err error) error { + return &DecodeError{ + path: path, + err: err, + } +} + +// Invalid address field error +type InvalidAddressError struct { + address string + err error +} + +func (e *InvalidAddressError) Error() string { + return fmt.Sprintf("Invalid address %s : it must be a valid ipv4 address, ipv6 address, or resolvable name", e.address) +} +func (e *InvalidAddressError) Unwrap() error { return e.err } + +func newInvalidAddressError(address string, err error) error { + return &InvalidAddressError{ + address: address, + err: err, + } +} + +// Invalid port field error +type InvalidPortError struct { + port string + err error +} + +func (e *InvalidPortError) Error() string { + return fmt.Sprintf("Invalid port %s : it must be a valid port number or tcp service name", e.port) +} +func (e *InvalidPortError) Unwrap() error { return e.err } + +func newInvalidPortError(port string, err error) error { + return &InvalidPortError{ + port: port, + err: err, + } +} + +// Invalid token field error +type InvalidTokenError struct { + token string +} + +func (e *InvalidTokenError) Error() string { + return fmt.Sprintf("Invalid token %s : it must be an hexadecimal string that lookslike 12345678-9abc-def0-1234-56789abcdef0", e.token) +} + +func newInvalidTokenError(token string) error { + return &InvalidTokenError{ + token: token, + } +} diff --git a/pkg/config/error_test.go b/pkg/config/error_test.go new file mode 100644 index 0000000..f9807c1 --- /dev/null +++ b/pkg/config/error_test.go @@ -0,0 +1,20 @@ +package config + +import "testing" + +func TestErrorsCoverage(t *testing.T) { + openErr := OpenError{} + _ = openErr.Error() + _ = openErr.Unwrap() + decodeErr := DecodeError{} + _ = decodeErr.Error() + _ = decodeErr.Unwrap() + invalidAddressErr := InvalidAddressError{} + _ = invalidAddressErr.Error() + _ = invalidAddressErr.Unwrap() + invalidPortErr := InvalidPortError{} + _ = invalidPortErr.Error() + _ = invalidPortErr.Unwrap() + invalidTokenErr := InvalidTokenError{} + _ = invalidTokenErr.Error() +} diff --git a/pkg/config/test_data/invalid.yaml b/pkg/config/test_data/invalid.yaml new file mode 100644 index 0000000..db1ddad --- /dev/null +++ b/pkg/config/test_data/invalid.yaml @@ -0,0 +1 @@ +blargh(ads) diff --git a/pkg/config/test_data/invalid_address.yaml b/pkg/config/test_data/invalid_address.yaml new file mode 100644 index 0000000..e3682b2 --- /dev/null +++ b/pkg/config/test_data/invalid_address.yaml @@ -0,0 +1,3 @@ +address: "0.0.0.0.0" +port: 8082 +token: 12345678-9abc-def0-1234-56789abcdef0 diff --git a/pkg/config/test_data/invalid_address_unresolvable.yaml b/pkg/config/test_data/invalid_address_unresolvable.yaml new file mode 100644 index 0000000..d476093 --- /dev/null +++ b/pkg/config/test_data/invalid_address_unresolvable.yaml @@ -0,0 +1,3 @@ +address: "invalid" +port: 8082 +token: 12345678-9abc-def0-1234-56789abcdef0 diff --git a/pkg/config/test_data/invalid_port.yaml b/pkg/config/test_data/invalid_port.yaml new file mode 100644 index 0000000..2790b3f --- /dev/null +++ b/pkg/config/test_data/invalid_port.yaml @@ -0,0 +1,3 @@ +address: localhost +port: invalid +token: 12345678-9abc-def0-1234-56789abcdef0 diff --git a/pkg/config/test_data/invalid_token.yaml b/pkg/config/test_data/invalid_token.yaml new file mode 100644 index 0000000..6f9e297 --- /dev/null +++ b/pkg/config/test_data/invalid_token.yaml @@ -0,0 +1,3 @@ +address: 127.0.0.1 +port: 8080 +token: invalid diff --git a/pkg/config/test_data/minimal.yaml b/pkg/config/test_data/minimal.yaml new file mode 100644 index 0000000..2944645 --- /dev/null +++ b/pkg/config/test_data/minimal.yaml @@ -0,0 +1,3 @@ +address: 127.0.0.2 +port: 8082 +token: 12345678-9abc-def0-1234-56789abcdef0 diff --git a/pkg/config/test_data/minimal_with_hostname.yaml b/pkg/config/test_data/minimal_with_hostname.yaml new file mode 100644 index 0000000..716b2d5 --- /dev/null +++ b/pkg/config/test_data/minimal_with_hostname.yaml @@ -0,0 +1,3 @@ +address: localhost +port: 8082 +token: 12345678-9abc-def0-1234-56789abcdef0 diff --git a/pkg/navitia_api_client/client.go b/pkg/navitia_api_client/client.go new file mode 100644 index 0000000..aef8d0e --- /dev/null +++ b/pkg/navitia_api_client/client.go @@ -0,0 +1,31 @@ +package navitia_api_client + +import ( + "fmt" + "net/http" + "sync" + "time" +) + +type Client struct { + baseURL string + httpClient *http.Client + + mutex sync.Mutex + cache map[string]cachedResult +} + +type cachedResult struct { + ts time.Time + result interface{} +} + +func NewClient(token string) *Client { + return &Client{ + baseURL: fmt.Sprintf("https://%s@api.sncf.com/v1", token), + httpClient: &http.Client{ + Timeout: time.Minute, + }, + cache: make(map[string]cachedResult), + } +} diff --git a/pkg/navitia_api_client/client_test.go b/pkg/navitia_api_client/client_test.go new file mode 100644 index 0000000..332b222 --- /dev/null +++ b/pkg/navitia_api_client/client_test.go @@ -0,0 +1,38 @@ +package navitia_api_client + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" +) + +// package utilities +func NewTestClient(ts *httptest.Server) *Client { + return &Client{ + baseURL: fmt.Sprintf(ts.URL), + httpClient: ts.Client(), + cache: make(map[string]cachedResult), + } +} + +func NewTestClientFromFilename(t *testing.T, filename string) (*Client, *httptest.Server) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + page, err := ioutil.ReadFile(filename) + if err != nil { + t.Fatalf("Could not open test file : %s", err) + } + w.Write(page) + })) + return NewTestClient(ts), ts +} + +// tests +func TestNewClient(t *testing.T) { + client := NewClient("test") + want := "https://test@api.sncf.com/v1" + if client.baseURL != want { + t.Fatal("Invalid new client") + } +} diff --git a/pkg/navitia_api_client/departures.go b/pkg/navitia_api_client/departures.go new file mode 100644 index 0000000..7738940 --- /dev/null +++ b/pkg/navitia_api_client/departures.go @@ -0,0 +1,73 @@ +package navitia_api_client + +import ( + "encoding/json" + "fmt" + "net/http" + "time" +) + +type DeparturesResponse struct { + Disruptions []interface{} `json:"disruptions"` + Notes []interface{} `json:"notes"` + Departures []struct { + DisplayInformations struct { + Direction string `json:"direction"` + Code string `json:"code"` + Network string `json:"network"` + Links []interface{} `json:"links"` + Color string `json:"color"` + Name string `json:"name"` + PhysicalMode string `json:"physical_mode"` + Headsign string `json:"headsign"` + Label string `json:"label"` + Equipments []interface{} `json:"equipments"` + TextColor string `json:"text_color"` + TripShortName string `json:"trip_short_name"` + CommercialMode string `json:"commercial_mode"` + Description string `json:"description"` + } `json:"display_informations"` + StopDateTime struct { + Links []interface{} `json:"links"` + ArrivalDateTime string `json:"arrival_date_time"` + AdditionalInformations []interface{} `json:"additional_informations"` + DepartureDateTime string `json:"departure_date_time"` + BaseArrivalDateTime string `json:"base_arrival_date_time"` + BaseDepartureDateTime string `json:"base_departure_date_time"` + DataFreshness string `json:"data_freshness"` + } `json:"stop_date_time"` + } `json:"departures"` + Context struct { + Timezone string `json:"timezone"` + CurrentDatetime string `json:"current_datetime"` + } `json:"context"` +} + +func (c *Client) GetDepartures() (departures *DeparturesResponse, err error) { + request := fmt.Sprintf("%s/coverage/sncf/stop_areas/stop_area:SNCF:87723502/departures", c.baseURL) + start := time.Now() + c.mutex.Lock() + defer c.mutex.Unlock() + if cachedResult, ok := c.cache[request]; ok { + if start.Sub(cachedResult.ts) < 60*1000*1000*1000 { + return cachedResult.result.(*DeparturesResponse), nil + } + } + req, err := http.NewRequest("GET", request, nil) + if err != nil { + return nil, err + } + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if err = json.NewDecoder(resp.Body).Decode(&departures); err != nil { + return nil, err + } + c.cache[request] = cachedResult{ + ts: start, + result: departures, + } + return +} diff --git a/pkg/navitia_api_client/departures_test.go b/pkg/navitia_api_client/departures_test.go new file mode 100644 index 0000000..a1658d2 --- /dev/null +++ b/pkg/navitia_api_client/departures_test.go @@ -0,0 +1,57 @@ +package navitia_api_client + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func TestGetDepartures(t *testing.T) { + // invalid characters in token + client := NewClient("}") + _, err := client.GetDepartures() + if err == nil { + t.Fatalf("invalid characters in token should raise an error") + } + // unreachable server + client = NewClient("https://") + _, err = client.GetDepartures() + if err == nil { + t.Fatalf("unreachable server should raise an error") + } + // invalid json + client, ts := NewTestClientFromFilename(t, "test_data/invalid.json") + defer ts.Close() + _, err = client.GetDepartures() + if err == nil { + t.Fatalf("invalid json should raise an error") + } + // http error + ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + client = NewTestClient(ts) + _, err = client.GetDepartures() + if err == nil { + t.Fatalf("404 should raise an error") + } + // normal working request + client, ts = NewTestClientFromFilename(t, "test_data/normal-crepieux.json") + defer ts.Close() + departures, err := client.GetDepartures() + if err != nil { + t.Fatalf("could not get normal-crepieux departures : %s", err) + } + if len(departures.Departures) != 10 { + t.Fatalf("did not decode normal-crepieux departures properly, got %d departures when expected 10", len(departures.Departures)) + } + // test the cache (assuming the test takes less than 60 seconds (and it really should) it will be accurate) + ts.Close() + departures, err = client.GetDepartures() + if err != nil { + t.Fatalf("could not get normal-crepieux departures : %s", err) + } + if len(departures.Departures) != 10 { + t.Fatalf("did not decode normal-crepieux departures properly, got %d departures when expected 10", len(departures.Departures)) + } +} diff --git a/pkg/navitia_api_client/test_data/invalid.json b/pkg/navitia_api_client/test_data/invalid.json new file mode 100644 index 0000000..98232c6 --- /dev/null +++ b/pkg/navitia_api_client/test_data/invalid.json @@ -0,0 +1 @@ +{ diff --git a/pkg/navitia_api_client/test_data/normal-crepieux.json b/pkg/navitia_api_client/test_data/normal-crepieux.json new file mode 100644 index 0000000..19a635a --- /dev/null +++ b/pkg/navitia_api_client/test_data/normal-crepieux.json @@ -0,0 +1,2135 @@ +{ + "pagination": { + "start_page": 0, + "items_on_page": 10, + "items_per_page": 10, + "total_result": 10 + }, + "links": [ + { + "href": "https://api.sncf.com/v1/coverage/sncf/stop_points/{stop_point.id}", + "type": "stop_point", + "rel": "stop_points", + "templated": true + }, + { + "href": "https://api.sncf.com/v1/coverage/sncf/commercial_modes/{commercial_modes.id}", + "type": "commercial_modes", + "rel": "commercial_modes", + "templated": true + }, + { + "href": "https://api.sncf.com/v1/coverage/sncf/stop_areas/{stop_area.id}", + "type": "stop_area", + "rel": "stop_areas", + "templated": true + }, + { + "href": "https://api.sncf.com/v1/coverage/sncf/physical_modes/{physical_modes.id}", + "type": "physical_modes", + "rel": "physical_modes", + "templated": true + }, + { + "href": "https://api.sncf.com/v1/coverage/sncf/routes/{route.id}", + "type": "route", + "rel": "routes", + "templated": true + }, + { + "href": "https://api.sncf.com/v1/coverage/sncf/commercial_modes/{commercial_mode.id}", + "type": "commercial_mode", + "rel": "commercial_modes", + "templated": true + }, + { + "href": "https://api.sncf.com/v1/coverage/sncf/vehicle_journeys/{vehicle_journey.id}", + "type": "vehicle_journey", + "rel": "vehicle_journeys", + "templated": true + }, + { + "href": "https://api.sncf.com/v1/coverage/sncf/lines/{line.id}", + "type": "line", + "rel": "lines", + "templated": true + }, + { + "href": "https://api.sncf.com/v1/coverage/sncf/physical_modes/{physical_mode.id}", + "type": "physical_mode", + "rel": "physical_modes", + "templated": true + }, + { + "href": "https://api.sncf.com/v1/coverage/sncf/networks/{network.id}", + "type": "network", + "rel": "networks", + "templated": true + }, + { + "href": "https://api.sncf.com/v1/coverage/sncf/stop_areas/stop_area:OCE:SA:87723502/departures", + "type": "first", + "templated": false + } + ], + "disruptions": [], + "notes": [], + "feed_publishers": [], + "departures": [ + { + "display_informations": { + "direction": "Ambérieu-en-Bugey (Ambérieu-en-Bugey)", + "code": "", + "network": "SNCF", + "links": [], + "color": "000000", + "name": "St-Etienne - Lyon - Ambérieu", + "physical_mode": "Train régional / TER", + "headsign": "886823", + "label": "St-Etienne - Lyon - Ambérieu", + "equipments": [], + "text_color": "FFFFFF", + "trip_short_name": "886823", + "commercial_mode": "TER", + "description": "" + }, + "stop_point": { + "commercial_modes": [ + { + "id": "commercial_mode:ter", + "name": "TER" + } + ], + "name": "Crépieux-la-Pape", + "links": [], + "physical_modes": [ + { + "id": "physical_mode:LocalTrain", + "name": "Train régional / TER" + } + ], + "coord": { + "lat": "45.803921", + "lon": "4.892737" + }, + "label": "Crépieux-la-Pape (Rillieux-la-Pape)", + "equipments": [], + "administrative_regions": [ + { + "insee": "69286", + "name": "Rillieux-la-Pape", + "level": 8, + "coord": { + "lat": "45.823514", + "lon": "4.8994366" + }, + "label": "Rillieux-la-Pape (69140)", + "id": "admin:fr:69286", + "zip_code": "69140" + } + ], + "fare_zone": { + "name": "0" + }, + "id": "stop_point:OCE:SP:TrainTER-87723502", + "stop_area": { + "codes": [ + { + "type": "CR-CI-CH", + "value": "0087-723502-00" + }, + { + "type": "UIC8", + "value": "87723502" + }, + { + "type": "external_code", + "value": "OCE87723502" + } + ], + "name": "Crépieux-la-Pape", + "links": [], + "coord": { + "lat": "45.803921", + "lon": "4.892737" + }, + "label": "Crépieux-la-Pape (Rillieux-la-Pape)", + "administrative_regions": [ + { + "insee": "69286", + "name": "Rillieux-la-Pape", + "level": 8, + "coord": { + "lat": "45.823514", + "lon": "4.8994366" + }, + "label": "Rillieux-la-Pape (69140)", + "id": "admin:fr:69286", + "zip_code": "69140" + } + ], + "timezone": "Europe/Paris", + "id": "stop_area:OCE:SA:87723502" + } + }, + "route": { + "direction": { + "embedded_type": "stop_area", + "stop_area": { + "codes": [ + { + "type": "CR-CI-CH", + "value": "0087-743716-BV" + }, + { + "type": "UIC8", + "value": "87743716" + }, + { + "type": "external_code", + "value": "OCE87743716" + } + ], + "name": "Ambérieu-en-Bugey", + "links": [], + "coord": { + "lat": "45.954008", + "lon": "5.342313" + }, + "label": "Ambérieu-en-Bugey (Ambérieu-en-Bugey)", + "timezone": "Europe/Paris", + "id": "stop_area:OCE:SA:87743716" + }, + "quality": 0, + "name": "Ambérieu-en-Bugey (Ambérieu-en-Bugey)", + "id": "stop_area:OCE:SA:87743716" + }, + "name": "St-Etienne-Châteaucreux vers Ambérieu-en-Bugey (Train TER)", + "links": [], + "physical_modes": [ + { + "id": "physical_mode:LocalTrain", + "name": "Train régional / TER" + } + ], + "is_frequence": "False", + "geojson": { + "type": "MultiLineString", + "coordinates": [] + }, + "direction_type": "forward", + "line": { + "code": "", + "name": "St-Etienne - Lyon - Ambérieu", + "links": [], + "color": "000000", + "geojson": { + "type": "MultiLineString", + "coordinates": [] + }, + "text_color": "FFFFFF", + "physical_modes": [ + { + "id": "physical_mode:LocalTrain", + "name": "Train régional / TER" + } + ], + "codes": [], + "closing_time": "221200", + "opening_time": "053500", + "commercial_mode": { + "id": "commercial_mode:ter", + "name": "TER" + }, + "id": "line:OCE:199" + }, + "id": "route:OCE:199-TrainTER-87726000-87743716" + }, + "links": [ + { + "type": "line", + "id": "line:OCE:199" + }, + { + "type": "vehicle_journey", + "id": "vehicle_journey:OCE:SN886823F29029_dst_1" + }, + { + "type": "route", + "id": "route:OCE:199-TrainTER-87726000-87743716" + }, + { + "type": "commercial_mode", + "id": "commercial_mode:ter" + }, + { + "type": "physical_mode", + "id": "physical_mode:LocalTrain" + }, + { + "type": "network", + "id": "network:sncf" + } + ], + "stop_date_time": { + "links": [], + "arrival_date_time": "20210218T131800", + "additional_informations": [], + "departure_date_time": "20210218T131800", + "base_arrival_date_time": "20210218T131800", + "base_departure_date_time": "20210218T131800", + "data_freshness": "base_schedule" + } + }, + { + "display_informations": { + "direction": "St-Etienne-Châteaucreux (Saint-Étienne)", + "code": "", + "network": "SNCF", + "links": [], + "color": "000000", + "name": "St-Etienne - Lyon - Ambérieu", + "physical_mode": "Train régional / TER", + "headsign": "886726", + "label": "St-Etienne - Lyon - Ambérieu", + "equipments": [], + "text_color": "FFFFFF", + "trip_short_name": "886726", + "commercial_mode": "TER", + "description": "" + }, + "stop_point": { + "commercial_modes": [ + { + "id": "commercial_mode:ter", + "name": "TER" + } + ], + "name": "Crépieux-la-Pape", + "links": [], + "physical_modes": [ + { + "id": "physical_mode:LocalTrain", + "name": "Train régional / TER" + } + ], + "coord": { + "lat": "45.803921", + "lon": "4.892737" + }, + "label": "Crépieux-la-Pape (Rillieux-la-Pape)", + "equipments": [], + "administrative_regions": [ + { + "insee": "69286", + "name": "Rillieux-la-Pape", + "level": 8, + "coord": { + "lat": "45.823514", + "lon": "4.8994366" + }, + "label": "Rillieux-la-Pape (69140)", + "id": "admin:fr:69286", + "zip_code": "69140" + } + ], + "fare_zone": { + "name": "0" + }, + "id": "stop_point:OCE:SP:TrainTER-87723502", + "stop_area": { + "codes": [ + { + "type": "CR-CI-CH", + "value": "0087-723502-00" + }, + { + "type": "UIC8", + "value": "87723502" + }, + { + "type": "external_code", + "value": "OCE87723502" + } + ], + "name": "Crépieux-la-Pape", + "links": [], + "coord": { + "lat": "45.803921", + "lon": "4.892737" + }, + "label": "Crépieux-la-Pape (Rillieux-la-Pape)", + "administrative_regions": [ + { + "insee": "69286", + "name": "Rillieux-la-Pape", + "level": 8, + "coord": { + "lat": "45.823514", + "lon": "4.8994366" + }, + "label": "Rillieux-la-Pape (69140)", + "id": "admin:fr:69286", + "zip_code": "69140" + } + ], + "timezone": "Europe/Paris", + "id": "stop_area:OCE:SA:87723502" + } + }, + "route": { + "direction": { + "embedded_type": "stop_area", + "stop_area": { + "codes": [ + { + "type": "CR-CI-CH", + "value": "0087-726000-BV" + }, + { + "type": "UIC8", + "value": "87726000" + }, + { + "type": "external_code", + "value": "OCE87726000" + } + ], + "name": "St-Etienne-Châteaucreux", + "links": [], + "coord": { + "lat": "45.443382", + "lon": "4.399996" + }, + "label": "St-Etienne-Châteaucreux (Saint-Étienne)", + "timezone": "Europe/Paris", + "id": "stop_area:OCE:SA:87726000" + }, + "quality": 0, + "name": "St-Etienne-Châteaucreux (Saint-Étienne)", + "id": "stop_area:OCE:SA:87726000" + }, + "name": "Ambérieu-en-Bugey vers St-Etienne-Châteaucreux (Train TER)", + "links": [], + "physical_modes": [ + { + "id": "physical_mode:LocalTrain", + "name": "Train régional / TER" + } + ], + "is_frequence": "False", + "geojson": { + "type": "MultiLineString", + "coordinates": [] + }, + "direction_type": "backward", + "line": { + "code": "", + "name": "St-Etienne - Lyon - Ambérieu", + "links": [], + "color": "000000", + "geojson": { + "type": "MultiLineString", + "coordinates": [] + }, + "text_color": "FFFFFF", + "physical_modes": [ + { + "id": "physical_mode:LocalTrain", + "name": "Train régional / TER" + } + ], + "codes": [], + "closing_time": "221200", + "opening_time": "053500", + "commercial_mode": { + "id": "commercial_mode:ter", + "name": "TER" + }, + "id": "line:OCE:199" + }, + "id": "route:OCE:199-TrainTER-87743716-87726000" + }, + "links": [ + { + "type": "line", + "id": "line:OCE:199" + }, + { + "type": "vehicle_journey", + "id": "vehicle_journey:OCE:SN886726F35035_dst_1" + }, + { + "type": "route", + "id": "route:OCE:199-TrainTER-87743716-87726000" + }, + { + "type": "commercial_mode", + "id": "commercial_mode:ter" + }, + { + "type": "physical_mode", + "id": "physical_mode:LocalTrain" + }, + { + "type": "network", + "id": "network:sncf" + } + ], + "stop_date_time": { + "links": [], + "arrival_date_time": "20210218T134100", + "additional_informations": [], + "departure_date_time": "20210218T134100", + "base_arrival_date_time": "20210218T134100", + "base_departure_date_time": "20210218T134100", + "data_freshness": "base_schedule" + } + }, + { + "display_informations": { + "direction": "Ambérieu-en-Bugey (Ambérieu-en-Bugey)", + "code": "", + "network": "SNCF", + "links": [], + "color": "000000", + "name": "St-Etienne - Lyon - Ambérieu", + "physical_mode": "Train régional / TER", + "headsign": "886827", + "label": "St-Etienne - Lyon - Ambérieu", + "equipments": [], + "text_color": "FFFFFF", + "trip_short_name": "886827", + "commercial_mode": "TER", + "description": "" + }, + "stop_point": { + "commercial_modes": [ + { + "id": "commercial_mode:ter", + "name": "TER" + } + ], + "name": "Crépieux-la-Pape", + "links": [], + "physical_modes": [ + { + "id": "physical_mode:LocalTrain", + "name": "Train régional / TER" + } + ], + "coord": { + "lat": "45.803921", + "lon": "4.892737" + }, + "label": "Crépieux-la-Pape (Rillieux-la-Pape)", + "equipments": [], + "administrative_regions": [ + { + "insee": "69286", + "name": "Rillieux-la-Pape", + "level": 8, + "coord": { + "lat": "45.823514", + "lon": "4.8994366" + }, + "label": "Rillieux-la-Pape (69140)", + "id": "admin:fr:69286", + "zip_code": "69140" + } + ], + "fare_zone": { + "name": "0" + }, + "id": "stop_point:OCE:SP:TrainTER-87723502", + "stop_area": { + "codes": [ + { + "type": "CR-CI-CH", + "value": "0087-723502-00" + }, + { + "type": "UIC8", + "value": "87723502" + }, + { + "type": "external_code", + "value": "OCE87723502" + } + ], + "name": "Crépieux-la-Pape", + "links": [], + "coord": { + "lat": "45.803921", + "lon": "4.892737" + }, + "label": "Crépieux-la-Pape (Rillieux-la-Pape)", + "administrative_regions": [ + { + "insee": "69286", + "name": "Rillieux-la-Pape", + "level": 8, + "coord": { + "lat": "45.823514", + "lon": "4.8994366" + }, + "label": "Rillieux-la-Pape (69140)", + "id": "admin:fr:69286", + "zip_code": "69140" + } + ], + "timezone": "Europe/Paris", + "id": "stop_area:OCE:SA:87723502" + } + }, + "route": { + "direction": { + "embedded_type": "stop_area", + "stop_area": { + "codes": [ + { + "type": "CR-CI-CH", + "value": "0087-743716-BV" + }, + { + "type": "UIC8", + "value": "87743716" + }, + { + "type": "external_code", + "value": "OCE87743716" + } + ], + "name": "Ambérieu-en-Bugey", + "links": [], + "coord": { + "lat": "45.954008", + "lon": "5.342313" + }, + "label": "Ambérieu-en-Bugey (Ambérieu-en-Bugey)", + "timezone": "Europe/Paris", + "id": "stop_area:OCE:SA:87743716" + }, + "quality": 0, + "name": "Ambérieu-en-Bugey (Ambérieu-en-Bugey)", + "id": "stop_area:OCE:SA:87743716" + }, + "name": "St-Etienne-Châteaucreux vers Ambérieu-en-Bugey (Train TER)", + "links": [], + "physical_modes": [ + { + "id": "physical_mode:LocalTrain", + "name": "Train régional / TER" + } + ], + "is_frequence": "False", + "geojson": { + "type": "MultiLineString", + "coordinates": [] + }, + "direction_type": "forward", + "line": { + "code": "", + "name": "St-Etienne - Lyon - Ambérieu", + "links": [], + "color": "000000", + "geojson": { + "type": "MultiLineString", + "coordinates": [] + }, + "text_color": "FFFFFF", + "physical_modes": [ + { + "id": "physical_mode:LocalTrain", + "name": "Train régional / TER" + } + ], + "codes": [], + "closing_time": "221200", + "opening_time": "053500", + "commercial_mode": { + "id": "commercial_mode:ter", + "name": "TER" + }, + "id": "line:OCE:199" + }, + "id": "route:OCE:199-TrainTER-87726000-87743716" + }, + "links": [ + { + "type": "line", + "id": "line:OCE:199" + }, + { + "type": "vehicle_journey", + "id": "vehicle_journey:OCE:SN886827F22022_dst_1" + }, + { + "type": "route", + "id": "route:OCE:199-TrainTER-87726000-87743716" + }, + { + "type": "commercial_mode", + "id": "commercial_mode:ter" + }, + { + "type": "physical_mode", + "id": "physical_mode:LocalTrain" + }, + { + "type": "network", + "id": "network:sncf" + } + ], + "stop_date_time": { + "links": [], + "arrival_date_time": "20210218T141800", + "additional_informations": [], + "departure_date_time": "20210218T141800", + "base_arrival_date_time": "20210218T141800", + "base_departure_date_time": "20210218T141800", + "data_freshness": "base_schedule" + } + }, + { + "display_informations": { + "direction": "St-Etienne-Châteaucreux (Saint-Étienne)", + "code": "", + "network": "SNCF", + "links": [], + "color": "000000", + "name": "St-Etienne - Lyon - Ambérieu", + "physical_mode": "Train régional / TER", + "headsign": "886728", + "label": "St-Etienne - Lyon - Ambérieu", + "equipments": [], + "text_color": "FFFFFF", + "trip_short_name": "886728", + "commercial_mode": "TER", + "description": "" + }, + "stop_point": { + "commercial_modes": [ + { + "id": "commercial_mode:ter", + "name": "TER" + } + ], + "name": "Crépieux-la-Pape", + "links": [], + "physical_modes": [ + { + "id": "physical_mode:LocalTrain", + "name": "Train régional / TER" + } + ], + "coord": { + "lat": "45.803921", + "lon": "4.892737" + }, + "label": "Crépieux-la-Pape (Rillieux-la-Pape)", + "equipments": [], + "administrative_regions": [ + { + "insee": "69286", + "name": "Rillieux-la-Pape", + "level": 8, + "coord": { + "lat": "45.823514", + "lon": "4.8994366" + }, + "label": "Rillieux-la-Pape (69140)", + "id": "admin:fr:69286", + "zip_code": "69140" + } + ], + "fare_zone": { + "name": "0" + }, + "id": "stop_point:OCE:SP:TrainTER-87723502", + "stop_area": { + "codes": [ + { + "type": "CR-CI-CH", + "value": "0087-723502-00" + }, + { + "type": "UIC8", + "value": "87723502" + }, + { + "type": "external_code", + "value": "OCE87723502" + } + ], + "name": "Crépieux-la-Pape", + "links": [], + "coord": { + "lat": "45.803921", + "lon": "4.892737" + }, + "label": "Crépieux-la-Pape (Rillieux-la-Pape)", + "administrative_regions": [ + { + "insee": "69286", + "name": "Rillieux-la-Pape", + "level": 8, + "coord": { + "lat": "45.823514", + "lon": "4.8994366" + }, + "label": "Rillieux-la-Pape (69140)", + "id": "admin:fr:69286", + "zip_code": "69140" + } + ], + "timezone": "Europe/Paris", + "id": "stop_area:OCE:SA:87723502" + } + }, + "route": { + "direction": { + "embedded_type": "stop_area", + "stop_area": { + "codes": [ + { + "type": "CR-CI-CH", + "value": "0087-726000-BV" + }, + { + "type": "UIC8", + "value": "87726000" + }, + { + "type": "external_code", + "value": "OCE87726000" + } + ], + "name": "St-Etienne-Châteaucreux", + "links": [], + "coord": { + "lat": "45.443382", + "lon": "4.399996" + }, + "label": "St-Etienne-Châteaucreux (Saint-Étienne)", + "timezone": "Europe/Paris", + "id": "stop_area:OCE:SA:87726000" + }, + "quality": 0, + "name": "St-Etienne-Châteaucreux (Saint-Étienne)", + "id": "stop_area:OCE:SA:87726000" + }, + "name": "Ambérieu-en-Bugey vers St-Etienne-Châteaucreux (Train TER)", + "links": [], + "physical_modes": [ + { + "id": "physical_mode:LocalTrain", + "name": "Train régional / TER" + } + ], + "is_frequence": "False", + "geojson": { + "type": "MultiLineString", + "coordinates": [] + }, + "direction_type": "backward", + "line": { + "code": "", + "name": "St-Etienne - Lyon - Ambérieu", + "links": [], + "color": "000000", + "geojson": { + "type": "MultiLineString", + "coordinates": [] + }, + "text_color": "FFFFFF", + "physical_modes": [ + { + "id": "physical_mode:LocalTrain", + "name": "Train régional / TER" + } + ], + "codes": [], + "closing_time": "221200", + "opening_time": "053500", + "commercial_mode": { + "id": "commercial_mode:ter", + "name": "TER" + }, + "id": "line:OCE:199" + }, + "id": "route:OCE:199-TrainTER-87743716-87726000" + }, + "links": [ + { + "type": "line", + "id": "line:OCE:199" + }, + { + "type": "vehicle_journey", + "id": "vehicle_journey:OCE:SN886728F16016_dst_1" + }, + { + "type": "route", + "id": "route:OCE:199-TrainTER-87743716-87726000" + }, + { + "type": "commercial_mode", + "id": "commercial_mode:ter" + }, + { + "type": "physical_mode", + "id": "physical_mode:LocalTrain" + }, + { + "type": "network", + "id": "network:sncf" + } + ], + "stop_date_time": { + "links": [], + "arrival_date_time": "20210218T144100", + "additional_informations": [], + "departure_date_time": "20210218T144100", + "base_arrival_date_time": "20210218T144100", + "base_departure_date_time": "20210218T144100", + "data_freshness": "base_schedule" + } + }, + { + "display_informations": { + "direction": "Ambérieu-en-Bugey (Ambérieu-en-Bugey)", + "code": "", + "network": "SNCF", + "links": [], + "color": "000000", + "name": "St-Etienne - Lyon - Ambérieu", + "physical_mode": "Train régional / TER", + "headsign": "886871", + "label": "St-Etienne - Lyon - Ambérieu", + "equipments": [], + "text_color": "FFFFFF", + "trip_short_name": "886871", + "commercial_mode": "TER", + "description": "" + }, + "stop_point": { + "commercial_modes": [ + { + "id": "commercial_mode:ter", + "name": "TER" + } + ], + "name": "Crépieux-la-Pape", + "links": [], + "physical_modes": [ + { + "id": "physical_mode:LocalTrain", + "name": "Train régional / TER" + } + ], + "coord": { + "lat": "45.803921", + "lon": "4.892737" + }, + "label": "Crépieux-la-Pape (Rillieux-la-Pape)", + "equipments": [], + "administrative_regions": [ + { + "insee": "69286", + "name": "Rillieux-la-Pape", + "level": 8, + "coord": { + "lat": "45.823514", + "lon": "4.8994366" + }, + "label": "Rillieux-la-Pape (69140)", + "id": "admin:fr:69286", + "zip_code": "69140" + } + ], + "fare_zone": { + "name": "0" + }, + "id": "stop_point:OCE:SP:TrainTER-87723502", + "stop_area": { + "codes": [ + { + "type": "CR-CI-CH", + "value": "0087-723502-00" + }, + { + "type": "UIC8", + "value": "87723502" + }, + { + "type": "external_code", + "value": "OCE87723502" + } + ], + "name": "Crépieux-la-Pape", + "links": [], + "coord": { + "lat": "45.803921", + "lon": "4.892737" + }, + "label": "Crépieux-la-Pape (Rillieux-la-Pape)", + "administrative_regions": [ + { + "insee": "69286", + "name": "Rillieux-la-Pape", + "level": 8, + "coord": { + "lat": "45.823514", + "lon": "4.8994366" + }, + "label": "Rillieux-la-Pape (69140)", + "id": "admin:fr:69286", + "zip_code": "69140" + } + ], + "timezone": "Europe/Paris", + "id": "stop_area:OCE:SA:87723502" + } + }, + "route": { + "direction": { + "embedded_type": "stop_area", + "stop_area": { + "codes": [ + { + "type": "CR-CI-CH", + "value": "0087-743716-BV" + }, + { + "type": "UIC8", + "value": "87743716" + }, + { + "type": "external_code", + "value": "OCE87743716" + } + ], + "name": "Ambérieu-en-Bugey", + "links": [], + "coord": { + "lat": "45.954008", + "lon": "5.342313" + }, + "label": "Ambérieu-en-Bugey (Ambérieu-en-Bugey)", + "timezone": "Europe/Paris", + "id": "stop_area:OCE:SA:87743716" + }, + "quality": 0, + "name": "Ambérieu-en-Bugey (Ambérieu-en-Bugey)", + "id": "stop_area:OCE:SA:87743716" + }, + "name": "St-Etienne-Châteaucreux vers Ambérieu-en-Bugey (Train TER)", + "links": [], + "physical_modes": [ + { + "id": "physical_mode:LocalTrain", + "name": "Train régional / TER" + } + ], + "is_frequence": "False", + "geojson": { + "type": "MultiLineString", + "coordinates": [] + }, + "direction_type": "forward", + "line": { + "code": "", + "name": "St-Etienne - Lyon - Ambérieu", + "links": [], + "color": "000000", + "geojson": { + "type": "MultiLineString", + "coordinates": [] + }, + "text_color": "FFFFFF", + "physical_modes": [ + { + "id": "physical_mode:LocalTrain", + "name": "Train régional / TER" + } + ], + "codes": [], + "closing_time": "221200", + "opening_time": "053500", + "commercial_mode": { + "id": "commercial_mode:ter", + "name": "TER" + }, + "id": "line:OCE:199" + }, + "id": "route:OCE:199-TrainTER-87726000-87743716" + }, + "links": [ + { + "type": "line", + "id": "line:OCE:199" + }, + { + "type": "vehicle_journey", + "id": "vehicle_journey:OCE:SN886871F27027_dst_1" + }, + { + "type": "route", + "id": "route:OCE:199-TrainTER-87726000-87743716" + }, + { + "type": "commercial_mode", + "id": "commercial_mode:ter" + }, + { + "type": "physical_mode", + "id": "physical_mode:LocalTrain" + }, + { + "type": "network", + "id": "network:sncf" + } + ], + "stop_date_time": { + "links": [], + "arrival_date_time": "20210218T151800", + "additional_informations": [], + "departure_date_time": "20210218T151800", + "base_arrival_date_time": "20210218T151800", + "base_departure_date_time": "20210218T151800", + "data_freshness": "base_schedule" + } + }, + { + "display_informations": { + "direction": "Ambérieu-en-Bugey (Ambérieu-en-Bugey)", + "code": "", + "network": "SNCF", + "links": [], + "color": "000000", + "name": "St-Etienne - Lyon - Ambérieu", + "physical_mode": "Train régional / TER", + "headsign": "886837", + "label": "St-Etienne - Lyon - Ambérieu", + "equipments": [], + "text_color": "FFFFFF", + "trip_short_name": "886837", + "commercial_mode": "TER", + "description": "" + }, + "stop_point": { + "commercial_modes": [ + { + "id": "commercial_mode:ter", + "name": "TER" + } + ], + "name": "Crépieux-la-Pape", + "links": [], + "physical_modes": [ + { + "id": "physical_mode:LocalTrain", + "name": "Train régional / TER" + } + ], + "coord": { + "lat": "45.803921", + "lon": "4.892737" + }, + "label": "Crépieux-la-Pape (Rillieux-la-Pape)", + "equipments": [], + "administrative_regions": [ + { + "insee": "69286", + "name": "Rillieux-la-Pape", + "level": 8, + "coord": { + "lat": "45.823514", + "lon": "4.8994366" + }, + "label": "Rillieux-la-Pape (69140)", + "id": "admin:fr:69286", + "zip_code": "69140" + } + ], + "fare_zone": { + "name": "0" + }, + "id": "stop_point:OCE:SP:TrainTER-87723502", + "stop_area": { + "codes": [ + { + "type": "CR-CI-CH", + "value": "0087-723502-00" + }, + { + "type": "UIC8", + "value": "87723502" + }, + { + "type": "external_code", + "value": "OCE87723502" + } + ], + "name": "Crépieux-la-Pape", + "links": [], + "coord": { + "lat": "45.803921", + "lon": "4.892737" + }, + "label": "Crépieux-la-Pape (Rillieux-la-Pape)", + "administrative_regions": [ + { + "insee": "69286", + "name": "Rillieux-la-Pape", + "level": 8, + "coord": { + "lat": "45.823514", + "lon": "4.8994366" + }, + "label": "Rillieux-la-Pape (69140)", + "id": "admin:fr:69286", + "zip_code": "69140" + } + ], + "timezone": "Europe/Paris", + "id": "stop_area:OCE:SA:87723502" + } + }, + "route": { + "direction": { + "embedded_type": "stop_area", + "stop_area": { + "codes": [ + { + "type": "CR-CI-CH", + "value": "0087-743716-BV" + }, + { + "type": "UIC8", + "value": "87743716" + }, + { + "type": "external_code", + "value": "OCE87743716" + } + ], + "name": "Ambérieu-en-Bugey", + "links": [], + "coord": { + "lat": "45.954008", + "lon": "5.342313" + }, + "label": "Ambérieu-en-Bugey (Ambérieu-en-Bugey)", + "timezone": "Europe/Paris", + "id": "stop_area:OCE:SA:87743716" + }, + "quality": 0, + "name": "Ambérieu-en-Bugey (Ambérieu-en-Bugey)", + "id": "stop_area:OCE:SA:87743716" + }, + "name": "St-Etienne-Châteaucreux vers Ambérieu-en-Bugey (Train TER)", + "links": [], + "physical_modes": [ + { + "id": "physical_mode:LocalTrain", + "name": "Train régional / TER" + } + ], + "is_frequence": "False", + "geojson": { + "type": "MultiLineString", + "coordinates": [] + }, + "direction_type": "forward", + "line": { + "code": "", + "name": "St-Etienne - Lyon - Ambérieu", + "links": [], + "color": "000000", + "geojson": { + "type": "MultiLineString", + "coordinates": [] + }, + "text_color": "FFFFFF", + "physical_modes": [ + { + "id": "physical_mode:LocalTrain", + "name": "Train régional / TER" + } + ], + "codes": [], + "closing_time": "221200", + "opening_time": "053500", + "commercial_mode": { + "id": "commercial_mode:ter", + "name": "TER" + }, + "id": "line:OCE:199" + }, + "id": "route:OCE:199-TrainTER-87726000-87743716" + }, + "links": [ + { + "type": "line", + "id": "line:OCE:199" + }, + { + "type": "vehicle_journey", + "id": "vehicle_journey:OCE:SN886837F18018_dst_1" + }, + { + "type": "route", + "id": "route:OCE:199-TrainTER-87726000-87743716" + }, + { + "type": "commercial_mode", + "id": "commercial_mode:ter" + }, + { + "type": "physical_mode", + "id": "physical_mode:LocalTrain" + }, + { + "type": "network", + "id": "network:sncf" + } + ], + "stop_date_time": { + "links": [], + "arrival_date_time": "20210218T161800", + "additional_informations": [], + "departure_date_time": "20210218T161800", + "base_arrival_date_time": "20210218T161800", + "base_departure_date_time": "20210218T161800", + "data_freshness": "base_schedule" + } + }, + { + "display_informations": { + "direction": "St-Etienne-Châteaucreux (Saint-Étienne)", + "code": "", + "network": "SNCF", + "links": [], + "color": "000000", + "name": "St-Etienne - Lyon - Ambérieu", + "physical_mode": "Train régional / TER", + "headsign": "886734", + "label": "St-Etienne - Lyon - Ambérieu", + "equipments": [], + "text_color": "FFFFFF", + "trip_short_name": "886734", + "commercial_mode": "TER", + "description": "" + }, + "stop_point": { + "commercial_modes": [ + { + "id": "commercial_mode:ter", + "name": "TER" + } + ], + "name": "Crépieux-la-Pape", + "links": [], + "physical_modes": [ + { + "id": "physical_mode:LocalTrain", + "name": "Train régional / TER" + } + ], + "coord": { + "lat": "45.803921", + "lon": "4.892737" + }, + "label": "Crépieux-la-Pape (Rillieux-la-Pape)", + "equipments": [], + "administrative_regions": [ + { + "insee": "69286", + "name": "Rillieux-la-Pape", + "level": 8, + "coord": { + "lat": "45.823514", + "lon": "4.8994366" + }, + "label": "Rillieux-la-Pape (69140)", + "id": "admin:fr:69286", + "zip_code": "69140" + } + ], + "fare_zone": { + "name": "0" + }, + "id": "stop_point:OCE:SP:TrainTER-87723502", + "stop_area": { + "codes": [ + { + "type": "CR-CI-CH", + "value": "0087-723502-00" + }, + { + "type": "UIC8", + "value": "87723502" + }, + { + "type": "external_code", + "value": "OCE87723502" + } + ], + "name": "Crépieux-la-Pape", + "links": [], + "coord": { + "lat": "45.803921", + "lon": "4.892737" + }, + "label": "Crépieux-la-Pape (Rillieux-la-Pape)", + "administrative_regions": [ + { + "insee": "69286", + "name": "Rillieux-la-Pape", + "level": 8, + "coord": { + "lat": "45.823514", + "lon": "4.8994366" + }, + "label": "Rillieux-la-Pape (69140)", + "id": "admin:fr:69286", + "zip_code": "69140" + } + ], + "timezone": "Europe/Paris", + "id": "stop_area:OCE:SA:87723502" + } + }, + "route": { + "direction": { + "embedded_type": "stop_area", + "stop_area": { + "codes": [ + { + "type": "CR-CI-CH", + "value": "0087-726000-BV" + }, + { + "type": "UIC8", + "value": "87726000" + }, + { + "type": "external_code", + "value": "OCE87726000" + } + ], + "name": "St-Etienne-Châteaucreux", + "links": [], + "coord": { + "lat": "45.443382", + "lon": "4.399996" + }, + "label": "St-Etienne-Châteaucreux (Saint-Étienne)", + "timezone": "Europe/Paris", + "id": "stop_area:OCE:SA:87726000" + }, + "quality": 0, + "name": "St-Etienne-Châteaucreux (Saint-Étienne)", + "id": "stop_area:OCE:SA:87726000" + }, + "name": "Ambérieu-en-Bugey vers St-Etienne-Châteaucreux (Train TER)", + "links": [], + "physical_modes": [ + { + "id": "physical_mode:LocalTrain", + "name": "Train régional / TER" + } + ], + "is_frequence": "False", + "geojson": { + "type": "MultiLineString", + "coordinates": [] + }, + "direction_type": "backward", + "line": { + "code": "", + "name": "St-Etienne - Lyon - Ambérieu", + "links": [], + "color": "000000", + "geojson": { + "type": "MultiLineString", + "coordinates": [] + }, + "text_color": "FFFFFF", + "physical_modes": [ + { + "id": "physical_mode:LocalTrain", + "name": "Train régional / TER" + } + ], + "codes": [], + "closing_time": "221200", + "opening_time": "053500", + "commercial_mode": { + "id": "commercial_mode:ter", + "name": "TER" + }, + "id": "line:OCE:199" + }, + "id": "route:OCE:199-TrainTER-87743716-87726000" + }, + "links": [ + { + "type": "line", + "id": "line:OCE:199" + }, + { + "type": "vehicle_journey", + "id": "vehicle_journey:OCE:SN886734F19019_dst_1" + }, + { + "type": "route", + "id": "route:OCE:199-TrainTER-87743716-87726000" + }, + { + "type": "commercial_mode", + "id": "commercial_mode:ter" + }, + { + "type": "physical_mode", + "id": "physical_mode:LocalTrain" + }, + { + "type": "network", + "id": "network:sncf" + } + ], + "stop_date_time": { + "links": [], + "arrival_date_time": "20210218T164100", + "additional_informations": [], + "departure_date_time": "20210218T164100", + "base_arrival_date_time": "20210218T164100", + "base_departure_date_time": "20210218T164100", + "data_freshness": "base_schedule" + } + }, + { + "display_informations": { + "direction": "Ambérieu-en-Bugey (Ambérieu-en-Bugey)", + "code": "", + "network": "SNCF", + "links": [], + "color": "000000", + "name": "St-Etienne - Lyon - Ambérieu", + "physical_mode": "Train régional / TER", + "headsign": "886839", + "label": "St-Etienne - Lyon - Ambérieu", + "equipments": [], + "text_color": "FFFFFF", + "trip_short_name": "886839", + "commercial_mode": "TER", + "description": "" + }, + "stop_point": { + "commercial_modes": [ + { + "id": "commercial_mode:ter", + "name": "TER" + } + ], + "name": "Crépieux-la-Pape", + "links": [], + "physical_modes": [ + { + "id": "physical_mode:LocalTrain", + "name": "Train régional / TER" + } + ], + "coord": { + "lat": "45.803921", + "lon": "4.892737" + }, + "label": "Crépieux-la-Pape (Rillieux-la-Pape)", + "equipments": [], + "administrative_regions": [ + { + "insee": "69286", + "name": "Rillieux-la-Pape", + "level": 8, + "coord": { + "lat": "45.823514", + "lon": "4.8994366" + }, + "label": "Rillieux-la-Pape (69140)", + "id": "admin:fr:69286", + "zip_code": "69140" + } + ], + "fare_zone": { + "name": "0" + }, + "id": "stop_point:OCE:SP:TrainTER-87723502", + "stop_area": { + "codes": [ + { + "type": "CR-CI-CH", + "value": "0087-723502-00" + }, + { + "type": "UIC8", + "value": "87723502" + }, + { + "type": "external_code", + "value": "OCE87723502" + } + ], + "name": "Crépieux-la-Pape", + "links": [], + "coord": { + "lat": "45.803921", + "lon": "4.892737" + }, + "label": "Crépieux-la-Pape (Rillieux-la-Pape)", + "administrative_regions": [ + { + "insee": "69286", + "name": "Rillieux-la-Pape", + "level": 8, + "coord": { + "lat": "45.823514", + "lon": "4.8994366" + }, + "label": "Rillieux-la-Pape (69140)", + "id": "admin:fr:69286", + "zip_code": "69140" + } + ], + "timezone": "Europe/Paris", + "id": "stop_area:OCE:SA:87723502" + } + }, + "route": { + "direction": { + "embedded_type": "stop_area", + "stop_area": { + "codes": [ + { + "type": "CR-CI-CH", + "value": "0087-743716-BV" + }, + { + "type": "UIC8", + "value": "87743716" + }, + { + "type": "external_code", + "value": "OCE87743716" + } + ], + "name": "Ambérieu-en-Bugey", + "links": [], + "coord": { + "lat": "45.954008", + "lon": "5.342313" + }, + "label": "Ambérieu-en-Bugey (Ambérieu-en-Bugey)", + "timezone": "Europe/Paris", + "id": "stop_area:OCE:SA:87743716" + }, + "quality": 0, + "name": "Ambérieu-en-Bugey (Ambérieu-en-Bugey)", + "id": "stop_area:OCE:SA:87743716" + }, + "name": "St-Etienne-Châteaucreux vers Ambérieu-en-Bugey (Train TER)", + "links": [], + "physical_modes": [ + { + "id": "physical_mode:LocalTrain", + "name": "Train régional / TER" + } + ], + "is_frequence": "False", + "geojson": { + "type": "MultiLineString", + "coordinates": [] + }, + "direction_type": "forward", + "line": { + "code": "", + "name": "St-Etienne - Lyon - Ambérieu", + "links": [], + "color": "000000", + "geojson": { + "type": "MultiLineString", + "coordinates": [] + }, + "text_color": "FFFFFF", + "physical_modes": [ + { + "id": "physical_mode:LocalTrain", + "name": "Train régional / TER" + } + ], + "codes": [], + "closing_time": "221200", + "opening_time": "053500", + "commercial_mode": { + "id": "commercial_mode:ter", + "name": "TER" + }, + "id": "line:OCE:199" + }, + "id": "route:OCE:199-TrainTER-87726000-87743716" + }, + "links": [ + { + "type": "line", + "id": "line:OCE:199" + }, + { + "type": "vehicle_journey", + "id": "vehicle_journey:OCE:SN886839F10010_dst_1" + }, + { + "type": "route", + "id": "route:OCE:199-TrainTER-87726000-87743716" + }, + { + "type": "commercial_mode", + "id": "commercial_mode:ter" + }, + { + "type": "physical_mode", + "id": "physical_mode:LocalTrain" + }, + { + "type": "network", + "id": "network:sncf" + } + ], + "stop_date_time": { + "links": [], + "arrival_date_time": "20210218T164800", + "additional_informations": [], + "departure_date_time": "20210218T164800", + "base_arrival_date_time": "20210218T164800", + "base_departure_date_time": "20210218T164800", + "data_freshness": "base_schedule" + } + }, + { + "display_informations": { + "direction": "Ambérieu-en-Bugey (Ambérieu-en-Bugey)", + "code": "", + "network": "SNCF", + "links": [], + "color": "000000", + "name": "St-Etienne - Lyon - Ambérieu", + "physical_mode": "Train régional / TER", + "headsign": "886843", + "label": "St-Etienne - Lyon - Ambérieu", + "equipments": [], + "text_color": "FFFFFF", + "trip_short_name": "886843", + "commercial_mode": "TER", + "description": "" + }, + "stop_point": { + "commercial_modes": [ + { + "id": "commercial_mode:ter", + "name": "TER" + } + ], + "name": "Crépieux-la-Pape", + "links": [], + "physical_modes": [ + { + "id": "physical_mode:LocalTrain", + "name": "Train régional / TER" + } + ], + "coord": { + "lat": "45.803921", + "lon": "4.892737" + }, + "label": "Crépieux-la-Pape (Rillieux-la-Pape)", + "equipments": [], + "administrative_regions": [ + { + "insee": "69286", + "name": "Rillieux-la-Pape", + "level": 8, + "coord": { + "lat": "45.823514", + "lon": "4.8994366" + }, + "label": "Rillieux-la-Pape (69140)", + "id": "admin:fr:69286", + "zip_code": "69140" + } + ], + "fare_zone": { + "name": "0" + }, + "id": "stop_point:OCE:SP:TrainTER-87723502", + "stop_area": { + "codes": [ + { + "type": "CR-CI-CH", + "value": "0087-723502-00" + }, + { + "type": "UIC8", + "value": "87723502" + }, + { + "type": "external_code", + "value": "OCE87723502" + } + ], + "name": "Crépieux-la-Pape", + "links": [], + "coord": { + "lat": "45.803921", + "lon": "4.892737" + }, + "label": "Crépieux-la-Pape (Rillieux-la-Pape)", + "administrative_regions": [ + { + "insee": "69286", + "name": "Rillieux-la-Pape", + "level": 8, + "coord": { + "lat": "45.823514", + "lon": "4.8994366" + }, + "label": "Rillieux-la-Pape (69140)", + "id": "admin:fr:69286", + "zip_code": "69140" + } + ], + "timezone": "Europe/Paris", + "id": "stop_area:OCE:SA:87723502" + } + }, + "route": { + "direction": { + "embedded_type": "stop_area", + "stop_area": { + "codes": [ + { + "type": "CR-CI-CH", + "value": "0087-743716-BV" + }, + { + "type": "UIC8", + "value": "87743716" + }, + { + "type": "external_code", + "value": "OCE87743716" + } + ], + "name": "Ambérieu-en-Bugey", + "links": [], + "coord": { + "lat": "45.954008", + "lon": "5.342313" + }, + "label": "Ambérieu-en-Bugey (Ambérieu-en-Bugey)", + "timezone": "Europe/Paris", + "id": "stop_area:OCE:SA:87743716" + }, + "quality": 0, + "name": "Ambérieu-en-Bugey (Ambérieu-en-Bugey)", + "id": "stop_area:OCE:SA:87743716" + }, + "name": "St-Etienne-Châteaucreux vers Ambérieu-en-Bugey (Train TER)", + "links": [], + "physical_modes": [ + { + "id": "physical_mode:LocalTrain", + "name": "Train régional / TER" + } + ], + "is_frequence": "False", + "geojson": { + "type": "MultiLineString", + "coordinates": [] + }, + "direction_type": "forward", + "line": { + "code": "", + "name": "St-Etienne - Lyon - Ambérieu", + "links": [], + "color": "000000", + "geojson": { + "type": "MultiLineString", + "coordinates": [] + }, + "text_color": "FFFFFF", + "physical_modes": [ + { + "id": "physical_mode:LocalTrain", + "name": "Train régional / TER" + } + ], + "codes": [], + "closing_time": "221200", + "opening_time": "053500", + "commercial_mode": { + "id": "commercial_mode:ter", + "name": "TER" + }, + "id": "line:OCE:199" + }, + "id": "route:OCE:199-TrainTER-87726000-87743716" + }, + "links": [ + { + "type": "line", + "id": "line:OCE:199" + }, + { + "type": "vehicle_journey", + "id": "vehicle_journey:OCE:SN886843F24024_dst_1" + }, + { + "type": "route", + "id": "route:OCE:199-TrainTER-87726000-87743716" + }, + { + "type": "commercial_mode", + "id": "commercial_mode:ter" + }, + { + "type": "physical_mode", + "id": "physical_mode:LocalTrain" + }, + { + "type": "network", + "id": "network:sncf" + } + ], + "stop_date_time": { + "links": [], + "arrival_date_time": "20210218T171800", + "additional_informations": [], + "departure_date_time": "20210218T171800", + "base_arrival_date_time": "20210218T171800", + "base_departure_date_time": "20210218T171800", + "data_freshness": "base_schedule" + } + }, + { + "display_informations": { + "direction": "St-Etienne-Châteaucreux (Saint-Étienne)", + "code": "", + "network": "SNCF", + "links": [], + "color": "000000", + "name": "St-Etienne - Lyon - Ambérieu", + "physical_mode": "Train régional / TER", + "headsign": "886738", + "label": "St-Etienne - Lyon - Ambérieu", + "equipments": [], + "text_color": "FFFFFF", + "trip_short_name": "886738", + "commercial_mode": "TER", + "description": "" + }, + "stop_point": { + "commercial_modes": [ + { + "id": "commercial_mode:ter", + "name": "TER" + } + ], + "name": "Crépieux-la-Pape", + "links": [], + "physical_modes": [ + { + "id": "physical_mode:LocalTrain", + "name": "Train régional / TER" + } + ], + "coord": { + "lat": "45.803921", + "lon": "4.892737" + }, + "label": "Crépieux-la-Pape (Rillieux-la-Pape)", + "equipments": [], + "administrative_regions": [ + { + "insee": "69286", + "name": "Rillieux-la-Pape", + "level": 8, + "coord": { + "lat": "45.823514", + "lon": "4.8994366" + }, + "label": "Rillieux-la-Pape (69140)", + "id": "admin:fr:69286", + "zip_code": "69140" + } + ], + "fare_zone": { + "name": "0" + }, + "id": "stop_point:OCE:SP:TrainTER-87723502", + "stop_area": { + "codes": [ + { + "type": "CR-CI-CH", + "value": "0087-723502-00" + }, + { + "type": "UIC8", + "value": "87723502" + }, + { + "type": "external_code", + "value": "OCE87723502" + } + ], + "name": "Crépieux-la-Pape", + "links": [], + "coord": { + "lat": "45.803921", + "lon": "4.892737" + }, + "label": "Crépieux-la-Pape (Rillieux-la-Pape)", + "administrative_regions": [ + { + "insee": "69286", + "name": "Rillieux-la-Pape", + "level": 8, + "coord": { + "lat": "45.823514", + "lon": "4.8994366" + }, + "label": "Rillieux-la-Pape (69140)", + "id": "admin:fr:69286", + "zip_code": "69140" + } + ], + "timezone": "Europe/Paris", + "id": "stop_area:OCE:SA:87723502" + } + }, + "route": { + "direction": { + "embedded_type": "stop_area", + "stop_area": { + "codes": [ + { + "type": "CR-CI-CH", + "value": "0087-726000-BV" + }, + { + "type": "UIC8", + "value": "87726000" + }, + { + "type": "external_code", + "value": "OCE87726000" + } + ], + "name": "St-Etienne-Châteaucreux", + "links": [], + "coord": { + "lat": "45.443382", + "lon": "4.399996" + }, + "label": "St-Etienne-Châteaucreux (Saint-Étienne)", + "timezone": "Europe/Paris", + "id": "stop_area:OCE:SA:87726000" + }, + "quality": 0, + "name": "St-Etienne-Châteaucreux (Saint-Étienne)", + "id": "stop_area:OCE:SA:87726000" + }, + "name": "Ambérieu-en-Bugey vers St-Etienne-Châteaucreux (Train TER)", + "links": [], + "physical_modes": [ + { + "id": "physical_mode:LocalTrain", + "name": "Train régional / TER" + } + ], + "is_frequence": "False", + "geojson": { + "type": "MultiLineString", + "coordinates": [] + }, + "direction_type": "backward", + "line": { + "code": "", + "name": "St-Etienne - Lyon - Ambérieu", + "links": [], + "color": "000000", + "geojson": { + "type": "MultiLineString", + "coordinates": [] + }, + "text_color": "FFFFFF", + "physical_modes": [ + { + "id": "physical_mode:LocalTrain", + "name": "Train régional / TER" + } + ], + "codes": [], + "closing_time": "221200", + "opening_time": "053500", + "commercial_mode": { + "id": "commercial_mode:ter", + "name": "TER" + }, + "id": "line:OCE:199" + }, + "id": "route:OCE:199-TrainTER-87743716-87726000" + }, + "links": [ + { + "type": "line", + "id": "line:OCE:199" + }, + { + "type": "vehicle_journey", + "id": "vehicle_journey:OCE:SN886738F27027_dst_1" + }, + { + "type": "route", + "id": "route:OCE:199-TrainTER-87743716-87726000" + }, + { + "type": "commercial_mode", + "id": "commercial_mode:ter" + }, + { + "type": "physical_mode", + "id": "physical_mode:LocalTrain" + }, + { + "type": "network", + "id": "network:sncf" + } + ], + "stop_date_time": { + "links": [], + "arrival_date_time": "20210218T174100", + "additional_informations": [], + "departure_date_time": "20210218T174100", + "base_arrival_date_time": "20210218T174100", + "base_departure_date_time": "20210218T174100", + "data_freshness": "base_schedule" + } + } + ], + "context": { + "timezone": "Europe/Paris", + "current_datetime": "20210218T125549" + }, + "exceptions": [] +} -- cgit v1.2.3