diff --git a/.github/workflows/package-publish.yml b/.github/workflows/package-publish.yml index d6f7bc1..496ba7c 100644 --- a/.github/workflows/package-publish.yml +++ b/.github/workflows/package-publish.yml @@ -32,3 +32,53 @@ jobs: - name: 🚀 Push nuget.org run: dotnet nuget push SpeciesDatabaseApi*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${NUGET_TOKEN} --skip-duplicate + + publish: + name: Publish for ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + include: + - os: ubuntu-latest + artifact_name: SpeciesDatabaseCmd_win-x64_$tag.zip + asset_name: SpeciesDatabaseCmd_linux-x64_$tag + os_identifier: linux-x64 + + - os: windows-latest + artifact_name: SpeciesDatabaseCmd_win-x64_$tag.zip + asset_name: SpeciesDatabaseCmd_win-x64_$tag + os_identifier: win-x64 + + - os: macos-latest + artifact_name: SpeciesDatabaseCmd_macos-x64_$tag.zip + asset_name: SpeciesDatabaseCmd_macos-x64_$tag + os_identifier: macos-x64 + + steps: + - name: 🛒 Checkout + uses: actions/checkout@v3 + - name: Verify commit exists in origin/master + run: | + git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/* + git branch --remote --contains | grep origin/master + + - name: 🟣 Setup .NET 7.0 + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 7.0.x + + - name: 🔧 Build + run: | + dotnet publish SpeciesDatabaseCmd -o ${{ matrix.asset_name }} -c Release -r ${{ matrix.os_identifier }} -p:PublishReadyToRun=true --self-contained + + - name: 📦 Zip + run: | + zip -rq ${{ matrix.asset_name }} . + + - name: 🚀 Upload binaries to release + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: target/release/${{ matrix.artifact_name }} + asset_name: ${{ matrix.asset_name }} + tag: ${{ github.ref }} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 11b5d74..6cacbe9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Change Log +## 12/08/2023 - v1.0.1 + +- Add Marine Regions (`MrClient`) +- Add `bool ThrowExceptionIfRequestStatusCodeFails` properties to clients: + - Gets or sets if it should throw an exception when the request code is other than success. + - If false it will return a null object +- Fix the regex for `WebsiteUrl` to better remove the api sub-domain + ## 11/08/2023 - v1.0.0 - First release \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props index 1e196a5..6e59c6f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,17 +4,17 @@ Tiago Conceição; sn4k3 PTRTECH Copyright 2023-$([System.DateTime]::Now.ToString(`yyyy`)) © PTRTECH - 1.0.0 + 1.0.1 Queries and fetch data from species, taxon and conservation database(s) to retrieve information using the provider API. MIT true https://github.com/sn4k3/SpeciesDatabaseApi https://github.com/sn4k3/SpeciesDatabaseApi - https://github.com/sn4k3/SpeciesDatabaseApi/blob/master/CHANGELOG.md + https://github.com/sn4k3/SpeciesDatabaseApi/releases README.md icon.png - species, taxomy, taxomony, biota, database, api, rest, world register of marine species, wrms, worms, iucn, conservation, nature + species, taxomy, taxomony, biota, database, api, rest, world, register, marine, wrms, worms, iucn, mr, conservation, nature, regions Git enable diff --git a/README.md b/README.md index 52da8c9..b6eb142 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ [![License](https://img.shields.io/github/license/sn4k3/SpeciesDatabaseApi?style=for-the-badge)](https://github.com/sn4k3/SpeciesDatabaseApi/blob/master/LICENSE.txt) [![GitHub repo size](https://img.shields.io/github/repo-size/sn4k3/SpeciesDatabaseApi?style=for-the-badge)](#) [![Code size](https://img.shields.io/github/languages/code-size/sn4k3/SpeciesDatabaseApi?style=for-the-badge)](#) -[![Total code](https://img.shields.io/tokei/lines/github/sn4k3/SpeciesDatabaseApi?style=for-the-badge)](#) [![Nuget](https://img.shields.io/nuget/v/SpeciesDatabaseApi?style=for-the-badge)](https://www.nuget.org/packages/SpeciesDatabaseApi) [![GitHub Sponsors](https://img.shields.io/github/sponsors/sn4k3?color=red&style=for-the-badge)](https://github.com/sponsors/sn4k3) + # Species Database Api @@ -14,7 +14,8 @@ Queries and fetch data from species, taxon and conservation database(s) to retri | Acronym | Name | Class | Terms of use | | --------------------------------------- | ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | | [WoRMS](https://www.marinespecies.org) | World Register of Marine Species | [WormsClient](https://github.com/sn4k3/SpeciesDatabaseApi/blob/master/SpeciesDatabaseApi/Worms/WormsClient.cs) | [Terms of use](https://www.marinespecies.org/about.php#terms) | -| [IUCN](https://www.iucn.org) | International Union for Conservation of Nature | [IucnClient](https://github.com/sn4k3/SpeciesDatabaseApi/blob/master/SpeciesDatabaseApi/Iucn/IucnClient.cs) | [Terms of use](http://apiv3.iucnredlist.org/about) | +| [IUCN](https://www.iucn.org) | International Union for Conservation of Nature | [IucnClient](https://github.com/sn4k3/SpeciesDatabaseApi/blob/master/SpeciesDatabaseApi/Iucn/IucnClient.cs) | [Terms of use](http://apiv3.iucnredlist.org/about) | +| [MR](https://www.iucn.org) | Marine Regions | [MrClient](https://github.com/sn4k3/SpeciesDatabaseApi/blob/master/SpeciesDatabaseApi/Mr/MrClient.cs) | [Terms of use](https://marineregions.org/disclaimer.php) | ## Terms of use @@ -76,7 +77,7 @@ Modified: 15/01/2008 17:27:08 ## Example (IUCN) ```C# - private static readonly IucnClient Client = new IucnClient(); + private static readonly IucnClient Client = new IucnClient("your-api-key"); private async void Main() { @@ -113,8 +114,9 @@ Run the "SpeciesDatabaseCmd.exe" and follow the in-terminal instructions to call # -?, -h, --help Show help and usage information # # Commands: -# WORMS Query - World Register of Marine Species -# IUCN Query - International Union for Conservation of Nature +# WORMS Query - World Register of Marine Species (https://marinespecies.org) +# IUCN Query - International Union for Conservation of Nature (http://iucnredlist.org) +# MR Query - Marine Regions (https://marineregions.org) SpeciesDatabaseCmd.exe IUCN SpecieCommonNames "Carcharodon carcharias" diff --git a/SpeciesDatabaseApi/BaseClient.cs b/SpeciesDatabaseApi/BaseClient.cs index a48625b..a9b0777 100644 --- a/SpeciesDatabaseApi/BaseClient.cs +++ b/SpeciesDatabaseApi/BaseClient.cs @@ -2,7 +2,6 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics; -using System.IO; using System.Linq; using System.Net; using System.Net.Http; @@ -23,12 +22,12 @@ namespace SpeciesDatabaseApi; public abstract class BaseClient : BindableBase, IDisposable { - #region Constants + #region Constants - private static readonly Lazy HttpClientShared = new(() => - { - var httpClient = new HttpClient(); - httpClient.DefaultRequestHeaders.Add("User-Agent", AboutLibrary.SoftwareWithVersion); + private static readonly Lazy HttpClientShared = new(() => + { + var httpClient = new HttpClient(); + httpClient.DefaultRequestHeaders.Add("User-Agent", AboutLibrary.SoftwareWithVersion); return httpClient; }); @@ -110,7 +109,7 @@ public virtual string WebsiteUrl get { var domain = ApiAddress.GetLeftPart(UriPartial.Authority); - return Regex.Replace(domain, @"\/\/(.*api|www)[.]", "//"); + return Regex.Replace(domain, @"\/\/(.*api([a-zA-Z0-9_-]+)?|www)[.]", "//"); } } @@ -159,6 +158,12 @@ public bool AutoWaitForRequestLimit /// public ProductInfoHeaderValue? ProductInfoHeader { get; set; } + /// + /// Gets or sets if it should throw an exception when the request code is other than success.
+ /// If false it will return a null object + ///
+ public bool ThrowExceptionIfRequestStatusCodeFails { get; set; } = true; + #endregion #region Constructors @@ -220,7 +225,7 @@ public static string UrlEncode(string query) /// /// /// - public string GetRawRequestUrl(string path) => $"{ApiAddress}/{path.Trim(' ', '/')}"; + public string GetRawRequestUrl(string path) => $"{ApiAddress}/{path.TrimStart(' ', '/').TrimEnd()}"; public string GetRequestUrl(string path) { @@ -239,7 +244,7 @@ public string GetRequestUrl(string path) /// Partial path without Api address /// Parameters to send with the request /// Absolute path for the request - public string GetRequestUrl(string path, IEnumerable> parameters) + public string GetRequestUrl(string path, IEnumerable> parameters) { return $"{GetRawRequestUrl(path)}{GetUrlParametersString(parameters)}"; } @@ -250,7 +255,7 @@ public string GetRequestUrl(string path, IEnumerablePartial path without Api address /// Parameters to send with the request /// Absolute path for the request - public string GetRequestUrl(string path, IReadOnlyDictionary parameters) => GetRequestUrl(path, parameters.AsEnumerable()); + public string GetRequestUrl(string path, IReadOnlyDictionary parameters) => GetRequestUrl(path, parameters.AsEnumerable()); /// /// Gets the absolute url for an request to the Api @@ -258,7 +263,7 @@ public string GetRequestUrl(string path, IEnumerablePartial path without Api address /// Parameter to send with the request /// Absolute path for the request - public string GetRequestUrl(string path, KeyValuePair parameter) => GetRequestUrl(path, new[] { parameter }); + public string GetRequestUrl(string path, KeyValuePair parameter) => GetRequestUrl(path, new[] { parameter }); /// /// Gets the absolute url for an request to the Api @@ -276,14 +281,42 @@ public string GetRequestUrl(string path, object classObj) /// /// /// The formatted string - public string GetUrlParametersString(IEnumerable> parameters) + public string GetUrlParametersString(IEnumerable> parameters) { var builder = new StringBuilder(); // Sort the parameters alphabetically to avoid http redirection. foreach (var item in parameters.OrderBy(x => x.Key)) { - builder.Append($"{(builder.Length == 0 ? "?" : " & ")}{UrlEncode(item.Key.ToLowerInvariant())}={UrlEncode(item.Value.ToString()?.ToLowerInvariant() ?? string.Empty)}"); + string? value; + switch (item.Value) + { + case null: + continue; + case IList list: + if (list.Count == 0) continue; + + var items = new List(list.Count); + foreach (var obj in list) + { + if (obj is null) continue; + value = obj.ToString()?.Trim().ToLowerInvariant(); + if (string.IsNullOrWhiteSpace(value)) continue; + items.Add(value); + } + value = string.Join(',', items); + break; + case string str: + value = str.Trim().ToLowerInvariant(); + if (string.IsNullOrWhiteSpace(value)) continue; + break; + default: + value = item.Value.ToString()?.Trim().ToLowerInvariant(); + if (string.IsNullOrWhiteSpace(value)) continue; + break; + } + + builder.Append($"{(builder.Length == 0 ? "?" : " & ")}{UrlEncode(item.Key.ToLowerInvariant())}={UrlEncode(value)}"); } if (ApiToken is { CanUse: true, Placement: ApiTokenPlacement.Get }) @@ -299,14 +332,14 @@ public string GetUrlParametersString(IEnumerable> p /// /// /// The formatted string - public string GetUrlParametersString(IReadOnlyDictionary parameters) => GetUrlParametersString(parameters.AsEnumerable()); + public string GetUrlParametersString(IReadOnlyDictionary parameters) => GetUrlParametersString(parameters.AsEnumerable()); /// /// Gets the parameters string from a dictionary of key and values parameters /// /// /// The formatted string - public string GetUrlParametersString(KeyValuePair keyValue) => GetUrlParametersString(new[]{ keyValue }); + public string GetUrlParametersString(KeyValuePair keyValue) => GetUrlParametersString(new[]{ keyValue }); /// /// Gets the parameters string from a class (Reflection) @@ -315,7 +348,7 @@ public string GetUrlParametersString(IEnumerable> p /// The formatted string public string GetUrlParametersString(object? obj) { - var dict = new Dictionary(); + var dict = new Dictionary(); if (obj is not null) { @@ -327,23 +360,6 @@ public string GetUrlParametersString(object? obj) if (method is null) continue; var value = propertyInfo.GetValue(obj); - switch (value) - { - case null: - continue; - case IList list: - if (list.Count == 0) continue; - - var items = new List(list.Count); - foreach (var item in list) - { - if (item is null) continue; - items.Add(item.ToString()!); - } - value = string.Join(',', items); - break; - } - var attr = propertyInfo.GetCustomAttribute(); dict.Add(attr?.Name ?? method.Name, value); } @@ -376,7 +392,7 @@ protected HttpRequestMessage PrepareJsonHttpRequestMessage(string requestUrl, Ht return request; } - public Task PostJsonAsync(string requestUrl, object postData, IEnumerable> urlParameters, CancellationToken cancellationToken = default) + public Task PostJsonAsync(string requestUrl, object postData, IEnumerable> urlParameters, CancellationToken cancellationToken = default) { using var request = PrepareJsonHttpRequestMessage(GetRequestUrl(requestUrl, urlParameters), HttpMethod.Post); @@ -386,7 +402,7 @@ protected HttpRequestMessage PrepareJsonHttpRequestMessage(string requestUrl, Ht return SendRequestAsync(request, cancellationToken); } - public Task PostJsonAsync(string requestUrl, object postData, IReadOnlyDictionary urlParameters, CancellationToken cancellationToken = default) + public Task PostJsonAsync(string requestUrl, object postData, IReadOnlyDictionary urlParameters, CancellationToken cancellationToken = default) { using var request = PrepareJsonHttpRequestMessage(GetRequestUrl(requestUrl, urlParameters), HttpMethod.Post); @@ -396,7 +412,7 @@ protected HttpRequestMessage PrepareJsonHttpRequestMessage(string requestUrl, Ht return SendRequestAsync(request, cancellationToken); } - public Task PostJsonAsync(string requestUrl, object postData, KeyValuePair urlParameter, CancellationToken cancellationToken = default) + public Task PostJsonAsync(string requestUrl, object postData, KeyValuePair urlParameter, CancellationToken cancellationToken = default) { using var request = PrepareJsonHttpRequestMessage(GetRequestUrl(requestUrl, urlParameter), HttpMethod.Post); @@ -426,19 +442,19 @@ protected HttpRequestMessage PrepareJsonHttpRequestMessage(string requestUrl, Ht return SendRequestAsync(request, cancellationToken); } - public Task GetJsonAsync(string requestUrl, IEnumerable> urlParameters, CancellationToken cancellationToken = default) + public Task GetJsonAsync(string requestUrl, IEnumerable> urlParameters, CancellationToken cancellationToken = default) { using var request = PrepareJsonHttpRequestMessage(GetRequestUrl(requestUrl, urlParameters), HttpMethod.Get); return SendRequestAsync(request, cancellationToken); } - public Task GetJsonAsync(string requestUrl, IReadOnlyDictionary urlParameters, CancellationToken cancellationToken = default) + public Task GetJsonAsync(string requestUrl, IReadOnlyDictionary urlParameters, CancellationToken cancellationToken = default) { using var request = PrepareJsonHttpRequestMessage(GetRequestUrl(requestUrl, urlParameters), HttpMethod.Get); return SendRequestAsync(request, cancellationToken); } - public Task GetJsonAsync(string requestUrl, KeyValuePair urlParameter, CancellationToken cancellationToken = default) + public Task GetJsonAsync(string requestUrl, KeyValuePair urlParameter, CancellationToken cancellationToken = default) { using var request = PrepareJsonHttpRequestMessage(GetRequestUrl(requestUrl, urlParameter), HttpMethod.Get); return SendRequestAsync(request, cancellationToken); @@ -456,19 +472,19 @@ protected HttpRequestMessage PrepareJsonHttpRequestMessage(string requestUrl, Ht return SendRequestAsync(request, cancellationToken); } - public Task DeleteJsonAsync(string requestUrl, IEnumerable> urlParameters, CancellationToken cancellationToken = default) + public Task DeleteJsonAsync(string requestUrl, IEnumerable> urlParameters, CancellationToken cancellationToken = default) { using var request = PrepareJsonHttpRequestMessage(GetRequestUrl(requestUrl, urlParameters), HttpMethod.Delete); return SendRequestAsync(request, cancellationToken); } - public Task DeleteJsonAsync(string requestUrl, IReadOnlyDictionary urlParameters, CancellationToken cancellationToken = default) + public Task DeleteJsonAsync(string requestUrl, IReadOnlyDictionary urlParameters, CancellationToken cancellationToken = default) { using var request = PrepareJsonHttpRequestMessage(GetRequestUrl(requestUrl, urlParameters), HttpMethod.Delete); return SendRequestAsync(request, cancellationToken); } - public Task DeleteJsonAsync(string requestUrl, KeyValuePair urlParameter, CancellationToken cancellationToken = default) + public Task DeleteJsonAsync(string requestUrl, KeyValuePair urlParameter, CancellationToken cancellationToken = default) { using var request = PrepareJsonHttpRequestMessage(GetRequestUrl(requestUrl, urlParameter), HttpMethod.Delete); return SendRequestAsync(request, cancellationToken); @@ -520,26 +536,28 @@ protected virtual Task OnBeforeSendRequestAsync(HttpRequestMessage request, Canc await Task.Delay(waitTime, cancellationToken).ConfigureAwait(false); } while (RequestsHitLimit); } - - using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); - Interlocked.Increment(ref _totalRequests); - response.EnsureSuccessStatusCode(); + using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); - switch (response.StatusCode) - { - case HttpStatusCode.NoContent: - return default; - } + Interlocked.Increment(ref _totalRequests); - if (_autoWaitForRequestLimit && _maximumRequestsPerSecond > 0) - { - Interlocked.Increment(ref _requestsInCurrentSecond); - _resetRequestsTimer.Start(); - } - - return await response.Content.ReadFromJsonAsync(DefaultJsonSerializerOptions, cancellationToken).ConfigureAwait(false); - } + if (ThrowExceptionIfRequestStatusCodeFails) response.EnsureSuccessStatusCode(); + else if (!response.IsSuccessStatusCode) return default; + + switch (response.StatusCode) + { + case HttpStatusCode.NoContent: + return default; + } + + if (_autoWaitForRequestLimit && _maximumRequestsPerSecond > 0) + { + Interlocked.Increment(ref _requestsInCurrentSecond); + _resetRequestsTimer.Start(); + } + + return await response.Content.ReadFromJsonAsync(DefaultJsonSerializerOptions, cancellationToken).ConfigureAwait(false); + } #endregion } \ No newline at end of file diff --git a/SpeciesDatabaseApi/Mr/Enumerations.cs b/SpeciesDatabaseApi/Mr/Enumerations.cs new file mode 100644 index 0000000..fb4bdf3 --- /dev/null +++ b/SpeciesDatabaseApi/Mr/Enumerations.cs @@ -0,0 +1,19 @@ +namespace SpeciesDatabaseApi.Mr; + +public enum MrRelationDirection +{ + Upper, + Lower, + Both +} + +public enum MrRelationType +{ + PartOf, + PartlyPartOf, + AdjacentTo, + SimilarTo, + AdministrativePartOf, + InfluencedBy, + All +} diff --git a/SpeciesDatabaseApi/Mr/GazetteerRecord.cs b/SpeciesDatabaseApi/Mr/GazetteerRecord.cs new file mode 100644 index 0000000..650a8e8 --- /dev/null +++ b/SpeciesDatabaseApi/Mr/GazetteerRecord.cs @@ -0,0 +1,104 @@ +using System; +using System.Text.Json.Serialization; + +namespace SpeciesDatabaseApi.Mr; + +public class GazetteerRecord : IEquatable +{ + [JsonPropertyName("MRGID")] + public int MrgId { get; set; } + + [JsonPropertyName("gazetteerSource")] + public string GazetteerSource { get; set; } = string.Empty; + + [JsonPropertyName("placeType")] + public string PlaceType { get; set; } = string.Empty; + + [JsonPropertyName("latitude")] + public decimal? Latitude { get; set; } = null; + + [JsonPropertyName("longitude")] + public decimal? Longitude { get; set; } = null; + + [JsonPropertyName("minLatitude")] + public decimal? MinLatitude { get; set; } = null; + + [JsonPropertyName("minLongitude")] + public decimal? MinLongitude { get; set; } = null; + + [JsonPropertyName("maxLatitude")] + public decimal? MaxLatitude { get; set; } = null; + + [JsonPropertyName("maxLongitude")] + public decimal? MaxLongitude { get; set; } = null; + + [JsonPropertyName("precision")] + public decimal? Precision { get; set; } = null; + + [JsonPropertyName("preferredGazetteerName")] + public string PreferredGazetteerName { get; set; } = string.Empty; + + [JsonPropertyName("preferredGazetteerNameLang")] + public string PreferredGazetteerNameLang { get; set; } = string.Empty; + + [JsonPropertyName("status")] + public string Status { get; set; } = string.Empty; + + [JsonPropertyName("accepted")] + public int Accepted { get; set; } + + + /// + public override string ToString() + { + return $"{nameof(MrgId)}: {MrgId}, {nameof(GazetteerSource)}: {GazetteerSource}, {nameof(PlaceType)}: {PlaceType}, {nameof(Latitude)}: {Latitude}, {nameof(Longitude)}: {Longitude}, {nameof(MinLatitude)}: {MinLatitude}, {nameof(MinLongitude)}: {MinLongitude}, {nameof(MaxLatitude)}: {MaxLatitude}, {nameof(MaxLongitude)}: {MaxLongitude}, {nameof(Precision)}: {Precision}, {nameof(PreferredGazetteerName)}: {PreferredGazetteerName}, {nameof(PreferredGazetteerNameLang)}: {PreferredGazetteerNameLang}, {nameof(Status)}: {Status}, {nameof(Accepted)}: {Accepted}"; + } + + /// + public bool Equals(GazetteerRecord? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return MrgId == other.MrgId && GazetteerSource == other.GazetteerSource && PlaceType == other.PlaceType && Latitude == other.Latitude && Longitude == other.Longitude && MinLatitude == other.MinLatitude && MinLongitude == other.MinLongitude && MaxLatitude == other.MaxLatitude && MaxLongitude == other.MaxLongitude && Precision == other.Precision && PreferredGazetteerName == other.PreferredGazetteerName && PreferredGazetteerNameLang == other.PreferredGazetteerNameLang && Status == other.Status && Accepted == other.Accepted; + } + + /// + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((GazetteerRecord)obj); + } + + /// + public override int GetHashCode() + { + var hashCode = new HashCode(); + hashCode.Add(MrgId); + hashCode.Add(GazetteerSource); + hashCode.Add(PlaceType); + hashCode.Add(Latitude); + hashCode.Add(Longitude); + hashCode.Add(MinLatitude); + hashCode.Add(MinLongitude); + hashCode.Add(MaxLatitude); + hashCode.Add(MaxLongitude); + hashCode.Add(Precision); + hashCode.Add(PreferredGazetteerName); + hashCode.Add(PreferredGazetteerNameLang); + hashCode.Add(Status); + hashCode.Add(Accepted); + return hashCode.ToHashCode(); + } + + public static bool operator ==(GazetteerRecord? left, GazetteerRecord? right) + { + return Equals(left, right); + } + + public static bool operator !=(GazetteerRecord? left, GazetteerRecord? right) + { + return !Equals(left, right); + } +} \ No newline at end of file diff --git a/SpeciesDatabaseApi/Mr/GazetteerSource.cs b/SpeciesDatabaseApi/Mr/GazetteerSource.cs new file mode 100644 index 0000000..b5e5ed2 --- /dev/null +++ b/SpeciesDatabaseApi/Mr/GazetteerSource.cs @@ -0,0 +1,58 @@ +using System; +using System.Text.Json.Serialization; +using System.Xml.Serialization; + +namespace SpeciesDatabaseApi.Mr; + +public class GazetteerSource : IEquatable +{ + [JsonPropertyName("sourceId")] + [XmlElement("sourceId")] + public int SourceId { get; set; } + + [JsonPropertyName("source")] + [XmlElement("source")] + public string Source { get; set; } = string.Empty; + + [JsonPropertyName("sourceURL")] + [XmlElement("sourceURL")] + public Uri? SourceUrl { get; set; } + + /// + public override string ToString() + { + return $"{nameof(SourceId)}: {SourceId}, {nameof(Source)}: {Source}, {nameof(SourceUrl)}: {SourceUrl}"; + } + + public bool Equals(GazetteerSource? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return SourceId == other.SourceId && Source == other.Source && Equals(SourceUrl, other.SourceUrl); + } + + /// + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((GazetteerSource)obj); + } + + /// + public override int GetHashCode() + { + return HashCode.Combine(SourceId, Source, SourceUrl); + } + + public static bool operator ==(GazetteerSource? left, GazetteerSource? right) + { + return Equals(left, right); + } + + public static bool operator !=(GazetteerSource? left, GazetteerSource? right) + { + return !Equals(left, right); + } +} \ No newline at end of file diff --git a/SpeciesDatabaseApi/Mr/GazetteerSourceName.cs b/SpeciesDatabaseApi/Mr/GazetteerSourceName.cs new file mode 100644 index 0000000..0c364f1 --- /dev/null +++ b/SpeciesDatabaseApi/Mr/GazetteerSourceName.cs @@ -0,0 +1,54 @@ +using System; +using System.Text.Json.Serialization; +using System.Xml.Serialization; + +namespace SpeciesDatabaseApi.Mr; + +public class GazetteerSourceName : IEquatable +{ + [JsonPropertyName("source")] + [XmlElement("source")] + public string Source { get; set; } = string.Empty; + + [JsonPropertyName("url")] + [XmlElement("url")] + public Uri? Url { get; set; } + + /// + public override string ToString() + { + return $"{nameof(Source)}: {Source}, {nameof(Url)}: {Url}"; + } + + public bool Equals(GazetteerSourceName? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Source == other.Source && Equals(Url, other.Url); + } + + /// + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((GazetteerSourceName)obj); + } + + /// + public override int GetHashCode() + { + return HashCode.Combine(Source, Url); + } + + public static bool operator ==(GazetteerSourceName? left, GazetteerSourceName? right) + { + return Equals(left, right); + } + + public static bool operator !=(GazetteerSourceName? left, GazetteerSourceName? right) + { + return !Equals(left, right); + } +} \ No newline at end of file diff --git a/SpeciesDatabaseApi/Mr/GazetteerType.cs b/SpeciesDatabaseApi/Mr/GazetteerType.cs new file mode 100644 index 0000000..6a2e29b --- /dev/null +++ b/SpeciesDatabaseApi/Mr/GazetteerType.cs @@ -0,0 +1,54 @@ +using System; +using System.Text.Json.Serialization; + +namespace SpeciesDatabaseApi.Mr; + +public class GazetteerType : IEquatable +{ + [JsonPropertyName("typeID")] + public int TypeId { get; set; } + + [JsonPropertyName("type")] + public string Type { get; set; } = string.Empty; + + [JsonPropertyName("description")] + public string? Description { get; set; } + + /// + public override string ToString() + { + return $"{nameof(TypeId)}: {TypeId}, {nameof(Type)}: {Type}, {nameof(Description)}: {Description}"; + } + + public bool Equals(GazetteerType? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return TypeId == other.TypeId && Type == other.Type && Description == other.Description; + } + + /// + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((GazetteerType)obj); + } + + /// + public override int GetHashCode() + { + return HashCode.Combine(TypeId, Type, Description); + } + + public static bool operator ==(GazetteerType? left, GazetteerType? right) + { + return Equals(left, right); + } + + public static bool operator !=(GazetteerType? left, GazetteerType? right) + { + return !Equals(left, right); + } +} \ No newline at end of file diff --git a/SpeciesDatabaseApi/Mr/GazetteerWms.cs b/SpeciesDatabaseApi/Mr/GazetteerWms.cs new file mode 100644 index 0000000..4a3a8b9 --- /dev/null +++ b/SpeciesDatabaseApi/Mr/GazetteerWms.cs @@ -0,0 +1,70 @@ +using System; +using System.Text.Json.Serialization; +using System.Xml.Serialization; + +namespace SpeciesDatabaseApi.Mr; + +public class GazetteerWms : IEquatable +{ + [JsonPropertyName("value")] + [XmlElement("value")] + public string Value { get; set; } = string.Empty; + + [JsonPropertyName("MRGID")] + [XmlElement("MRGID")] + public int MrgId { get; set; } + + [JsonPropertyName("url")] + [XmlElement("url")] + public Uri? Url { get; set; } + + [JsonPropertyName("namespace")] + [XmlElement("namespace")] + public string Namespace { get; set; } = string.Empty; + + [JsonPropertyName("featureType")] + [XmlElement("featureType")] + public string FeatureType { get; set; } = string.Empty; + + [JsonPropertyName("featureName")] + [XmlElement("featureName")] + public string FeatureName { get; set; } = string.Empty; + + /// + public override string ToString() + { + return $"{nameof(Value)}: {Value}, {nameof(MrgId)}: {MrgId}, {nameof(Url)}: {Url}, {nameof(Namespace)}: {Namespace}, {nameof(FeatureType)}: {FeatureType}, {nameof(FeatureName)}: {FeatureName}"; + } + + public bool Equals(GazetteerWms? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Value == other.Value && MrgId == other.MrgId && Equals(Url, other.Url) && Namespace == other.Namespace && FeatureType == other.FeatureType && FeatureName == other.FeatureName; + } + + /// + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((GazetteerWms)obj); + } + + /// + public override int GetHashCode() + { + return HashCode.Combine(Value, MrgId, Url, Namespace, FeatureType, FeatureName); + } + + public static bool operator ==(GazetteerWms? left, GazetteerWms? right) + { + return Equals(left, right); + } + + public static bool operator !=(GazetteerWms? left, GazetteerWms? right) + { + return !Equals(left, right); + } +} \ No newline at end of file diff --git a/SpeciesDatabaseApi/Mr/MrClient.cs b/SpeciesDatabaseApi/Mr/MrClient.cs new file mode 100644 index 0000000..179351c --- /dev/null +++ b/SpeciesDatabaseApi/Mr/MrClient.cs @@ -0,0 +1,275 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Net.Http; +using System.Runtime.ConstrainedExecution; +using System.Text; +using System.Text.Json.Nodes; +using System.Threading; +using System.Threading.Tasks; + +namespace SpeciesDatabaseApi.Mr; + +/// +/// The client for https://marineregions.org API +/// +public class MrClient : BaseClient +{ + #region Static objects + /// + /// The client full name/provider + /// + public const string FullName = "Marine Regions"; + + /// + /// The Api default address + /// + public static readonly Uri DefaultApiAddress = new("https://marineregions.org/rest"); + #endregion + + #region Properties + /// + public override int Version => 1; + + /// + public override string ClientFullName => FullName; + + #endregion + + #region Constructor + + public MrClient(HttpClient? httpClient = null) : base(DefaultApiAddress, httpClient) + { + } + + #endregion + + #region Methods + /// + /// Gets one record for the given MRGID + /// + /// The MRGID to search for + /// + /// + public Task GetGazetteerRecord(int mrgId, CancellationToken token = default) + { + return GetJsonAsync($"getGazetteerRecordByMRGID.json/{mrgId}/", token); + } + + /// + /// Gets one record for the given MRGID + /// + /// The MRGID to search for + /// + /// + public Task GetFullGazetteerRecord(int mrgId, CancellationToken token = default) + { + return GetJsonAsync($"getGazetteerRecordByMRGID.jsonld/{mrgId}/", token); + } + + /// + /// Gets all geometries associated with a gazetteer record + /// + /// The MRGID to search for + /// + /// + public Task GetGazetteerGeometries(int mrgId, CancellationToken token = default) + { + return GetJsonAsync($"getGazetteerGeometries.jsonld/{mrgId}/", token); + } + + /// + /// Gets all possible types + /// + /// + /// + public Task GetGazetteerTypes(CancellationToken token = default) + { + return GetJsonAsync("getGazetteerTypes.json/", token); + } + + /// + /// Gets a list of the first 100 matching records for given name + /// + /// The name to search for + /// Adds a '%'-sign before and after the GazetteerName (SQL LIKE function). Default=true + /// Uses Levenshtein query to find nearest matches. Default=false + /// One or more placetypeIDs. See to retrieve a list of placetypeIDs. Default=(empty) + /// Language (ISO 639-1 code). Default=(empty) + /// Start record number, in order to page through next batch of results. Default=0 + /// Number of records to retrieve. Default=100; max=100 + /// + /// + public Task GetGazetteerRecords(string name, bool like = true, bool fuzzy = false, IEnumerable? typeIds = null, string? language = null, int offset = 0, int count = 100, CancellationToken token = default) + { + var parameters = new Dictionary + { + {"like", like}, + {"fuzzy", fuzzy}, + {"typeID", typeIds}, + {"language", language}, + {"offset", offset}, + {"count", count}, + }; + return GetJsonAsync($"getGazetteerRecordsByName.json/{name}/", parameters, token); + } + + /// + /// Gets a list of the first 100 matching records for all given names + /// + /// The name to search for + /// Adds a '%'-sign before and after the GazetteerName (SQL LIKE function). Default=true + /// Uses Levenshtein query to find nearest matches. Default=false + /// + /// + public Task?> GetGazetteerRecords(IEnumerable names, bool like = true, bool fuzzy = false, CancellationToken token = default) + { + var namesPath = new StringBuilder(); + foreach (var name in names) + { + if(string.IsNullOrWhiteSpace(name)) continue; + namesPath.Append($"{name}/"); + } + + if (namesPath.Length == 0) throw new ArgumentException("It must contain at least one name", nameof(names)); + + return GetJsonAsync>($"getGazetteerRecordsByNames.json/{like.ToString().ToLowerInvariant()}/{fuzzy.ToString().ToLowerInvariant()}/{namesPath}", token); + } + + /// + /// Gets the Linked Data Event Stream feed + /// + /// Feed start range date time + /// Feed end range date time + /// + /// + public Task GetFeed(DateTime startDateTime, DateTime endDateTime, CancellationToken token = default) + { + // page=2023-07-31T15%3A00%3A00Z%2F2023-07-31T16%3A00%3A00Z + // page=2023-07-31T15:00:00Z/2023-07-31T16:00:00Z + // 2023-07-11T22:05:53.8948950+01:00/2023-08-11T22:05:53.8962167+01:00 + // 2023-07-11T21:06:43.0750477Z/2023-08-11T21:06:43.0763630Z + var parameters = new KeyValuePair("page", $"{startDateTime.ToUniversalTime():O}/{endDateTime.ToUniversalTime():O}"); + return GetJsonAsync("getFeed.jsonld", parameters, token); + } + + /// + /// Gets the Linked Data Event Stream feed + /// + /// + /// + public Task GetFeed(CancellationToken token = default) + { + return GetJsonAsync("getFeed.jsonld", token); + + } + + /// + /// Gets WMS information for the given MRGID + /// + /// The MRGID to search for + /// + /// + public Task GetgetGazetteerWmSes(int mrgId, CancellationToken token = default) + { + return GetJsonAsync($"getGazetteerWMSes.json/{mrgId}/", token); + } + + /// + /// Gets the first 100 related records for the given MRGID + /// + /// The MRGID to search for + /// + /// + /// + /// + public Task GetGazetteerRelations(int mrgId, MrRelationDirection direction = MrRelationDirection.Upper, MrRelationType type = MrRelationType.PartOf, CancellationToken token = default) + { + var parameters = new Dictionary + { + {"direction", direction}, + {"type", type}, + }; + return GetJsonAsync($"getGazetteerRelationsByMRGID.json/{mrgId}/", parameters, token); + } + + /// + /// Gets all sources per batch of 100. + /// + /// Provide offset to return next batch of 100 sources. + /// + /// + public Task GetGazetteerSources(int offset = 0, CancellationToken token = default) + { + var parameters = new KeyValuePair("offset", offset); + return GetJsonAsync("getGazetteerSources.json/", parameters, token); + } + + /// + /// Gets the source name corresponding to a source ID + /// + /// + /// + /// + public Task GetGazetteerSource(int sourceId, CancellationToken token = default) + { + return GetJsonAsync($"getGazetteerSourceBySourceID.json/{sourceId}/", token); + } + + /// + /// Gets the first 100 names for the given MRGID + /// + /// The MRGID to search for + /// + /// + public Task GetGazetteerNames(int mrgId, CancellationToken token = default) + { + return GetJsonAsync($"getGazetteerNamesByMRGID.json/{mrgId}/", token); + } + + /// + /// Gets the first 100 records for the given source + /// + /// The source name to search for + /// + /// + public Task GetGazetteerRecordsBySource(string sourceName, CancellationToken token = default) + { + return GetJsonAsync($"getGazetteerRecordsBySource.json/{EscapeDataString(sourceName)}/", token); + } + + /// + /// Gets all records for the given type, per batch of 100 + /// + /// Use to view a complete list of types + /// Provide offset to return next batch of 100 records. + /// + /// + public Task GetGazetteerRecordsByType(string type, int offset = 0, CancellationToken token = default) + { + var parameters = new KeyValuePair("offset", offset); + return GetJsonAsync($"getGazetteerRecordsByType.json/{EscapeDataString(type)}/", parameters, token); + } + + /// + /// Gets all gazetteer records where the geometry intersects with the given latitude and longitude per batch of 100. Results are sorted by area, smallest to largest + /// + /// A decimal number which ranges from -90 to 90 + /// A decimal number which ranges from -180 to +180 + /// One or more placetypeIDs. See to retrieve a list of placetypeIDs. Default=(empty) + /// Start record number, in order to page through next batch of results. Default=0 + /// + /// + public Task GetGazetteerRecordsByLatLong(decimal latitude, decimal longitude, IEnumerable? typeIds = null, int offset = 0, CancellationToken token = default) + { + if (latitude is < -90 or > 90) throw new ArgumentOutOfRangeException(nameof(latitude), "Latitude must be between -90 and 90"); + if (longitude is < -180 or > 180) throw new ArgumentOutOfRangeException(nameof(longitude), "Longitude must be between -90 and 90"); + var parameters = new Dictionary + { + {"typeID", typeIds}, + {"offset", offset}, + }; + return GetJsonAsync($"getGazetteerRecordsByLatLong.json/{latitude}/{longitude}/", parameters, token); + } + #endregion +} \ No newline at end of file diff --git a/SpeciesDatabaseApi/SpeciesDatabaseApi.csproj b/SpeciesDatabaseApi/SpeciesDatabaseApi.csproj index c632161..2ef1a36 100644 --- a/SpeciesDatabaseApi/SpeciesDatabaseApi.csproj +++ b/SpeciesDatabaseApi/SpeciesDatabaseApi.csproj @@ -1,3 +1 @@ - - - + diff --git a/SpeciesDatabaseApi/Worms/AphiaRecord.cs b/SpeciesDatabaseApi/Worms/AphiaRecord.cs index e8b5080..c0a987f 100644 --- a/SpeciesDatabaseApi/Worms/AphiaRecord.cs +++ b/SpeciesDatabaseApi/Worms/AphiaRecord.cs @@ -62,14 +62,14 @@ public class AphiaRecord : IEquatable /// [JsonPropertyName("unacceptreason")] [XmlElement("unacceptreason")] - public string? UnacceptReason { get; set; } + public string? UnAcceptReason { get; set; } /// /// The AphiaID (for the ) of the currently accepted taxon. NULL if there is no currently accepted taxon. /// [JsonPropertyName("valid_AphiaID")] [XmlElement("valid_AphiaID")] - public int? ValidAphiaID { get; set; } + public int? ValidAphiaId { get; set; } /// /// The of the currently accepted taxon @@ -146,7 +146,7 @@ public class AphiaRecord : IEquatable /// [JsonPropertyName("lsid")] [XmlElement("lsid")] - public string lsId { get; set; } = string.Empty; + public string LsId { get; set; } = string.Empty; /// /// A flag indicating whether the taxon is a marine organism, i.e. can be found in/above sea water. Possible values: 0/1/NULL @@ -201,14 +201,14 @@ public class AphiaRecord : IEquatable /// public override string ToString() { - return $"{nameof(AphiaId)}: {AphiaId}, {nameof(Url)}: {Url}, {nameof(ScientificName)}: {ScientificName}, {nameof(Authority)}: {Authority}, {nameof(TaxonRankId)}: {TaxonRankId}, {nameof(Rank)}: {Rank}, {nameof(Status)}: {Status}, {nameof(UnacceptReason)}: {UnacceptReason}, {nameof(ValidAphiaID)}: {ValidAphiaID}, {nameof(ValidName)}: {ValidName}, {nameof(ValidAuthority)}: {ValidAuthority}, {nameof(ParentNameUsageId)}: {ParentNameUsageId}, {nameof(Kingdom)}: {Kingdom}, {nameof(Phylum)}: {Phylum}, {nameof(Class)}: {Class}, {nameof(Order)}: {Order}, {nameof(Family)}: {Family}, {nameof(Genus)}: {Genus}, {nameof(Citation)}: {Citation}, {nameof(lsId)}: {lsId}, {nameof(IsMarine)}: {IsMarine}, {nameof(IsBrackish)}: {IsBrackish}, {nameof(IsFreshwater)}: {IsFreshwater}, {nameof(IsTerrestrial)}: {IsTerrestrial}, {nameof(IsExtinct)}: {IsExtinct}, {nameof(MatchType)}: {MatchType}, {nameof(Modified)}: {Modified}"; + return $"{nameof(AphiaId)}: {AphiaId}, {nameof(Url)}: {Url}, {nameof(ScientificName)}: {ScientificName}, {nameof(Authority)}: {Authority}, {nameof(TaxonRankId)}: {TaxonRankId}, {nameof(Rank)}: {Rank}, {nameof(Status)}: {Status}, {nameof(UnAcceptReason)}: {UnAcceptReason}, {nameof(ValidAphiaId)}: {ValidAphiaId}, {nameof(ValidName)}: {ValidName}, {nameof(ValidAuthority)}: {ValidAuthority}, {nameof(ParentNameUsageId)}: {ParentNameUsageId}, {nameof(Kingdom)}: {Kingdom}, {nameof(Phylum)}: {Phylum}, {nameof(Class)}: {Class}, {nameof(Order)}: {Order}, {nameof(Family)}: {Family}, {nameof(Genus)}: {Genus}, {nameof(Citation)}: {Citation}, {nameof(LsId)}: {LsId}, {nameof(IsMarine)}: {IsMarine}, {nameof(IsBrackish)}: {IsBrackish}, {nameof(IsFreshwater)}: {IsFreshwater}, {nameof(IsTerrestrial)}: {IsTerrestrial}, {nameof(IsExtinct)}: {IsExtinct}, {nameof(MatchType)}: {MatchType}, {nameof(Modified)}: {Modified}"; } public bool Equals(AphiaRecord? other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; - return AphiaId == other.AphiaId && Url.Equals(other.Url) && ScientificName == other.ScientificName && Authority == other.Authority && TaxonRankId == other.TaxonRankId && Rank == other.Rank && Status == other.Status && UnacceptReason == other.UnacceptReason && ValidAphiaID == other.ValidAphiaID && ValidName == other.ValidName && ValidAuthority == other.ValidAuthority && ParentNameUsageId == other.ParentNameUsageId && Kingdom == other.Kingdom && Phylum == other.Phylum && Class == other.Class && Order == other.Order && Family == other.Family && Genus == other.Genus && Citation == other.Citation && lsId == other.lsId && IsMarine == other.IsMarine && IsBrackish == other.IsBrackish && IsFreshwater == other.IsFreshwater && IsTerrestrial == other.IsTerrestrial && IsExtinct == other.IsExtinct && MatchType == other.MatchType && Nullable.Equals(Modified, other.Modified); + return AphiaId == other.AphiaId && Url.Equals(other.Url) && ScientificName == other.ScientificName && Authority == other.Authority && TaxonRankId == other.TaxonRankId && Rank == other.Rank && Status == other.Status && UnAcceptReason == other.UnAcceptReason && ValidAphiaId == other.ValidAphiaId && ValidName == other.ValidName && ValidAuthority == other.ValidAuthority && ParentNameUsageId == other.ParentNameUsageId && Kingdom == other.Kingdom && Phylum == other.Phylum && Class == other.Class && Order == other.Order && Family == other.Family && Genus == other.Genus && Citation == other.Citation && LsId == other.LsId && IsMarine == other.IsMarine && IsBrackish == other.IsBrackish && IsFreshwater == other.IsFreshwater && IsTerrestrial == other.IsTerrestrial && IsExtinct == other.IsExtinct && MatchType == other.MatchType && Nullable.Equals(Modified, other.Modified); } /// @@ -241,8 +241,8 @@ public override int GetHashCode() hashCode.Add(TaxonRankId); hashCode.Add(Rank); hashCode.Add(Status); - hashCode.Add(UnacceptReason); - hashCode.Add(ValidAphiaID); + hashCode.Add(UnAcceptReason); + hashCode.Add(ValidAphiaId); hashCode.Add(ValidName); hashCode.Add(ValidAuthority); hashCode.Add(ParentNameUsageId); @@ -253,7 +253,7 @@ public override int GetHashCode() hashCode.Add(Family); hashCode.Add(Genus); hashCode.Add(Citation); - hashCode.Add(lsId); + hashCode.Add(LsId); hashCode.Add(IsMarine); hashCode.Add(IsBrackish); hashCode.Add(IsFreshwater); diff --git a/SpeciesDatabaseApi/Worms/WormsClient.cs b/SpeciesDatabaseApi/Worms/WormsClient.cs index a9d8a50..1ab3ea3 100644 --- a/SpeciesDatabaseApi/Worms/WormsClient.cs +++ b/SpeciesDatabaseApi/Worms/WormsClient.cs @@ -52,7 +52,7 @@ public WormsClient(HttpClient? httpClient = null) : base(DefaultApiAddress, http /// public Task GetAphiaAttributeKeysById(int attributeId = 0, bool includeInherited = true, CancellationToken token = default) { - var parameters = new KeyValuePair("include_children", includeInherited); + var parameters = new KeyValuePair("include_children", includeInherited); return GetJsonAsync($"AphiaAttributeKeysByID/{attributeId}", parameters, token); } @@ -65,7 +65,7 @@ public WormsClient(HttpClient? httpClient = null) : base(DefaultApiAddress, http /// public Task GetAphiaAttributesByAphiaId(int aphiaId, bool includeInherited = false, CancellationToken token = default) { - var parameters = new KeyValuePair("include_inherited", includeInherited); + var parameters = new KeyValuePair("include_inherited", includeInherited); return GetJsonAsync($"AphiaAttributesByAphiaID/{aphiaId}", parameters, token); } @@ -89,7 +89,7 @@ public WormsClient(HttpClient? httpClient = null) : base(DefaultApiAddress, http /// public Task GetAphiaIDsByAttributeKeyId(int attributeId, int offset = 1, CancellationToken token = default) { - var parameters = new KeyValuePair("offset", offset); + var parameters = new KeyValuePair("offset", offset); return GetJsonAsync($"AphiaIDsByAttributeKeyID/{attributeId}", parameters, token); } #endregion @@ -117,7 +117,7 @@ public WormsClient(HttpClient? httpClient = null) : base(DefaultApiAddress, http /// public Task GetAphiaExternalIdByAphiaId(int aphiaId, ExternalIdentifierType type, CancellationToken token = default) { - var parameters = new KeyValuePair("type", type); + var parameters = new KeyValuePair("type", type); return GetJsonAsync($"AphiaExternalIDByAphiaID/{aphiaId}", parameters, token); } @@ -130,7 +130,7 @@ public WormsClient(HttpClient? httpClient = null) : base(DefaultApiAddress, http /// public Task GetAphiaRecordByExternalId(string externalId, ExternalIdentifierType type, CancellationToken token = default) { - var parameters = new KeyValuePair("type", type); + var parameters = new KeyValuePair("type", type); return GetJsonAsync($"AphiaRecordByExternalID/{externalId}", parameters, token); } #endregion @@ -160,7 +160,7 @@ public WormsClient(HttpClient? httpClient = null) : base(DefaultApiAddress, http /// public Task GetAphiaChildrenByAphiaId(int aphiaId, bool marineOnly = true, int offset = 1, CancellationToken token = default) { - var parameters = new Dictionary + var parameters = new Dictionary { {"marine_only", marineOnly}, {"offset", offset}, @@ -188,7 +188,7 @@ public WormsClient(HttpClient? httpClient = null) : base(DefaultApiAddress, http /// NULL when no match is found; -999 when multiple matches are found; an integer(AphiaID) when one exact match was found public Task GetAphiaIdByName(string scientificName, bool marineOnly = true, CancellationToken token = default) { - var parameters = new KeyValuePair("marine_only", marineOnly); + var parameters = new KeyValuePair("marine_only", marineOnly); return GetJsonAsync($"AphiaIDByName/{Uri.EscapeDataString(scientificName)}", parameters, token); } @@ -235,7 +235,7 @@ public WormsClient(HttpClient? httpClient = null) : base(DefaultApiAddress, http /// public Task GetAphiaRecordsByAphiaIds(IEnumerable aphiaIds, CancellationToken token = default) { - var parameters = aphiaIds.Select(aphiaId => new KeyValuePair("aphiaids[]", aphiaId)); + var parameters = aphiaIds.Select(aphiaId => new KeyValuePair("aphiaids[]", aphiaId)); return GetJsonAsync("AphiaRecordsByAphiaIDs", parameters, token); } @@ -250,7 +250,7 @@ public WormsClient(HttpClient? httpClient = null) : base(DefaultApiAddress, http /// public Task GetAphiaRecordsByDate(DateTime startDate, DateTime endDate, bool marineOnly = true, int offset = 1, CancellationToken token = default) { - var parameters = new Dictionary + var parameters = new Dictionary { {"startdate", startDate.ToString("O")}, {"enddate", endDate.ToString("O")}, @@ -271,8 +271,8 @@ public WormsClient(HttpClient? httpClient = null) : base(DefaultApiAddress, http /// public Task?> GetAphiaRecordsByMatchNames(IEnumerable scientificNames, bool marineOnly = true, CancellationToken token = default) { - var parameters = scientificNames.Select(aphiaId => new KeyValuePair("scientificnames[]", aphiaId)).ToList(); - parameters.Add(new KeyValuePair("marine_only", marineOnly)); + var parameters = scientificNames.Select(aphiaId => new KeyValuePair("scientificnames[]", aphiaId)).ToList(); + parameters.Add(new ("marine_only", marineOnly)); return GetJsonAsync>("AphiaRecordsByMatchNames", parameters, token); } @@ -287,7 +287,7 @@ public WormsClient(HttpClient? httpClient = null) : base(DefaultApiAddress, http /// public Task GetAphiaRecordsByName(string scientificName, bool like = true, bool marineOnly = true, int offset = 1, CancellationToken token = default) { - var parameters = new Dictionary + var parameters = new Dictionary { {"like", like}, {"marine_only", marineOnly}, @@ -308,9 +308,9 @@ public WormsClient(HttpClient? httpClient = null) : base(DefaultApiAddress, http /// public Task?> GetAphiaRecordsByNames(IEnumerable scientificNames, bool like = false, bool marineOnly = true, CancellationToken token = default) { - var parameters = scientificNames.Select(aphiaId => new KeyValuePair("scientificnames[]", aphiaId)).ToList(); - parameters.Add(new KeyValuePair("like", like)); - parameters.Add(new KeyValuePair("marine_only", marineOnly)); + var parameters = scientificNames.Select(aphiaId => new KeyValuePair("scientificnames[]", aphiaId)).ToList(); + parameters.Add(new ("like", like)); + parameters.Add(new ("marine_only", marineOnly)); return GetJsonAsync>("AphiaRecordsByNames", parameters, token); } @@ -324,7 +324,7 @@ public WormsClient(HttpClient? httpClient = null) : base(DefaultApiAddress, http /// public Task GetAphiaRecordsByTaxonRankId(int taxonId, int belongsToAphiaId, int offset = 1, CancellationToken token = default) { - var parameters = new Dictionary + var parameters = new Dictionary { {"belongsTo", belongsToAphiaId}, {"offset", offset}, @@ -341,7 +341,7 @@ public WormsClient(HttpClient? httpClient = null) : base(DefaultApiAddress, http /// public Task GetAphiaSynonymsByAphiaId(int aphiaId, int offset = 1, CancellationToken token = default) { - var parameter = new KeyValuePair("offset", offset); + var parameter = new KeyValuePair("offset", offset); return GetJsonAsync($"AphiaSynonymsByAphiaID/{aphiaId}", parameter, token); } @@ -354,7 +354,7 @@ public WormsClient(HttpClient? httpClient = null) : base(DefaultApiAddress, http /// public Task GetAphiaTaxonRanksById(int taxonId = -1, int aphiaId = -1, CancellationToken token = default) { - var parameter = new KeyValuePair("AphiaID", aphiaId); + var parameter = new KeyValuePair("AphiaID", aphiaId); return GetJsonAsync($"AphiaTaxonRanksByID/{taxonId}", parameter, token); } @@ -367,7 +367,7 @@ public WormsClient(HttpClient? httpClient = null) : base(DefaultApiAddress, http /// public Task GetAphiaTaxonRanksByName(string taxonRank = "", int aphiaId = -1, CancellationToken token = default) { - var parameter = new KeyValuePair("AphiaID", aphiaId); + var parameter = new KeyValuePair("AphiaID", aphiaId); return GetJsonAsync($"AphiaTaxonRanksByName/{Uri.EscapeDataString(taxonRank)}", parameter, token); } #endregion @@ -383,7 +383,7 @@ public WormsClient(HttpClient? httpClient = null) : base(DefaultApiAddress, http /// A representing the asynchronous operation. public Task GetAphiaRecordsByVernacular(string vernacular, bool like = false, int offset = 1, CancellationToken token = default) { - var parameters = new Dictionary + var parameters = new Dictionary { {nameof(like), like}, {nameof(offset), offset}, diff --git a/SpeciesDatabaseCmd/IucnCommand.cs b/SpeciesDatabaseCmd/IucnCommand.cs index 9a3e574..076cdb6 100644 --- a/SpeciesDatabaseCmd/IucnCommand.cs +++ b/SpeciesDatabaseCmd/IucnCommand.cs @@ -20,7 +20,7 @@ internal static class IucnCommand internal static Command CreateCommand() { - var command = new Command(Client.ClientAcronym.ToUpper(), $"Query - {Client.ClientFullName}") + var command = new Command(Client.ClientAcronym.ToUpper(), Program.GetRootCommandDescription(Client)) { VersionCommand(), diff --git a/SpeciesDatabaseCmd/MrCommand.cs b/SpeciesDatabaseCmd/MrCommand.cs new file mode 100644 index 0000000..fc424de --- /dev/null +++ b/SpeciesDatabaseCmd/MrCommand.cs @@ -0,0 +1,319 @@ +using System.CommandLine; +using SpeciesDatabaseApi.Mr; + +namespace SpeciesDatabaseCmd; + +internal static class MrCommand +{ + private static readonly MrClient Client = new(); + + private static readonly Argument MrgIdArgument = new("mrgid", "The MRGID to search for."); + private static readonly Argument NameArgument = new("name", "The name to search for."); + private static readonly Argument NamesArgument = new("names", "The names to search for."); + private static readonly Argument TypeIdsArgument = new("type-ids", "One or more placetypeIDs."); + private static readonly Argument OffsetArgument = new( "offset", () => 0, "Provide offset to return next batch of 100 sources."); + private static readonly Argument SourceIdArgument = new( "source-id", "The SourceID to search for."); + private static readonly Argument SourceNameArgument = new( "source-name", "The source name to search for."); + private static readonly Argument TypeNameArgument = new( "type-name", "The type name to search for."); + + private static readonly Option LanguageOption = new(new[] { "--language" }, "Language (ISO 639-1 code)."); + private static readonly Option LikeOption = new(new[] { "-l", "--like" }, () => true, "Adds a '%'-sign before and after the GazetteerName (SQL LIKE function)."); + private static readonly Option FuzzyOption = new(new[] { "-f", "--fuzzy" }, () => false, "Uses Levenshtein query to find nearest matches."); + private static readonly Option OffsetOption = new(new[]{"-o", "--offset"}, "Start record number, in order to page through next batch of results."); + private static readonly Option CountOption = new(new[]{"-c", "--count"}, () => 100, "number of records to retrieve. max=100."); + + internal static Command CreateCommand() + { + var command = new Command(Client.ClientAcronym.ToUpper(), Program.GetRootCommandDescription(Client)) + { + GazetteerRecordCommand(), + FullGazetteerRecordCommand(), + + GazetteerGeometriesCommand(), + GazetteerTypesCommand(), + GazetteerRecordsByNameCommand(), + GazetteerRecordsByNamesCommand(), + + FeedCommand(), + + GazetteerWmSesCommand(), + GazetteerRelationsCommand(), + GazetteerSourcesCommand(), + GazetteerSourceCommand(), + GazetteerNamesCommand(), + GazetteerRecordsBySourceCommand(), + GazetteerRecordsByTypeCommand(), + GazetteerRecordsByLatLongCommand() + }; + + return command; + } + + private static Command GazetteerRecordCommand() + { + var command = new Command("GazetteerRecord", "Gets one record for the given MRGID.") + { + MrgIdArgument + }; + + command.SetHandler(async (mrgId) => + { + var result = await Client.GetGazetteerRecord(mrgId); + Program.Print(result); + }, MrgIdArgument); + + return command; + } + + private static Command FullGazetteerRecordCommand() + { + var command = new Command("FullGazetteerRecord", "Gets one record for the given MRGID.") + { + MrgIdArgument + }; + + command.SetHandler(async (mrgId) => + { + var result = await Client.GetFullGazetteerRecord(mrgId); + Program.Print(result); + }, MrgIdArgument); + + return command; + } + + private static Command GazetteerGeometriesCommand() + { + var command = new Command("GazetteerGeometries", "Gets all geometries associated with a gazetteer record.") + { + MrgIdArgument + }; + + command.SetHandler(async (mrgId) => + { + var result = await Client.GetGazetteerGeometries(mrgId); + Program.Print(result); + }, MrgIdArgument); + + return command; + } + + private static Command GazetteerTypesCommand() + { + var command = new Command("GazetteerTypes", "Gets all possible types.") + { + }; + + command.SetHandler(async () => + { + var result = await Client.GetGazetteerTypes(); + Program.Print(result); + }); + + return command; + } + + private static Command GazetteerRecordsByNameCommand() + { + var command = new Command("GazetteerRecordsByName", "Gets a list of the first 100 matching records for given name.") + { + NameArgument, + TypeIdsArgument, + + LikeOption, + FuzzyOption, + LanguageOption, + OffsetOption, + CountOption, + }; + + command.SetHandler(async (name, typeIds, like, fuzzy, language, offset, count) => + { + var result = await Client.GetGazetteerRecords(name, like, fuzzy, typeIds, language, offset, count); + Program.Print(result); + }, NameArgument, TypeIdsArgument, LikeOption, FuzzyOption, LanguageOption, OffsetOption, CountOption); + + return command; + } + + private static Command GazetteerRecordsByNamesCommand() + { + var command = new Command("GazetteerRecordsByNames", "Gets a list of the first 100 matching records for given names.") + { + NamesArgument, + + LikeOption, + FuzzyOption, + }; + + command.SetHandler(async (names, like, fuzzy) => + { + var result = await Client.GetGazetteerRecords(names, like, fuzzy); + if (result is null) + { + Program.Print(result); + return; + } + + foreach (var record in result) + { + Program.Print(record); + } + + }, NamesArgument, LikeOption, FuzzyOption); + + return command; + } + + private static Command FeedCommand() + { + var command = new Command("Feed", "Gets the Linked Data Event Stream feed") + { + }; + + command.SetHandler(async () => + { + var result = await Client.GetFeed(); + Program.Print(result); + }); + + return command; + } + + private static Command GazetteerWmSesCommand() + { + var command = new Command("GazetteerWMSes", "Gets WMS information for the given MRGID.") + { + MrgIdArgument + }; + + command.SetHandler(async (mrgId) => + { + var result = await Client.GetgetGazetteerWmSes(mrgId); + Program.Print(result); + }, MrgIdArgument); + + return command; + } + + private static Command GazetteerRelationsCommand() + { + var directionArgument = new Argument("direction", () => MrRelationDirection.Upper); + var typeArgument = new Argument("type", () => MrRelationType.PartOf); + var command = new Command("GazetteerRelations", "Gets the first 100 related records for the given MRGID.") + { + MrgIdArgument, + directionArgument, + typeArgument + }; + + command.SetHandler(async (mrgId, direction, type) => + { + var result = await Client.GetGazetteerRelations(mrgId, direction, type); + Program.Print(result); + }, MrgIdArgument, directionArgument, typeArgument); + + return command; + } + + private static Command GazetteerSourcesCommand() + { + var command = new Command("GazetteerSources", "Gets all sources per batch of 100.") + { + OffsetArgument + }; + + command.SetHandler(async (offset) => + { + var result = await Client.GetGazetteerSources(offset); + Program.Print(result); + }, OffsetArgument); + + return command; + } + + private static Command GazetteerSourceCommand() + { + var command = new Command("GazetteerSource", "Gets the source name corresponding to a source ID.") + { + SourceIdArgument + }; + + command.SetHandler(async (sourceId) => + { + var result = await Client.GetGazetteerSource(sourceId); + Program.Print(result); + }, SourceIdArgument); + + return command; + } + + private static Command GazetteerNamesCommand() + { + var command = new Command("GazetteerNames", "Gets the first 100 names for the given MRGID.") + { + MrgIdArgument + }; + + command.SetHandler(async (mrgId) => + { + var result = await Client.GetGazetteerNames(mrgId); + Program.Print(result); + }, MrgIdArgument); + + return command; + } + + private static Command GazetteerRecordsBySourceCommand() + { + var command = new Command("GazetteerRecordsBySource", "Gets the first 100 records for the given source.") + { + SourceNameArgument + }; + + command.SetHandler(async (sourceName) => + { + var result = await Client.GetGazetteerRecordsBySource(sourceName); + Program.Print(result); + }, SourceNameArgument); + + return command; + } + + private static Command GazetteerRecordsByTypeCommand() + { + var command = new Command("GazetteerRecordsByType", "Gets the first 100 records for the given source.") + { + TypeNameArgument, + OffsetArgument + }; + + command.SetHandler(async (typeName, offset) => + { + var result = await Client.GetGazetteerRecordsByType(typeName, offset); + Program.Print(result); + }, TypeNameArgument, OffsetArgument); + + return command; + } + + private static Command GazetteerRecordsByLatLongCommand() + { + var latitudeArgument = new Argument("latitude", "A decimal number which ranges from -90 to 90"); + var longitudeArgument = new Argument("longitude", "A decimal number which ranges from -180 to +180"); + var command = new Command("GazetteerRecordsByLatLong", "Gets all gazetteer records where the geometry intersects with the given latitude and longitude per batch of 100. Results are sorted by area, smallest to largest.") + { + latitudeArgument, + longitudeArgument, + TypeIdsArgument, + OffsetOption + }; + + command.SetHandler(async (latitude, longitude, typeIds, offset) => + { + var result = await Client.GetGazetteerRecordsByLatLong(latitude, longitude, typeIds, offset); + Program.Print(result); + }, latitudeArgument, longitudeArgument, TypeIdsArgument, OffsetOption); + + return command; + } +} \ No newline at end of file diff --git a/SpeciesDatabaseCmd/Program.cs b/SpeciesDatabaseCmd/Program.cs index 138c7ed..19db217 100644 --- a/SpeciesDatabaseCmd/Program.cs +++ b/SpeciesDatabaseCmd/Program.cs @@ -1,5 +1,6 @@ using System.Collections; using System.CommandLine; +using SpeciesDatabaseApi; namespace SpeciesDatabaseCmd; @@ -10,7 +11,8 @@ static async Task Main(string[] args) var rootCommand = new RootCommand("Query specific taxonomy and species database") { WormsCommand.CreateCommand(), - IucnCommand.CreateCommand() + IucnCommand.CreateCommand(), + MrCommand.CreateCommand() }; try @@ -25,6 +27,8 @@ static async Task Main(string[] args) return 1; } + internal static string GetRootCommandDescription(BaseClient client) => $"Query - {client.ClientFullName} ({client.WebsiteUrl})"; + internal static string FormatResults(string? text) { if (string.IsNullOrWhiteSpace(text)) return string.Empty; diff --git a/SpeciesDatabaseCmd/Properties/launchSettings.json b/SpeciesDatabaseCmd/Properties/launchSettings.json index 37f5ae3..fb0247c 100644 --- a/SpeciesDatabaseCmd/Properties/launchSettings.json +++ b/SpeciesDatabaseCmd/Properties/launchSettings.json @@ -47,6 +47,18 @@ "IUCN SpeciesCitationById 2467 europe": { "commandName": "Project", "commandLineArgs": "IUCN SpecieCitationById 2467 europe" + }, + "MR GazetteerRecordsByName belgian": { + "commandName": "Project", + "commandLineArgs": "MR GazetteerRecordsByName belgian" + }, + "MR GazetteerRecordsByNames belgian portugal spain": { + "commandName": "Project", + "commandLineArgs": "MR GazetteerRecordsByNames belgian portugal spain" + }, + "MR Feed": { + "commandName": "Project", + "commandLineArgs": "MR Feed" } } } \ No newline at end of file diff --git a/SpeciesDatabaseCmd/WormsCommand.cs b/SpeciesDatabaseCmd/WormsCommand.cs index be0a044..117070b 100644 --- a/SpeciesDatabaseCmd/WormsCommand.cs +++ b/SpeciesDatabaseCmd/WormsCommand.cs @@ -1,5 +1,4 @@ using System.CommandLine; -using SpeciesDatabaseApi; using SpeciesDatabaseApi.Worms; namespace SpeciesDatabaseCmd; @@ -34,7 +33,7 @@ internal static class WormsCommand internal static Command CreateCommand() { - var command = new Command(Client.ClientAcronym.ToUpper(), $"Query - {Client.ClientFullName}") + var command = new Command(Client.ClientAcronym.ToUpper(), Program.GetRootCommandDescription(Client)) { //Attributes AphiaAttributeKeysByIdCommand(),