diff --git a/Octokit.Reactive/Clients/IObservableMiscellaneousClient.cs b/Octokit.Reactive/Clients/IObservableMiscellaneousClient.cs index 3116b40978..56ac658a8c 100644 --- a/Octokit.Reactive/Clients/IObservableMiscellaneousClient.cs +++ b/Octokit.Reactive/Clients/IObservableMiscellaneousClient.cs @@ -50,5 +50,14 @@ public interface IObservableMiscellaneousClient /// /// A that includes the license key, text, and attributes of the license. IObservable GetLicense(string key); + + /// + /// Gets API Rate Limits (API service rather than header info). + /// + /// Thrown when a general API error occurs. + /// An of Rate Limits. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")] + IObservable GetRateLimits(); + } } diff --git a/Octokit.Reactive/Clients/ObservableMiscellaneousClient.cs b/Octokit.Reactive/Clients/ObservableMiscellaneousClient.cs index befd0de374..0675437f56 100644 --- a/Octokit.Reactive/Clients/ObservableMiscellaneousClient.cs +++ b/Octokit.Reactive/Clients/ObservableMiscellaneousClient.cs @@ -74,5 +74,16 @@ public IObservable GetLicense(string key) { return _client.GetLicense(key).ToObservable(); } + + /// + /// Gets API Rate Limits (API service rather than header info). + /// + /// Thrown when a general API error occurs. + /// An of Rate Limits. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")] + public IObservable GetRateLimits() + { + return _client.GetRateLimits().ToObservable(); + } } } diff --git a/Octokit.Tests.Integration/Clients/MiscellaneousClientTests.cs b/Octokit.Tests.Integration/Clients/MiscellaneousClientTests.cs index 256a73f3a4..149c49b262 100644 --- a/Octokit.Tests.Integration/Clients/MiscellaneousClientTests.cs +++ b/Octokit.Tests.Integration/Clients/MiscellaneousClientTests.cs @@ -70,4 +70,37 @@ public async Task CanRetrieveListOfLicenses() Assert.Equal("MIT License", result.Name); } } + + public class TheGetResourceRateLimitsMethod + { + [IntegrationTest] + public async Task CanRetrieveResourceRateLimits() + { + var github = Helper.GetAuthenticatedClient(); + + var result = await github.Miscellaneous.GetRateLimits(); + + // Test the core limits + Assert.True(result.Resources.Core.Limit > 0); + Assert.True(result.Resources.Core.Remaining > -1); + Assert.True(result.Resources.Core.Remaining <= result.Resources.Core.Limit); + Assert.True(result.Resources.Core.ResetAsUtcEpochSeconds > 0); + Assert.NotNull(result.Resources.Core.Reset); + + // Test the search limits + Assert.True(result.Resources.Search.Limit > 0); + Assert.True(result.Resources.Search.Remaining > -1); + Assert.True(result.Resources.Search.Remaining <= result.Resources.Search.Limit); + Assert.True(result.Resources.Search.ResetAsUtcEpochSeconds > 0); + Assert.NotNull(result.Resources.Search.Reset); + + // Test the depreciated rate limits + Assert.True(result.Rate.Limit > 0); + Assert.True(result.Rate.Remaining > -1); + Assert.True(result.Rate.Remaining <= result.Rate.Limit); + Assert.True(result.Resources.Search.ResetAsUtcEpochSeconds > 0); + Assert.NotNull(result.Resources.Search.Reset); + + } + } } diff --git a/Octokit.Tests/Clients/MiscellaneousClientTests.cs b/Octokit.Tests/Clients/MiscellaneousClientTests.cs index 0b397c7e30..98eaac131a 100644 --- a/Octokit.Tests/Clients/MiscellaneousClientTests.cs +++ b/Octokit.Tests/Clients/MiscellaneousClientTests.cs @@ -4,6 +4,7 @@ using NSubstitute; using Octokit.Internal; using Xunit; +using System.Globalization; namespace Octokit.Tests.Clients { @@ -58,6 +59,63 @@ public async Task RequestsTheEmojiEndpoint() } } + public class TheGetResourceRateLimitsMethod + { + [Fact] + public async Task RequestsTheRecourceRateLimitEndpoint() + { + IApiResponse response = new ApiResponse + ( + new Response(), + new MiscellaneousRateLimit( + new ResourceRateLimit( + new RateLimit(5000, 4999, 1372700873), + new RateLimit(30, 18, 1372700873) + ), + new RateLimit(100, 75, 1372700873) + ) + ); + var connection = Substitute.For(); + connection.Get(Args.Uri, null, null).Returns(Task.FromResult(response)); + var client = new MiscellaneousClient(connection); + + var result = await client.GetRateLimits(); + + // Test the core limits + Assert.Equal(5000, result.Resources.Core.Limit); + Assert.Equal(4999, result.Resources.Core.Remaining); + Assert.Equal(1372700873, result.Resources.Core.ResetAsUtcEpochSeconds); + var expectedReset = DateTimeOffset.ParseExact( + "Mon 01 Jul 2013 5:47:53 PM -00:00", + "ddd dd MMM yyyy h:mm:ss tt zzz", + CultureInfo.InvariantCulture); + Assert.Equal(expectedReset, result.Resources.Core.Reset); + + // Test the search limits + Assert.Equal(30, result.Resources.Search.Limit); + Assert.Equal(18, result.Resources.Search.Remaining); + Assert.Equal(1372700873, result.Resources.Search.ResetAsUtcEpochSeconds); + expectedReset = DateTimeOffset.ParseExact( + "Mon 01 Jul 2013 5:47:53 PM -00:00", + "ddd dd MMM yyyy h:mm:ss tt zzz", + CultureInfo.InvariantCulture); + Assert.Equal(expectedReset, result.Resources.Search.Reset); + + // Test the depreciated rate limits + Assert.Equal(100, result.Rate.Limit); + Assert.Equal(75, result.Rate.Remaining); + Assert.Equal(1372700873, result.Rate.ResetAsUtcEpochSeconds); + expectedReset = DateTimeOffset.ParseExact( + "Mon 01 Jul 2013 5:47:53 PM -00:00", + "ddd dd MMM yyyy h:mm:ss tt zzz", + CultureInfo.InvariantCulture); + Assert.Equal(expectedReset, result.Rate.Reset); + + connection.Received() + .Get(Arg.Is(u => u.ToString() == "rate_limit"), null, null); + } + } + public class TheCtor { [Fact] diff --git a/Octokit/Clients/IMiscellaneousClient.cs b/Octokit/Clients/IMiscellaneousClient.cs index 04153a319a..ef3a1696a1 100644 --- a/Octokit/Clients/IMiscellaneousClient.cs +++ b/Octokit/Clients/IMiscellaneousClient.cs @@ -59,5 +59,13 @@ public interface IMiscellaneousClient /// /// A that includes the license key, text, and attributes of the license. Task GetLicense(string key); + + /// + /// Gets API Rate Limits (API service rather than header info). + /// + /// Thrown when a general API error occurs. + /// An of Rate Limits. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")] + Task GetRateLimits(); } } diff --git a/Octokit/Clients/MiscellaneousClient.cs b/Octokit/Clients/MiscellaneousClient.cs index 05025a3666..100116563d 100644 --- a/Octokit/Clients/MiscellaneousClient.cs +++ b/Octokit/Clients/MiscellaneousClient.cs @@ -116,5 +116,18 @@ public async Task GetLicense(string key) .ConfigureAwait(false); return response.Body; } + + /// + /// Gets API Rate Limits (API service rather than header info). + /// + /// Thrown when a general API error occurs. + /// An of Rate Limits. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")] + public async Task GetRateLimits() + { + var endpoint = new Uri("rate_limit", UriKind.Relative); + var response = await _connection.Get(endpoint, null, null).ConfigureAwait(false); + return response.Body; + } } } \ No newline at end of file diff --git a/Octokit/Helpers/UnixTimeStampExtensions.cs b/Octokit/Helpers/UnixTimeStampExtensions.cs index 397c155fd2..70ec92b32b 100644 --- a/Octokit/Helpers/UnixTimeStampExtensions.cs +++ b/Octokit/Helpers/UnixTimeStampExtensions.cs @@ -18,5 +18,14 @@ public static DateTimeOffset FromUnixTime(this long unixTime) { return new DateTimeOffset(unixTime * TimeSpan.TicksPerSecond + unixEpochTicks, TimeSpan.Zero); } + + /// + /// Convert with UTC offset to a Unix tick + /// + /// Date Time with UTC offset + public static long ToUnixTime(this DateTimeOffset date) + { + return (date.Ticks - unixEpochTicks) / TimeSpan.TicksPerSecond; + } } } diff --git a/Octokit/Http/RateLimit.cs b/Octokit/Http/RateLimit.cs index 44b572da79..6d30371164 100644 --- a/Octokit/Http/RateLimit.cs +++ b/Octokit/Http/RateLimit.cs @@ -2,17 +2,22 @@ using System.Collections.Generic; using System.Runtime.Serialization; using Octokit.Helpers; +using System.Diagnostics; +using System.Globalization; +using Octokit.Internal; namespace Octokit { #if !NETFX_CORE [Serializable] #endif + [DebuggerDisplay("{DebuggerDisplay,nq}")] public class RateLimit #if !NETFX_CORE : ISerializable #endif { + public RateLimit() {} public RateLimit(IDictionary responseHeaders) { @@ -23,6 +28,17 @@ public RateLimit(IDictionary responseHeaders) Reset = GetHeaderValueAsInt32Safe(responseHeaders, "X-RateLimit-Reset").FromUnixTime(); } + public RateLimit(int limit, int remaining, long reset) + { + Ensure.ArgumentNotNull(limit, "limit"); + Ensure.ArgumentNotNull(remaining, "remaining"); + Ensure.ArgumentNotNull(reset, "reset"); + + Limit = limit; + Remaining = remaining; + Reset = reset.FromUnixTime(); + } + /// /// The maximum number of requests that the consumer is permitted to make per hour. /// @@ -36,8 +52,16 @@ public RateLimit(IDictionary responseHeaders) /// /// The date and time at which the current rate limit window resets /// + [ParameterAttribute(Key = "ignoreThisField")] public DateTimeOffset Reset { get; private set; } + /// + /// The date and time at which the current rate limit window resets - in UTC epoch seconds + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + [ParameterAttribute(Key = "reset")] + public long ResetAsUtcEpochSeconds { get { return Reset.ToUnixTime(); } private set { Reset = value.FromUnixTime(); } } + static long GetHeaderValueAsInt32Safe(IDictionary responseHeaders, string key) { string value; @@ -66,5 +90,14 @@ public virtual void GetObjectData(SerializationInfo info, StreamingContext conte info.AddValue("Reset", Reset.Ticks); } #endif + + internal string DebuggerDisplay + { + get + { + return String.Format(CultureInfo.InvariantCulture, "Limit {0}, Remaining {1}, Reset {2} ", Limit, Remaining, Reset); + } + } + } } diff --git a/Octokit/Models/Response/MiscellaneousRateLimit.cs b/Octokit/Models/Response/MiscellaneousRateLimit.cs new file mode 100644 index 0000000000..707e0d5393 --- /dev/null +++ b/Octokit/Models/Response/MiscellaneousRateLimit.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Octokit +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class MiscellaneousRateLimit + { + public MiscellaneousRateLimit() {} + + public MiscellaneousRateLimit(ResourceRateLimit resources, RateLimit rate) + { + Ensure.ArgumentNotNull(resources, "resource"); + Ensure.ArgumentNotNull(rate, "rate"); + + Resources = resources; + Rate = rate; + } + + /// + /// Object of resources rate limits + /// + public ResourceRateLimit Resources { get; private set; } + + /// + /// Legacy rate limit - to be depreciated - https://developer.github.com/v3/rate_limit/#deprecation-notice + /// + public RateLimit Rate { get; private set; } + + internal string DebuggerDisplay + { + get + { + return Resources == null ? "No rates found" : String.Format(CultureInfo.InvariantCulture, Resources.DebuggerDisplay); + } + } + } +} diff --git a/Octokit/Models/Response/ResourceRateLimit.cs b/Octokit/Models/Response/ResourceRateLimit.cs new file mode 100644 index 0000000000..99bd74f137 --- /dev/null +++ b/Octokit/Models/Response/ResourceRateLimit.cs @@ -0,0 +1,42 @@ +using System; +using System.Diagnostics; +using System.Globalization; + +using Octokit.Helpers; + +namespace Octokit +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class ResourceRateLimit + { + public ResourceRateLimit() {} + + public ResourceRateLimit(RateLimit core, RateLimit search) + { + Ensure.ArgumentNotNull(core, "core"); + Ensure.ArgumentNotNull(search, "search"); + + Core = core; + Search = search; + } + + /// + /// Rate limits for core API (rate limit for everything except Search API) + /// + public RateLimit Core { get; private set; } + + /// + /// Rate Limits for Search API + /// + public RateLimit Search { get; private set; } + + internal string DebuggerDisplay + { + get + { + return String.Format(CultureInfo.InvariantCulture, "Core: {0}; Search: {1} ", Core.DebuggerDisplay, Search.DebuggerDisplay); + } + } + } + +} diff --git a/Octokit/Octokit-Mono.csproj b/Octokit/Octokit-Mono.csproj index caef5dfcc5..64daa00563 100644 --- a/Octokit/Octokit-Mono.csproj +++ b/Octokit/Octokit-Mono.csproj @@ -396,6 +396,8 @@ + + diff --git a/Octokit/Octokit-MonoAndroid.csproj b/Octokit/Octokit-MonoAndroid.csproj index 7c7c420b3a..9002587a63 100644 --- a/Octokit/Octokit-MonoAndroid.csproj +++ b/Octokit/Octokit-MonoAndroid.csproj @@ -412,6 +412,8 @@ + + diff --git a/Octokit/Octokit-Monotouch.csproj b/Octokit/Octokit-Monotouch.csproj index 810d8272fb..607ec59518 100644 --- a/Octokit/Octokit-Monotouch.csproj +++ b/Octokit/Octokit-Monotouch.csproj @@ -405,6 +405,8 @@ + + diff --git a/Octokit/Octokit-Portable.csproj b/Octokit/Octokit-Portable.csproj index 428ac9c1ef..74f211dfd1 100644 --- a/Octokit/Octokit-Portable.csproj +++ b/Octokit/Octokit-Portable.csproj @@ -395,6 +395,8 @@ + + diff --git a/Octokit/Octokit-netcore45.csproj b/Octokit/Octokit-netcore45.csproj index 104e805289..944bb68159 100644 --- a/Octokit/Octokit-netcore45.csproj +++ b/Octokit/Octokit-netcore45.csproj @@ -399,6 +399,8 @@ + + diff --git a/Octokit/Octokit.csproj b/Octokit/Octokit.csproj index 3a394da8f8..60a9607f7c 100644 --- a/Octokit/Octokit.csproj +++ b/Octokit/Octokit.csproj @@ -117,8 +117,10 @@ + +