diff options
| author | Ariel Costas Guerrero <ariel@costas.dev> | 2025-11-06 16:48:43 +0100 |
|---|---|---|
| committer | Ariel Costas Guerrero <ariel@costas.dev> | 2025-11-06 16:48:43 +0100 |
| commit | 817b29603e5b7f79bfe6489eebf73961e6ca93f2 (patch) | |
| tree | 09057b38ca6ed23ce7852b62ae1d5c97a91cfe9b | |
| parent | 94658119f2c0b555f83e870fbeb50150f18fc38d (diff) | |
Move controllers, add config, read stop schedules from local directory
| -rw-r--r-- | Taskfile.yml | 5 | ||||
| -rw-r--r-- | src/Costasdev.Busurbano.Backend/Configuration/AppConfiguration.cs | 6 | ||||
| -rw-r--r-- | src/Costasdev.Busurbano.Backend/Controllers/SantiagoController.cs (renamed from src/Costasdev.Busurbano.Backend/SantiagoController.cs) | 2 | ||||
| -rw-r--r-- | src/Costasdev.Busurbano.Backend/Controllers/VigoController.cs (renamed from src/Costasdev.Busurbano.Backend/VigoController.cs) | 93 | ||||
| -rw-r--r-- | src/Costasdev.Busurbano.Backend/Program.cs | 4 | ||||
| -rw-r--r-- | src/Costasdev.Busurbano.Backend/Types/VigoSchedules.cs | 43 | ||||
| -rw-r--r-- | src/gtfs_vigo_stops/pytest.ini | 4 |
7 files changed, 68 insertions, 89 deletions
diff --git a/Taskfile.yml b/Taskfile.yml index 8a15306..84aab94 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -32,3 +32,8 @@ tasks: - npm run build --prefix src/frontend - mkdir dist/frontend - mv src/frontend/build/client/ dist/frontend/ + + gen-stop-report: + desc: Generate stop-based JSON reports for specified dates or date ranges. + cmds: + - uv --directory ./src/gtfs_vigo_stops run ./stop_report.py --output-dir ./output --feed-url https://datos.vigo.org/data/transporte/gtfs_vigo.zip diff --git a/src/Costasdev.Busurbano.Backend/Configuration/AppConfiguration.cs b/src/Costasdev.Busurbano.Backend/Configuration/AppConfiguration.cs new file mode 100644 index 0000000..97296e5 --- /dev/null +++ b/src/Costasdev.Busurbano.Backend/Configuration/AppConfiguration.cs @@ -0,0 +1,6 @@ +namespace Costasdev.Busurbano.Backend.Configuration; + +public class AppConfiguration +{ + public required string ScheduleBasePath { get; set; } +} diff --git a/src/Costasdev.Busurbano.Backend/SantiagoController.cs b/src/Costasdev.Busurbano.Backend/Controllers/SantiagoController.cs index c86c74b..24ecab9 100644 --- a/src/Costasdev.Busurbano.Backend/SantiagoController.cs +++ b/src/Costasdev.Busurbano.Backend/Controllers/SantiagoController.cs @@ -3,7 +3,7 @@ using Costasdev.VigoTransitApi.Types; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; -namespace Costasdev.Busurbano.Backend; +namespace Costasdev.Busurbano.Backend.Controllers; [ApiController] [Route("api/santiago")] diff --git a/src/Costasdev.Busurbano.Backend/VigoController.cs b/src/Costasdev.Busurbano.Backend/Controllers/VigoController.cs index 320d898..e82baed 100644 --- a/src/Costasdev.Busurbano.Backend/VigoController.cs +++ b/src/Costasdev.Busurbano.Backend/Controllers/VigoController.cs @@ -1,27 +1,29 @@ using System.Globalization; using System.Text; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Caching.Memory; -using Costasdev.VigoTransitApi; using System.Text.Json; +using Costasdev.Busurbano.Backend.Configuration; using Costasdev.Busurbano.Backend.Types; -using Costasdev.VigoTransitApi.Types; +using Costasdev.VigoTransitApi; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using SysFile = System.IO.File; -namespace Costasdev.Busurbano.Backend; +namespace Costasdev.Busurbano.Backend.Controllers; [ApiController] [Route("api/vigo")] public class VigoController : ControllerBase { + private readonly ILogger<VigoController> _logger; private readonly VigoTransitApiClient _api; - private readonly IMemoryCache _cache; - private readonly HttpClient _httpClient; + private readonly AppConfiguration _configuration; - public VigoController(HttpClient http, IMemoryCache cache) + public VigoController(HttpClient http, IOptions<AppConfiguration> options, ILogger<VigoController> logger) { + _logger = logger; _api = new VigoTransitApiClient(http); - _cache = cache; - _httpClient = http; + _configuration = options.Value; } [HttpGet("GetStopEstimates")] @@ -53,44 +55,21 @@ public class VigoController : ControllerBase return BadRequest("Invalid date format. Please use yyyy-MM-dd format."); } - // Create cache key - var cacheKey = $"timetable_{date}_{stopId}"; - - // Try to get from cache first - if (_cache.TryGetValue(cacheKey, out var cachedData)) - { - Response.Headers.Append("App-CacheUsage", "HIT"); - return new OkObjectResult(cachedData); - } - try { var timetableData = await LoadTimetable(stopId.ToString(), date); - // Cache the data for 12 hours - var cacheOptions = new MemoryCacheEntryOptions - { - AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(12), - SlidingExpiration = TimeSpan.FromHours(6), // Refresh cache if accessed within 6 hours of expiry - Priority = CacheItemPriority.Normal - }; - - _cache.Set(cacheKey, timetableData, cacheOptions); - - Response.Headers.Append("App-CacheUsage", "MISS"); return new OkObjectResult(timetableData); } - catch (HttpRequestException ex) - { - return StatusCode((int?)ex.StatusCode ?? 500, $"Error fetching timetable data: {ex.Message}"); - } - catch (JsonException ex) + catch (FileNotFoundException ex) { - return StatusCode(500, $"Error parsing timetable data: {ex.Message}"); + _logger.LogError(ex, "Stop data not found for stop {StopId} on date {Date}", stopId, date); + return StatusCode(404, $"Stop data not found for stop {stopId} on date {date}"); } - catch (Exception ex) + catch(Exception ex) { - return StatusCode(500, $"Unexpected error: {ex.Message}"); + _logger.LogError(ex, "Error loading stop data"); + return StatusCode(500, "Error loading timetable"); } } @@ -119,23 +98,19 @@ public class VigoController : ControllerBase var realtimeTask = _api.GetStopEstimates(stopId); var timetableTask = LoadTimetable(stopId.ToString(), now.ToString("yyyy-MM-dd")); - Task.WaitAll(realtimeTask, timetableTask); + await Task.WhenAll(realtimeTask, timetableTask); var realTimeEstimates = realtimeTask.Result.Estimates; var timetable = timetableTask.Result; - /*var now = DateTime.Today.AddHours(17).AddMinutes(59); - var realTimeEstimates = LoadDebugEstimates(); - var timetable = LoadDebugTimetable();*/ - foreach (var estimate in realTimeEstimates) { outputBuffer.AppendLine($"Parsing estimate with line={estimate.Line}, route={estimate.Route} and minutes={estimate.Minutes} - Arrives at {now.AddMinutes(estimate.Minutes):HH:mm}"); var fullArrivalTime = now.AddMinutes(estimate.Minutes); var possibleCirculations = timetable - .Where(c => c.Line.Name.Trim() == estimate.Line.Trim() && c.Trip.Headsign.Trim() == estimate.Route.Trim()) - .OrderBy(c => c.DepartureDateTime()) + .Where(c => c.Line.Trim() == estimate.Line.Trim() && c.Route.Trim() == estimate.Route.Trim()) + .OrderBy(c => c.CallingDateTime()) .ToArray(); outputBuffer.AppendLine($"Found {possibleCirculations.Length} potential circulations"); @@ -145,7 +120,7 @@ public class VigoController : ControllerBase foreach (var circulation in possibleCirculations) { - var diffBetweenScheduleAndTrip = (int)Math.Round((fullArrivalTime - circulation.DepartureDateTime()).TotalMinutes); + var diffBetweenScheduleAndTrip = (int)Math.Round((fullArrivalTime - circulation.CallingDateTime()).TotalMinutes); var diffBetweenNowAndSchedule = (int)(fullArrivalTime - now).TotalMinutes; var tolerance = Math.Max(2, diffBetweenNowAndSchedule * 0.15); // Positive amount of minutes @@ -168,7 +143,7 @@ public class VigoController : ControllerBase foreach (var circulation in possibleCirculations) { // Circulation A 03LP000_008003_16 stopping at 05/11/2025 22:06:00 (diff: -03:29:59.2644092) - outputBuffer.AppendLine($"Circulation {circulation.Trip.Id} stopping at {circulation.DepartureDateTime()} (diff: {fullArrivalTime - circulation.DepartureDateTime():HH:mm})"); + outputBuffer.AppendLine($"Circulation {circulation.TripId} stopping at {circulation.CallingDateTime()} (diff: {fullArrivalTime - circulation.CallingDateTime():HH:mm})"); } outputBuffer.AppendLine(); @@ -177,19 +152,19 @@ public class VigoController : ControllerBase if (closestCirculationTime > 0) { - outputBuffer.Append($"Closest circulation is {closestCirculation.Trip.Id} and arriving {closestCirculationTime} minutes LATE"); + outputBuffer.Append($"Closest circulation is {closestCirculation.TripId} and arriving {closestCirculationTime} minutes LATE"); } else if (closestCirculationTime == 0) { - outputBuffer.Append($"Closest circulation is {closestCirculation.Trip.Id} and arriving ON TIME"); + outputBuffer.Append($"Closest circulation is {closestCirculation.TripId} and arriving ON TIME"); } else { - outputBuffer.Append($"Closest circulation is {closestCirculation.Trip.Id} and arriving {Math.Abs(closestCirculationTime)} minutes EARLY"); + outputBuffer.Append($"Closest circulation is {closestCirculation.TripId} and arriving {Math.Abs(closestCirculationTime)} minutes EARLY"); } outputBuffer.AppendLine( - $" -- Circulation expected at {closestCirculation.DepartureDateTime():HH:mm)}"); + $" -- Circulation expected at {closestCirculation.CallingDateTime():HH:mm)}"); outputBuffer.AppendLine(); } @@ -199,12 +174,12 @@ public class VigoController : ControllerBase private async Task<List<ScheduledStop>> LoadTimetable(string stopId, string dateString) { - var url = $"https://www.costas.dev/static-storage/vitrasa_svc/stops/{dateString}/{stopId}.json"; - var response = await _httpClient.GetAsync(url); - - response.EnsureSuccessStatusCode(); - - var jsonContent = await response.Content.ReadAsStringAsync(); - return JsonSerializer.Deserialize<List<ScheduledStop>>(jsonContent) ?? []; + var file = Path.Combine(_configuration.ScheduleBasePath, dateString, stopId + ".json"); + if (!SysFile.Exists(file)) + { + throw new FileNotFoundException(); + } + var contents = await SysFile.ReadAllTextAsync(file); + return JsonSerializer.Deserialize<List<ScheduledStop>>(contents)!; } } diff --git a/src/Costasdev.Busurbano.Backend/Program.cs b/src/Costasdev.Busurbano.Backend/Program.cs index 68f84fb..7de4039 100644 --- a/src/Costasdev.Busurbano.Backend/Program.cs +++ b/src/Costasdev.Busurbano.Backend/Program.cs @@ -1,5 +1,9 @@ +using Costasdev.Busurbano.Backend.Configuration; + var builder = WebApplication.CreateBuilder(args); +builder.Services.Configure<AppConfiguration>(builder.Configuration.GetSection("App")); + builder.Services.AddControllers(); builder.Services.AddHttpClient(); builder.Services.AddMemoryCache(); diff --git a/src/Costasdev.Busurbano.Backend/Types/VigoSchedules.cs b/src/Costasdev.Busurbano.Backend/Types/VigoSchedules.cs index d6012e5..f8e5634 100644 --- a/src/Costasdev.Busurbano.Backend/Types/VigoSchedules.cs +++ b/src/Costasdev.Busurbano.Backend/Types/VigoSchedules.cs @@ -4,35 +4,28 @@ namespace Costasdev.Busurbano.Backend.Types; public class ScheduledStop { - [JsonPropertyName("line")] public required Line Line { get; set; } - [JsonPropertyName("trip")] public required Trip Trip { get; set; } - [JsonPropertyName("route_id")] public required string RouteId { get; set; } - [JsonPropertyName("departure_time")] public required string DepartureTime { get; set; } - - public DateTime DepartureDateTime() - { - var dt = DateTime.Today + TimeOnly.Parse(DepartureTime).ToTimeSpan(); - return dt.AddSeconds(60 - dt.Second); - } - + [JsonPropertyName("trip_id")] public required string TripId { get; set; } + [JsonPropertyName("service_id")] public required string ServiceId { get; set; } + [JsonPropertyName("line")] public required string Line { get; set; } + [JsonPropertyName("route")] public required string Route { get; set; } [JsonPropertyName("stop_sequence")] public required int StopSequence { get; set; } [JsonPropertyName("shape_dist_traveled")] - public required float ShapeDistTraveled { get; set; } + public required double ShapeDistTraveled { get; set; } [JsonPropertyName("next_streets")] public required string[] NextStreets { get; set; } -} - -public class Line -{ - [JsonPropertyName("name")] public required string Name { get; set; } - [JsonPropertyName("colour")] public required string Colour { get; set; } -} + [JsonPropertyName("starting_code")] public required string StartingCode { get; set; } + [JsonPropertyName("starting_name")] public required string StartingName { get; set; } + [JsonPropertyName("starting_time")] public required string StartingTime { get; set; } + [JsonPropertyName("calling_time")] public required string CallingTime { get; set; } + public DateTime CallingDateTime() + { + var dt = DateTime.Today + TimeOnly.Parse(CallingTime).ToTimeSpan(); + return dt.AddSeconds(60 - dt.Second); + } -public class Trip -{ - [JsonPropertyName("id")] public required string Id { get; set; } - [JsonPropertyName("service_id")] public required string ServiceId { get; set; } - [JsonPropertyName("headsign")] public required string Headsign { get; set; } - [JsonPropertyName("direction_id")] public required int DirectionId { get; set; } + [JsonPropertyName("calling_ssm")] public required int CallingSsm { get; set; } + [JsonPropertyName("terminus_code")] public required string TerminusCode { get; set; } + [JsonPropertyName("terminus_name")] public required string TerminusName { get; set; } + [JsonPropertyName("terminus_time")] public required string TerminusTime { get; set; } } diff --git a/src/gtfs_vigo_stops/pytest.ini b/src/gtfs_vigo_stops/pytest.ini deleted file mode 100644 index e455bb4..0000000 --- a/src/gtfs_vigo_stops/pytest.ini +++ /dev/null @@ -1,4 +0,0 @@ -[pytest] -minversion = 6.0 -testpaths = tests -python_files = test_*.py |
