Skip to content

Commit

Permalink
Merge pull request #208 from AdrianJSClark/205-support-driver-stats-b…
Browse files Browse the repository at this point in the history
…y-category-csv-download

Driver Statistics CSV Retrieval
  • Loading branch information
AdrianJSClark authored Jun 9, 2024
2 parents a5ce56f + 0919f4f commit 144e330
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 24 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace Aydsko.iRacingData.IntegrationTests.Stats;

internal sealed class DriverStatisticsByCategoryCsvTests : DataClientIntegrationFixture
{
[Test]
public async Task TestDriverStatisticsByCategoryCsvAsync()
{
var driverStats = await Client.GetDriverStatisticsByCategoryCsvAsync(4).ConfigureAwait(false);

Assert.Multiple(() =>
{
Assert.That(driverStats, Is.Not.Null);
Assert.That(driverStats.FileName, Is.Not.Null.Or.Empty);
Assert.That(driverStats.FileName, Is.EqualTo("Dirt_Road_driver_stats.csv"));
Assert.That(driverStats.ContentBytes, Is.Not.Null);
Assert.That(driverStats.ContentBytes, Is.Not.Empty);
});
}
}
4 changes: 2 additions & 2 deletions src/Aydsko.iRacingData/Common/LinkResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace Aydsko.iRacingData.Common;

internal sealed class LinkResult
public sealed class LinkResult
{
[JsonPropertyName("link")]
public string Link { get; set; } = default!;
Expand All @@ -12,5 +12,5 @@ internal sealed class LinkResult
}

[JsonSerializable(typeof(LinkResult)), JsonSourceGenerationOptions(WriteIndented = true)]
internal partial class LinkResultContext : JsonSerializerContext
public partial class LinkResultContext : JsonSerializerContext
{ }
19 changes: 19 additions & 0 deletions src/Aydsko.iRacingData/CompatibilitySuppressions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,13 @@
<Right>lib/netstandard2.0/Aydsko.iRacingData.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Aydsko.iRacingData.IDataClient.GetDriverStatisticsByCategoryCsvAsync(System.Int32,System.Threading.CancellationToken)</Target>
<Left>lib/netstandard2.0/Aydsko.iRacingData.dll</Left>
<Right>lib/netstandard2.0/Aydsko.iRacingData.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Aydsko.iRacingData.IDataClient.GetMemberChartDataAsync(System.Nullable{System.Int32},System.Int32,Aydsko.iRacingData.Member.MemberChartType,System.Threading.CancellationToken)</Target>
Expand All @@ -261,6 +268,12 @@
<Left>lib/net6.0/Aydsko.iRacingData.dll</Left>
<Right>lib/net8.0/Aydsko.iRacingData.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0008</DiagnosticId>
<Target>T:Aydsko.iRacingData.Common.LinkResultContext</Target>
<Left>lib/net6.0/Aydsko.iRacingData.dll</Left>
<Right>lib/net8.0/Aydsko.iRacingData.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0008</DiagnosticId>
<Target>T:Aydsko.iRacingData.Common.ResultOrderDirection</Target>
Expand Down Expand Up @@ -297,4 +310,10 @@
<Left>lib/net6.0/Aydsko.iRacingData.dll</Left>
<Right>lib/net8.0/Aydsko.iRacingData.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0008</DiagnosticId>
<Target>T:Aydsko.iRacingData.Common.LinkResultContext</Target>
<Left>lib/netstandard2.0/Aydsko.iRacingData.dll</Left>
<Right>lib/net6.0/Aydsko.iRacingData.dll</Right>
</Suppression>
</Suppressions>
92 changes: 71 additions & 21 deletions src/Aydsko.iRacingData/DataClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1828,11 +1828,54 @@ public async Task<DataResponse<SpectatorSubsessionIds>> GetSpectatorSubsessionId
cancellationToken).ConfigureAwait(false);
}

/// <inheritdoc />
public async Task<DriverStatisticsCsvFile> GetDriverStatisticsByCategoryCsvAsync(int categoryId, CancellationToken cancellationToken = default)
{
await EnsureLoggedInAsync(cancellationToken).ConfigureAwait(false);

var infoLinkUri = categoryId switch
{
1 => new Uri("https://members-ng.iracing.com/data/driver_stats_by_category/oval"),
2 => new Uri("https://members-ng.iracing.com/data/driver_stats_by_category/road"),
3 => new Uri("https://members-ng.iracing.com/data/driver_stats_by_category/dirt_oval"),
4 => new Uri("https://members-ng.iracing.com/data/driver_stats_by_category/dirt_road"),
5 => new Uri("https://members-ng.iracing.com/data/driver_stats_by_category/sports_car"),
6 => new Uri("https://members-ng.iracing.com/data/driver_stats_by_category/formula_car"),
_ => throw new ArgumentOutOfRangeException(nameof(categoryId), categoryId, "Invalid Category Id value. Must be between 1 and 6 (inclusive)."),
};

var (infoLink, _) = await BuildLinkResultAsync(infoLinkUri, cancellationToken).ConfigureAwait(false);

var infoLinkUrl = new Uri(infoLink.Link);

var csvDataResponse = await httpClient.GetAsync(infoLinkUrl, cancellationToken).ConfigureAwait(false);

if (!csvDataResponse.IsSuccessStatusCode)
{
throw new iRacingDataClientException($"Failed to retrieve CSV data. HTTP response was \"{csvDataResponse.StatusCode} {csvDataResponse.ReasonPhrase}\"");
}

var fileName = csvDataResponse.Content.Headers.ContentDisposition?.FileName
?? infoLinkUrl.AbsolutePath.Split('/').LastOrDefault()
?? $"DriverStatistics_CategoryId_{categoryId}.csv";

#pragma warning disable CA2016 // Forward the 'CancellationToken' parameter to methods - this method doesn't support cancellation
var result = new DriverStatisticsCsvFile
{
CategoryId = categoryId,
FileName = fileName,
ContentBytes = await csvDataResponse.Content.ReadAsByteArrayAsync().ConfigureAwait(false)
};
#pragma warning restore CA2016 // Forward the 'CancellationToken' parameter to methods

return result;
}

/// <summary>Will ensure the client is authenticated by checking the <see cref="IsLoggedIn"/> property and executing the login process if required.</summary>
/// <param name="cancellationToken">A token to allow the operation to be cancelled.</param>
/// <returns>A <see cref="Task"/> that resolves when the process is complete.</returns>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Maintainability", "CA1508:Avoid dead conditional code", Justification = "Double-check of the precondition is a common pattern when using a lock and initialisation method.")]
protected async Task EnsureLoggedInAsync(CancellationToken cancellationToken)
protected internal async Task EnsureLoggedInAsync(CancellationToken cancellationToken)
{
if (IsLoggedIn is false)
{
Expand Down Expand Up @@ -1946,30 +1989,13 @@ private async Task LoginInternalAsync(CancellationToken cancellationToken)

protected virtual async Task<DataResponse<TData>> CreateResponseViaInfoLinkAsync<TData>(Uri infoLinkUri, JsonTypeInfo<TData> jsonTypeInfo, CancellationToken cancellationToken)
{
var infoLinkResponse = await httpClient.GetAsync(infoLinkUri, cancellationToken).ConfigureAwait(false);

#if NET6_0_OR_GREATER
var content = await infoLinkResponse.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
#else
var content = await infoLinkResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
#endif
var (infoLink, headers) = await BuildLinkResultAsync(infoLinkUri, cancellationToken).ConfigureAwait(false);

if (!infoLinkResponse.IsSuccessStatusCode || content == RateLimitExceededContent)
{
HandleUnsuccessfulResponse(infoLinkResponse, content, logger);
}

var infoLink = JsonSerializer.Deserialize(content, LinkResultContext.Default.LinkResult);
if (infoLink?.Link is null)
{
throw new iRacingDataClientException("Unrecognized result.");
}

var data = await httpClient.GetFromJsonAsync(infoLink.Link, jsonTypeInfo, cancellationToken: cancellationToken)
var data = await httpClient.GetFromJsonAsync(infoLink.Link, jsonTypeInfo, cancellationToken)
.ConfigureAwait(false)
?? throw new iRacingDataClientException("Data not found.");

return BuildDataResponse(infoLinkResponse.Headers, data, logger, infoLink.Expires);
return BuildDataResponse(headers, data, logger, infoLink.Expires);
}

protected virtual async Task<DataResponse<(TData, TChunkData[])>> CreateResponseFromChunkedDataAsync<TData, THeaderData, TChunkData>(Uri uri, JsonTypeInfo<TData> jsonTypeInfo, JsonTypeInfo<TChunkData[]> chunkArrayTypeInfo, CancellationToken cancellationToken)
Expand Down Expand Up @@ -2024,6 +2050,30 @@ protected virtual async Task<DataResponse<TData>> CreateResponseViaInfoLinkAsync
return BuildDataResponse<(TData Header, TChunkData[] Results)>(response.Headers, (headerData, searchResults.ToArray()), logger);
}

protected virtual async Task<(LinkResult, HttpResponseHeaders)> BuildLinkResultAsync(Uri infoLinkUri, CancellationToken cancellationToken)
{
var infoLinkResponse = await httpClient.GetAsync(infoLinkUri, cancellationToken).ConfigureAwait(false);

#if NET6_0_OR_GREATER
var content = await infoLinkResponse.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
#else
var content = await infoLinkResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
#endif

if (!infoLinkResponse.IsSuccessStatusCode || content == RateLimitExceededContent)
{
HandleUnsuccessfulResponse(infoLinkResponse, content, logger);
}

var infoLink = JsonSerializer.Deserialize(content, LinkResultContext.Default.LinkResult);
if (infoLink is null || infoLink.Link is null)
{
throw new iRacingDataClientException("Unrecognized result.");
}

return (infoLink, infoLinkResponse.Headers);
}

protected virtual void HandleUnsuccessfulResponse(HttpResponseMessage httpResponse, string content, ILogger logger)
{
#if NET6_0_OR_GREATER
Expand Down
7 changes: 7 additions & 0 deletions src/Aydsko.iRacingData/IDataClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -703,4 +703,11 @@ public interface IDataClient
/// <param name="cancellationToken">A token to allow the operation to be cancelled.</param>
/// <returns>A collection of <see cref="WeatherForecast"/> objects detailing the forecasted weather.</returns>
Task<IEnumerable<WeatherForecast>> GetWeatherForecastFromUrlAsync(string url, CancellationToken cancellationToken = default);

Check warning on line 705 in src/Aydsko.iRacingData/IDataClient.cs

View workflow job for this annotation

GitHub Actions / Build & Test / build

Change the type of parameter 'url' of method 'IDataClient.GetWeatherForecastFromUrlAsync(string,

/// <summary>Retrieve a comma separated value (CSV) file containing driver statistics for the given category.</summary>
/// <param name="categoryId">A valid category identifier.</param>
/// <param name="cancellationToken">A token to allow the operation to be cancelled.</param>
/// <returns>A <see cref="Task"/> that resolves to the content of the CSV.</returns>
/// <seealso cref="Constants.Category"/>
Task<DriverStatisticsCsvFile> GetDriverStatisticsByCategoryCsvAsync(int categoryId, CancellationToken cancellationToken = default);
}
3 changes: 2 additions & 1 deletion src/Aydsko.iRacingData/Package Release Notes.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
BREAKING CHANGES:

- "Aydsko.iRacingData.Results.SessionResultsWeather" several properties have had their type changed to "decimal" to properly represent the data. (Issue: 202)
- "Aydsko.iRacingData.Results.SessionResultsWeather" several properties have had their type changed to "decimal" to properly represent the data. (Issue: #202)



Fixes / Changes:

- New property "AverageLapTime" on "Aydsko.iRacingData.Results.RaceSummary" to give a proper duration representation of the "AverageLap" value.
- Support "Driver Stats by Category CSV" Download (Issue: #205)
16 changes: 16 additions & 0 deletions src/Aydsko.iRacingData/Stats/DriverStatisticsCsvFile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace Aydsko.iRacingData.Stats;

/// <summary>Represents a comma separated value (CSV) file containing statistics about a category of drivers.</summary>
/// <seealso cref="IDataClient.GetDriverStatisticsByCategoryCsvAsync(int, CancellationToken)"/>
/// <seealso cref="Constants.Category"/>
public class DriverStatisticsCsvFile
{
/// <summary>The Category Id value used to retrieve these statistics.</summary>
public int CategoryId { get; set; }

/// <summary>The name of the file.</summary>
public string FileName { get; set; } = default!;

/// <summary>Content of the CSV file.</summary>
public byte[] ContentBytes { get; set; } = default!;
}

0 comments on commit 144e330

Please sign in to comment.