From c3cd62ed415330523832ca51d82b7e030c3d9c4d Mon Sep 17 00:00:00 2001 From: nik-solcast Date: Fri, 22 Nov 2024 16:21:04 +1100 Subject: [PATCH] added update check (#57) --- generate_sdk_csharp.py | 155 +++++++++++++++++++++++++++--- src/Solcast/Clients/BaseClient.cs | 153 ++++++++++++++++++++++++++--- 2 files changed, 280 insertions(+), 28 deletions(-) diff --git a/generate_sdk_csharp.py b/generate_sdk_csharp.py index 09ed2d3..bb25e77 100644 --- a/generate_sdk_csharp.py +++ b/generate_sdk_csharp.py @@ -155,16 +155,24 @@ def generate_base_client(): using System.Net.Http; using System.Reflection; using System.Net; +using System.Text.RegularExpressions; namespace Solcast.Clients { public abstract class BaseClient : IDisposable { + private static bool _updateChecked = false; private bool _disposed = false; protected readonly HttpClient _httpClient; - protected BaseClient(string baseUrl = null, IWebProxy proxy = null) + protected BaseClient(string baseUrl = null, IWebProxy proxy = null, bool checkForUpdates = true) { + if (checkForUpdates && !_updateChecked) + { + CheckForUpdates(); + _updateChecked = true; + } + var apiKey = Environment.GetEnvironmentVariable("SOLCAST_API_KEY"); if (string.IsNullOrEmpty(apiKey)) { @@ -184,11 +192,141 @@ def generate_base_client(): }; _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}"); - // Get the version from the assembly metadata for User-Agent var version = GetAssemblyVersion(); _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd($"solcast-api-csharp-sdk/{version}"); } + public static void CheckForUpdates() + { + const string githubApiUrl = "https://api.github.com/repos/solcast/solcast-api-csharp-sdk/releases/latest"; + + try + { + string currentVersion = NormalizeVersion(GetAssemblyVersion()); + + using var client = new HttpClient(); + client.DefaultRequestHeaders.UserAgent.ParseAdd("solcast-sdk-version-check"); + var response = client.GetStringAsync(githubApiUrl).Result; + dynamic releaseInfo = Newtonsoft.Json.JsonConvert.DeserializeObject(response); + + string latestVersionRaw = releaseInfo?.tag_name ?? "unknown"; + string latestVersion = NormalizeVersion(latestVersionRaw); + + if (CompareSemanticVersions(currentVersion, latestVersion) < 0) + { + Console.WriteLine($@"A new version of the SDK is available: {latestVersionRaw}. +To update, run the following command: + dotnet add package Solcast --version {latestVersion} +"); + } + } + catch (Exception e) + { + // Gracefully handle any errors (e.g., network issues or API rate limits) + Console.WriteLine($"Failed to check for SDK updates: {e.Message}"); + } + } + + private static bool IsPreReleaseVersion(string version) + { + // Check if the version contains a pre-release identifier + var regex = new Regex(@"-\w+(\.\w+)*$"); + return regex.IsMatch(version); + } + + private static string NormalizeVersion(string version) + { + return version.TrimStart('v', 'V'); // Remove the "v" prefix (case-insensitive) + } + + private static int CompareSemanticVersions(string currentVersion, string latestVersion) + { + var currentParts = ParseSemanticVersion(currentVersion); + var latestParts = ParseSemanticVersion(latestVersion); + + // Compare numeric components: major, minor, patch + for (int i = 0; i < 3; i++) + { + int currentPart = currentParts.NumericParts[i]; + int latestPart = latestParts.NumericParts[i]; + + if (currentPart < latestPart) return -1; + if (currentPart > latestPart) return 1; + } + + // Compare pre-release components + return ComparePreRelease(currentParts.PreRelease, latestParts.PreRelease); + } + + private static (int[] NumericParts, string PreRelease) ParseSemanticVersion(string version) + { + var regex = new Regex(@"^(?\d+)\.(?\d+)\.(?\d+)(-(?[a-zA-Z0-9.-]+))?$"); + var match = regex.Match(version); + + if (!match.Success) + throw new FormatException($"Invalid semantic version: {version}"); + + int major = int.Parse(match.Groups["major"].Value); + int minor = int.Parse(match.Groups["minor"].Value); + int patch = int.Parse(match.Groups["patch"].Value); + string preRelease = match.Groups["preRelease"].Value; // May be empty + + return (new[] { major, minor, patch }, preRelease); + } + + private static int ComparePreRelease(string currentPreRelease, string latestPreRelease) + { + // No pre-release means higher precedence + if (string.IsNullOrEmpty(currentPreRelease) && !string.IsNullOrEmpty(latestPreRelease)) + return 1; + if (!string.IsNullOrEmpty(currentPreRelease) && string.IsNullOrEmpty(latestPreRelease)) + return -1; + + // If both are null or empty, they are equal + if (string.IsNullOrEmpty(currentPreRelease) && string.IsNullOrEmpty(latestPreRelease)) + return 0; + + // Split pre-release identifiers by dots and compare lexicographically + var currentParts = currentPreRelease.Split('.'); + var latestParts = latestPreRelease.Split('.'); + + for (int i = 0; i < Math.Max(currentParts.Length, latestParts.Length); i++) + { + string currentPart = i < currentParts.Length ? currentParts[i] : ""; + string latestPart = i < latestParts.Length ? latestParts[i] : ""; + + int currentNumeric, latestNumeric; + bool currentIsNumeric = int.TryParse(currentPart, out currentNumeric); + bool latestIsNumeric = int.TryParse(latestPart, out latestNumeric); + + if (currentIsNumeric && latestIsNumeric) + { + // Compare as numbers + if (currentNumeric < latestNumeric) return -1; + if (currentNumeric > latestNumeric) return 1; + } + else + { + // Compare as strings + int comparison = string.Compare(currentPart, latestPart, StringComparison.Ordinal); + if (comparison != 0) return comparison; + } + } + + return 0; // Versions are equal + } + + private static string GetAssemblyVersion() + { + var attribute = (AssemblyInformationalVersionAttribute)Attribute.GetCustomAttribute( + Assembly.GetExecutingAssembly(), + typeof(AssemblyInformationalVersionAttribute) + ); + + var version = attribute?.InformationalVersion ?? "1.0.0"; + return version.Split('+')[0]; // Return the version without build metadata + } + protected void HandleUnauthorizedResponse(HttpResponseMessage response) { if (response.StatusCode == HttpStatusCode.Unauthorized) @@ -215,17 +353,6 @@ def generate_base_client(): _disposed = true; } } - - private static string GetAssemblyVersion() - { - var attribute = (AssemblyInformationalVersionAttribute)Attribute.GetCustomAttribute( - Assembly.GetExecutingAssembly(), - typeof(AssemblyInformationalVersionAttribute) - ); - - var version = attribute?.InformationalVersion ?? "1.0.0"; - return version.Split('+')[0]; - } } public class MissingApiKeyException : Exception @@ -787,9 +914,7 @@ def extract_class_name(class_code): '/data/live/', '/data/forecast/', '/data/historic/', - # '/data/historic_forecast/', '/data/tmy/', - # '/data/geographic/', '/resources/pv_power_site', ] diff --git a/src/Solcast/Clients/BaseClient.cs b/src/Solcast/Clients/BaseClient.cs index d53981d..841a9e6 100644 --- a/src/Solcast/Clients/BaseClient.cs +++ b/src/Solcast/Clients/BaseClient.cs @@ -3,16 +3,24 @@ using System.Net.Http; using System.Reflection; using System.Net; +using System.Text.RegularExpressions; namespace Solcast.Clients { public abstract class BaseClient : IDisposable { + private static bool _updateChecked = false; private bool _disposed = false; protected readonly HttpClient _httpClient; - protected BaseClient(string baseUrl = null, IWebProxy proxy = null) + protected BaseClient(string baseUrl = null, IWebProxy proxy = null, bool checkForUpdates = true) { + if (checkForUpdates && !_updateChecked) + { + CheckForUpdates(); + _updateChecked = true; + } + var apiKey = Environment.GetEnvironmentVariable("SOLCAST_API_KEY"); if (string.IsNullOrEmpty(apiKey)) { @@ -32,11 +40,141 @@ protected BaseClient(string baseUrl = null, IWebProxy proxy = null) }; _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}"); - // Get the version from the assembly metadata for User-Agent var version = GetAssemblyVersion(); _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd($"solcast-api-csharp-sdk/{version}"); } + public static void CheckForUpdates() + { + const string githubApiUrl = "https://api.github.com/repos/solcast/solcast-api-csharp-sdk/releases/latest"; + + try + { + string currentVersion = NormalizeVersion(GetAssemblyVersion()); + + using var client = new HttpClient(); + client.DefaultRequestHeaders.UserAgent.ParseAdd("solcast-sdk-version-check"); + var response = client.GetStringAsync(githubApiUrl).Result; + dynamic releaseInfo = Newtonsoft.Json.JsonConvert.DeserializeObject(response); + + string latestVersionRaw = releaseInfo?.tag_name ?? "unknown"; + string latestVersion = NormalizeVersion(latestVersionRaw); + + if (CompareSemanticVersions(currentVersion, latestVersion) < 0) + { + Console.WriteLine($@"A new version of the SDK is available: {latestVersionRaw}. +To update, run the following command: + dotnet add package Solcast --version {latestVersion} +"); + } + } + catch (Exception e) + { + // Gracefully handle any errors (e.g., network issues or API rate limits) + Console.WriteLine($"Failed to check for SDK updates: {e.Message}"); + } + } + + private static bool IsPreReleaseVersion(string version) + { + // Check if the version contains a pre-release identifier + var regex = new Regex(@"-\w+(\.\w+)*$"); + return regex.IsMatch(version); + } + + private static string NormalizeVersion(string version) + { + return version.TrimStart('v', 'V'); // Remove the "v" prefix (case-insensitive) + } + + private static int CompareSemanticVersions(string currentVersion, string latestVersion) + { + var currentParts = ParseSemanticVersion(currentVersion); + var latestParts = ParseSemanticVersion(latestVersion); + + // Compare numeric components: major, minor, patch + for (int i = 0; i < 3; i++) + { + int currentPart = currentParts.NumericParts[i]; + int latestPart = latestParts.NumericParts[i]; + + if (currentPart < latestPart) return -1; + if (currentPart > latestPart) return 1; + } + + // Compare pre-release components + return ComparePreRelease(currentParts.PreRelease, latestParts.PreRelease); + } + + private static (int[] NumericParts, string PreRelease) ParseSemanticVersion(string version) + { + var regex = new Regex(@"^(?\d+)\.(?\d+)\.(?\d+)(-(?[a-zA-Z0-9.-]+))?$"); + var match = regex.Match(version); + + if (!match.Success) + throw new FormatException($"Invalid semantic version: {version}"); + + int major = int.Parse(match.Groups["major"].Value); + int minor = int.Parse(match.Groups["minor"].Value); + int patch = int.Parse(match.Groups["patch"].Value); + string preRelease = match.Groups["preRelease"].Value; // May be empty + + return (new[] { major, minor, patch }, preRelease); + } + + private static int ComparePreRelease(string currentPreRelease, string latestPreRelease) + { + // No pre-release means higher precedence + if (string.IsNullOrEmpty(currentPreRelease) && !string.IsNullOrEmpty(latestPreRelease)) + return 1; + if (!string.IsNullOrEmpty(currentPreRelease) && string.IsNullOrEmpty(latestPreRelease)) + return -1; + + // If both are null or empty, they are equal + if (string.IsNullOrEmpty(currentPreRelease) && string.IsNullOrEmpty(latestPreRelease)) + return 0; + + // Split pre-release identifiers by dots and compare lexicographically + var currentParts = currentPreRelease.Split('.'); + var latestParts = latestPreRelease.Split('.'); + + for (int i = 0; i < Math.Max(currentParts.Length, latestParts.Length); i++) + { + string currentPart = i < currentParts.Length ? currentParts[i] : ""; + string latestPart = i < latestParts.Length ? latestParts[i] : ""; + + int currentNumeric, latestNumeric; + bool currentIsNumeric = int.TryParse(currentPart, out currentNumeric); + bool latestIsNumeric = int.TryParse(latestPart, out latestNumeric); + + if (currentIsNumeric && latestIsNumeric) + { + // Compare as numbers + if (currentNumeric < latestNumeric) return -1; + if (currentNumeric > latestNumeric) return 1; + } + else + { + // Compare as strings + int comparison = string.Compare(currentPart, latestPart, StringComparison.Ordinal); + if (comparison != 0) return comparison; + } + } + + return 0; // Versions are equal + } + + private static string GetAssemblyVersion() + { + var attribute = (AssemblyInformationalVersionAttribute)Attribute.GetCustomAttribute( + Assembly.GetExecutingAssembly(), + typeof(AssemblyInformationalVersionAttribute) + ); + + var version = attribute?.InformationalVersion ?? "1.0.0"; + return version.Split('+')[0]; // Return the version without build metadata + } + protected void HandleUnauthorizedResponse(HttpResponseMessage response) { if (response.StatusCode == HttpStatusCode.Unauthorized) @@ -63,17 +201,6 @@ protected virtual void Dispose(bool disposing) _disposed = true; } } - - private static string GetAssemblyVersion() - { - var attribute = (AssemblyInformationalVersionAttribute)Attribute.GetCustomAttribute( - Assembly.GetExecutingAssembly(), - typeof(AssemblyInformationalVersionAttribute) - ); - - var version = attribute?.InformationalVersion ?? "1.0.0"; - return version.Split('+')[0]; - } } public class MissingApiKeyException : Exception