diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9a22f02 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,187 @@ +root = true + +[*] +charset = utf-8 +indent_style = tab +indent_size = 4 + +[*.{md,markdown,json,js,csproj,fsproj,targets,targets,props,xml}] +indent_style = space +indent_size = 2 + +# Dotnet code style settings: +[*.{cs,vb}] + +# --- +# naming conventions +# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-naming-conventions + +# language conventions +# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#language-conventions + +dotnet_sort_system_directives_first = true + +# Style rules +# https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/naming-rules?view=vs-2017 + +# Constants always pascal case +dotnet_naming_rule.constants_should_be_pascal_case.symbols = consts +dotnet_naming_rule.constants_should_be_pascal_case.style = consts +dotnet_naming_rule.constants_should_be_pascal_case.severity = suggestion + +dotnet_naming_symbols.consts.applicable_kinds = field +dotnet_naming_symbols.consts.applicable_accessibilities = * +dotnet_naming_symbols.consts.required_modifiers = const + +dotnet_naming_style.consts.capitalization = pascal_case + +# Non-public static fields always pascal case +dotnet_naming_rule.non_public_static_fields_should_be_pascal_case.symbols = non_public_static_fields +dotnet_naming_rule.non_public_static_fields_should_be_pascal_case.style = non_public_static_fields +dotnet_naming_rule.non_public_static_fields_should_be_pascal_case.severity = suggestion + +dotnet_naming_symbols.non_public_static_fields.applicable_kinds = field +dotnet_naming_symbols.non_public_static_fields.applicable_accessibilities = private, protected, internal, protected_internal, private_protected +dotnet_naming_symbols.non_public_static_fields.required_modifiers = static + +dotnet_naming_style.non_public_static_fields.capitalization = pascal_case + +# Non-private readonly fields are pascal case +dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.symbols = non_private_readonly_fields +dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.style = non_private_readonly_fields + +dotnet_naming_symbols.non_private_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.non_private_readonly_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected +dotnet_naming_symbols.non_private_readonly_fields.required_modifiers = readonly + +dotnet_naming_style.non_private_readonly_fields.capitalization = pascal_case + +# Private instance fields are camel case prefixed underscore +dotnet_naming_rule.private_fields_should_be_camelcase_prefix_underscore.symbols = private_fields +dotnet_naming_rule.private_fields_should_be_camelcase_prefix_underscore.style = private_fields +dotnet_naming_rule.private_fields_should_be_camelcase_prefix_underscore.severity = suggestion + +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private + +dotnet_naming_style.private_fields.capitalization = camel_case +dotnet_naming_style.private_fields.required_prefix = _ + +# Locals and parameters are camel case +dotnet_naming_rule.locals.severity = suggestion +dotnet_naming_rule.locals.symbols = locals +dotnet_naming_rule.locals.style = locals + +dotnet_naming_symbols.locals.applicable_kinds = parameter, local + +dotnet_naming_style.locals.capitalization = camel_case + +# Local functions are pascal case +dotnet_naming_rule.local_functions.severity = suggestion +dotnet_naming_rule.local_functions.symbols = local_functions +dotnet_naming_rule.local_functions.style = local_functions + +dotnet_naming_symbols.local_functions.applicable_kinds = local_function + +dotnet_naming_style.local_functions.capitalization = pascal_case + +# Public members always pascal case +dotnet_naming_rule.public_members_should_be_pascal_case.symbols = public_members +dotnet_naming_rule.public_members_should_be_pascal_case.style = public_members +dotnet_naming_rule.public_members_should_be_pascal_case.severity = suggestion + +dotnet_naming_symbols.public_members.applicable_kinds = property, method, field, event, delegate +dotnet_naming_symbols.public_members.applicable_accessibilities = public + +dotnet_naming_style.public_members.capitalization = pascal_case + +dotnet_style_qualification_for_field = false:error +dotnet_style_qualification_for_property = false:error +dotnet_style_qualification_for_method = false:error +dotnet_style_qualification_for_event = false:error + +# Use language keywords instead of framework type names for type references +dotnet_style_predefined_type_for_locals_parameters_members = true:error +dotnet_style_predefined_type_for_member_access = true:error + +# Suggest more modern language features when available +dotnet_style_object_initializer = true:error +dotnet_style_collection_initializer = true:error +dotnet_style_explicit_tuple_names = true:error +dotnet_style_prefer_inferred_anonymous_type_member_names = true:error +dotnet_style_prefer_inferred_tuple_names = true:error +dotnet_style_coalesce_expression = true:error +dotnet_style_null_propagation = true:error + +dotnet_style_require_accessibility_modifiers = for_non_interface_members:error +dotnet_style_readonly_field = true:error + +# CSharp code style settings: +[*.cs] +trim_trailing_whitespace = true +insert_final_newline = true + +# Prefer "var" everywhere +csharp_style_var_for_built_in_types = true:error +csharp_style_var_when_type_is_apparent = true:error +csharp_style_var_elsewhere = true:error + +csharp_style_expression_bodied_methods = true:error +csharp_style_expression_bodied_constructors = true:error +csharp_style_expression_bodied_operators = true:error +csharp_style_expression_bodied_properties = true:error +csharp_style_expression_bodied_indexers = true:error +csharp_style_expression_bodied_accessors = true:error + +csharp_style_pattern_matching_over_is_with_cast_check = true:error +csharp_style_pattern_matching_over_as_with_null_check = true:error +csharp_style_inlined_variable_declaration = true:error +csharp_style_deconstructed_variable_declaration = true:error +csharp_style_pattern_local_over_anonymous_function = true:error +csharp_style_throw_expression = true:error +csharp_style_conditional_delegate_call = true:error + +csharp_prefer_braces = false:warning +csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async:error + +# formatting conventions +# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#formatting-conventions + +# newline settings +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true + +# indent +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_method_call_parameter_list_parentheses = false + +# wrap +csharp_preserve_single_line_statements = false +csharp_preserve_single_line_blocks = true + +# resharper +resharper_csharp_braces_for_lock = required_for_multiline +resharper_csharp_braces_for_using = required_for_multiline +resharper_csharp_braces_for_while = required_for_multiline +resharper_csharp_braces_for_foreach = required_for_multiline +resharper_csharp_braces_for_for = required_for_multiline +resharper_csharp_braces_for_fixed = required_for_multiline +resharper_csharp_braces_for_ifelse = required_for_multiline +resharper_csharp_accessor_owner_body = expression_body + +[*.{sh,bat,ps1}] +trim_trailing_whitespace = true +insert_final_newline = true + +[*.sh] +end_of_line = lf \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..08d9181 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,15 @@ +# Auto detect text files and perform LF normalization +* text=auto eol=lf + +# Set default behavior for command prompt diff. +# This gives output on command line taking C# language constructs into consideration (e.g showing class name) +*.cs text diff=csharp + +# Set windows specific files explicitly to crlf line ending +*.cmd eol=crlf +*.bat eol=crlf +*.ps1 eol=crlf + +# Mark files specifically as binary to avoid line ending conversion +*.snk binary +*.png binary \ No newline at end of file diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml new file mode 100644 index 0000000..d9ab55c --- /dev/null +++ b/.github/workflows/dotnet.yml @@ -0,0 +1,30 @@ +# This workflow will build a .NET project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net + +name: .NET + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 7.0.x + - name: Test + env: + PINECONE_APIKEY: ${{ secrets.PINECONE_APIKEY }} + PINECONE_INDEXNAME: ${{ secrets.PINECONE_INDEXNAME }} + PINECONE_ENVIRONMENT: ${{ secrets.PINECONE_ENVIRONMENT }} + PINECONE_PROJECTNAME: ${{ secrets.PINECONE_PROJECTNAME }} + run: ./build.sh test + shell: bash diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..39cd2c8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +bin/ +obj/ +/packages/ +riderModule.iml +/_ReSharper.Caches/ + +.idea +nuget +protos + +*.sln.DotSettings.user + +credentials.json \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..214c386 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,27 @@ + + + + Russ Cam + Russ Cam + Apache-2.0 + https://github.com/russcam/pinecone-dotnet-client + https://github.com/russcam/pinecone-dotnet-client + https://github.com/russcam/pinecone-dotnet-client/releases + pinecone, database, vector, search + V3.0.0rc2 + + + $([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), build.bat)) + 0.1 + true + latest + true + False + false + + + + + + + \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7a4a3ea --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/Pinecone.sln b/Pinecone.sln new file mode 100644 index 0000000..bc314df --- /dev/null +++ b/Pinecone.sln @@ -0,0 +1,40 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pinecone.Grpc", "src\Pinecone.Grpc\Pinecone.Grpc.csproj", "{CBEA50A5-9A0C-486E-BCE5-223E978627C9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pinecone.Grpc.Tests", "tests\Pinecone.Grpc.Tests\Pinecone.Grpc.Tests.csproj", "{7D5DDA3D-ADAC-4193-9FA9-5A62EC6ED784}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Build", "build\Build.csproj", "{C202A4B5-DE75-424E-B3A2-A3BF4123FB0D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Root", "Root", "{178B0A64-74DA-495C-A126-1BF8D66ACF65}" + ProjectSection(SolutionItems) = preProject + .gitignore = .gitignore + build.bat = build.bat + build.sh = build.sh + .gitattributes = .gitattributes + .editorconfig = .editorconfig + Directory.Build.props = Directory.Build.props + dotnet-tools.json = dotnet-tools.json + README.md = README.md + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {CBEA50A5-9A0C-486E-BCE5-223E978627C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CBEA50A5-9A0C-486E-BCE5-223E978627C9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CBEA50A5-9A0C-486E-BCE5-223E978627C9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CBEA50A5-9A0C-486E-BCE5-223E978627C9}.Release|Any CPU.Build.0 = Release|Any CPU + {7D5DDA3D-ADAC-4193-9FA9-5A62EC6ED784}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7D5DDA3D-ADAC-4193-9FA9-5A62EC6ED784}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7D5DDA3D-ADAC-4193-9FA9-5A62EC6ED784}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7D5DDA3D-ADAC-4193-9FA9-5A62EC6ED784}.Release|Any CPU.Build.0 = Release|Any CPU + {C202A4B5-DE75-424E-B3A2-A3BF4123FB0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C202A4B5-DE75-424E-B3A2-A3BF4123FB0D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C202A4B5-DE75-424E-B3A2-A3BF4123FB0D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C202A4B5-DE75-424E-B3A2-A3BF4123FB0D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/README.md b/README.md new file mode 100644 index 0000000..427ef3e --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# .NET gRPC client for Pinecone vector database + +A .NET gRPC client for [Pinecone vector database](https://www.pinecone.io/). + +## Getting started + +### Installing + +```sh +dotnet add package Pinecone.Grpc --version 1.0.0-alpha1 +``` + +### Usage + +The `Pinecone.Grpc.VectorService` provides an entry point to interact with all of +Pinecone's gRPC services + +```csharp +using static Pinecone.Grpc.VectorService; + +namespace Example; + +public class Program +{ + public static void Main(string[] args) + { + var configuration = new ClientConfiguration( + "", + "", + ""); + + var channel = PineconeChannel.ForConfiguration(configuration, ""); + var client = new VectorServiceClient(channel); + + var describeIndexStatsResponse = + client.DescribeIndexStats(new DescribeIndexStatsRequest()); + } +} +``` \ No newline at end of file diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..31a442a --- /dev/null +++ b/build.bat @@ -0,0 +1,2 @@ +@echo Off +dotnet run --project build -- %* diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..b8feb2c --- /dev/null +++ b/build.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +set -euo pipefail +dotnet run --project build -- "$@" diff --git a/build/Build.csproj b/build/Build.csproj new file mode 100644 index 0000000..b133ea9 --- /dev/null +++ b/build/Build.csproj @@ -0,0 +1,21 @@ + + + + net7.0 + enable + enable + Exe + + + + + + + + + + + + + + diff --git a/build/Main.cs b/build/Main.cs new file mode 100644 index 0000000..8834f5b --- /dev/null +++ b/build/Main.cs @@ -0,0 +1,142 @@ +using System.CommandLine; +using System.IO.Compression; +using System.Net.Http.Headers; +using System.Text.RegularExpressions; +using System.Xml.Linq; +using Bullseye; +using SharpCompress.Readers; +using static BuildTargets; +using static Bullseye.Targets; +using static SimpleExec.Command; + +const string envVarMissing = " environment variable is missing. Aborting."; +const string packOutput = "nuget"; +const string protosDir = "protos"; +const string project = "Pinecone.Grpc"; + +var doc = XDocument.Load("Directory.Build.props"); +var pineconeVersion = doc.Descendants(XName.Get("PineconeVersion", "http://schemas.microsoft.com/developer/msbuild/2003")) + .First().Value; + +var cmd = new RootCommand +{ + new Argument("targets") + { + Description = + "A list of targets to run or list. If not specified, the \"default\" target will be run, or all targets will be listed.", + } +}; + +foreach (var (aliases, description) in Options.Definitions) + cmd.Add(new Option(aliases.ToArray(), description)); + +cmd.SetHandler(async () => +{ + // translate from System.CommandLine to Bullseye + var cmdLine = cmd.Parse(args); + var targets = cmdLine.CommandResult.Tokens.Select(token => token.Value); + var options = new Options(Options.Definitions.Select(d => (d.Aliases[0], + cmdLine.GetValueForOption(cmd.Options.OfType>().Single(o => o.HasAlias(d.Aliases[0])))))); + + Target(Restore, () => + { + Run("dotnet", "restore"); + }); + + Target(CleanBuildOutput, DependsOn(Restore), () => + { + Run("dotnet", "clean -c Release -v m --nologo"); + }); + + Target(DownloadProtos, async () => + { + var protosTagDir = Path.Combine(protosDir, pineconeVersion); + if (Directory.Exists(protosTagDir) && Directory.EnumerateFileSystemEntries(protosTagDir).Any()) + { + Console.WriteLine($"Already downloaded protos for {pineconeVersion}"); + return; + } + + Directory.CreateDirectory(protosTagDir); + Console.WriteLine($"Downloading protos for tag {pineconeVersion} to {protosTagDir}"); + var url = $"https://api.github.com/repos/pinecone-io/pinecone-client/tarball/refs/tags/{pineconeVersion}"; + var protoFileRegex = new Regex(".*?client_sdk/src/proto/.*?.proto"); + var client = new HttpClient + { + DefaultRequestHeaders = { UserAgent = { new ProductInfoHeaderValue("PineconeNet", "1.0.0") } }, + }; + + var response = await client.GetAsync(url); + await using var stream = await response.Content.ReadAsStreamAsync(); + await using var gzip = new GZipStream(stream, CompressionMode.Decompress); + var reader = ReaderFactory.Open(gzip); + while (reader.MoveToNextEntry()) + { + if (!reader.Entry.IsDirectory && protoFileRegex.IsMatch(reader.Entry.Key)) + reader.WriteEntryToDirectory(protosTagDir); + } + + // add csharp namespace if file does not contain one + foreach (var file in Directory.EnumerateFiles(protosTagDir)) + { + var contents = File.ReadAllLines(file).ToList(); + if (contents.Any(line => line.Contains("option csharp_namespace"))) + continue; + + var index = 0; + for (var i = 0; i < contents.Count; i++) + { + if (contents[i].StartsWith("syntax") || string.IsNullOrEmpty(contents[i])) + continue; + + index = i; + break; + } + + contents.Insert(index,"option csharp_namespace = \"Pinecone.Grpc\";"); + File.WriteAllLines(file, contents); + } + }); + + Target(Build, DependsOn(DownloadProtos, CleanBuildOutput), () => + { + Run("dotnet", "build -c Release --nologo"); + }); + + Target(Test, DependsOn(Build), () => + { + Run("dotnet", "test -c Release --no-build"); + }); + + Target(CleanPackOutput, () => + { + if (Directory.Exists(packOutput)) + Directory.Delete(packOutput, true); + }); + + Target(Pack, DependsOn(Build, CleanPackOutput), () => + { + var outputDir = Directory.CreateDirectory(packOutput); + Run("dotnet", + $"pack src/{project}/{project}.csproj -c Release -o \"{outputDir.FullName}\" --no-build --nologo"); + }); + + Target(Default, DependsOn(Test)); + + await RunTargetsAndExitAsync(targets, options, + messageOnly: ex => ex is SimpleExec.ExitCodeException || ex.Message.EndsWith(envVarMissing)); +}); + +return await cmd.InvokeAsync(args); + +internal static class BuildTargets +{ + public const string CleanBuildOutput = "clean-build-output"; + public const string CleanPackOutput = "clean-pack-output"; + public const string Build = "build"; + public const string Test = "test"; + public const string Default = "default"; + public const string Restore = "restore"; + public const string Pack = "pack"; + public const string DownloadProtos = "download-protos"; +} diff --git a/build/keys/keypair.snk b/build/keys/keypair.snk new file mode 100644 index 0000000..1827450 Binary files /dev/null and b/build/keys/keypair.snk differ diff --git a/build/keys/public.snk b/build/keys/public.snk new file mode 100644 index 0000000..dd938b3 Binary files /dev/null and b/build/keys/public.snk differ diff --git a/dotnet-tools.json b/dotnet-tools.json new file mode 100644 index 0000000..6127231 --- /dev/null +++ b/dotnet-tools.json @@ -0,0 +1,18 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "minver-cli": { + "version": "2.5.0", + "commands": [ + "minver" + ] + }, + "nupkg-validator": { + "version": "0.5.0", + "commands": [ + "nupkg-validator" + ] + } + } +} \ No newline at end of file diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 0000000..545192b --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,39 @@ + + + + + true + + true + ..\..\build\keys\keypair.snk + + + README.md + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + true + true + true + + + + + + + + + + + + + + + + $(NoWarn);IDT001;IDT002 + + + + + + + + \ No newline at end of file diff --git a/src/Pinecone.Grpc/ClientConfiguration.cs b/src/Pinecone.Grpc/ClientConfiguration.cs new file mode 100644 index 0000000..aa86171 --- /dev/null +++ b/src/Pinecone.Grpc/ClientConfiguration.cs @@ -0,0 +1,36 @@ +using Grpc.Core; +using Grpc.Net.Client; + +namespace Pinecone.Grpc; + +/// +/// Configuration to interact with Pinecone service +/// +public class ClientConfiguration +{ + /// + /// Gets the environment + /// + public string Environment { get; } + /// + /// Gets the project name + /// + public string ProjectName { get; } + /// + /// Gets the API key + /// + public string ApiKey { get; } + + /// + /// Instantiates a new instance of to interact with Pinecone + /// + /// The environment e.g. eu-west1-gcp + /// The project name. e.g. f43c8f1 + /// The API key e.g. + public ClientConfiguration(string environment, string projectName, string apiKey) + { + Environment = environment; + ProjectName = projectName; + ApiKey = apiKey; + } +} diff --git a/src/Pinecone.Grpc/Pinecone.Grpc.csproj b/src/Pinecone.Grpc/Pinecone.Grpc.csproj new file mode 100644 index 0000000..674a9ce --- /dev/null +++ b/src/Pinecone.Grpc/Pinecone.Grpc.csproj @@ -0,0 +1,58 @@ + + + + Pinecone .NET gRPC client + .NET gRPC client for Pinecone vector database + net462; net471; netstandard2.0; net6.0; net7.0 + enable + enable + true + true + true + true + Pinecone.Grpc + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + /// The version of pinecone from which this client was created + /// + public const string PineconeVersion = "$(PineconeVersion)"%3B + } +} + ]]> + + + + + $(IntermediateOutputPath)PineconeGrpcClient.g.cs + + + + + + + + + + diff --git a/src/Pinecone.Grpc/PineconeCallInvoker.cs b/src/Pinecone.Grpc/PineconeCallInvoker.cs new file mode 100644 index 0000000..1eecd1d --- /dev/null +++ b/src/Pinecone.Grpc/PineconeCallInvoker.cs @@ -0,0 +1,42 @@ +using Grpc.Core; +using Grpc.Core.Interceptors; +using Grpc.Net.Client; + +namespace Pinecone.Grpc; + +/// +/// Abstraction of client-side RPC invocation for interacting with Pinecone service +/// +public class PineconeCallInvoker : CallInvoker +{ + private readonly CallInvoker _callInvoker; + + /// + /// Instantiates a new instance of + /// + /// the channel to connect to the Pinecone service + /// the API key to send with each request + public PineconeCallInvoker(GrpcChannel channel, string apiKey) => + _callInvoker = channel.Intercept(metadata => + { + metadata.Add("api-key", apiKey); + return metadata; + }); + + /// + public override TResponse BlockingUnaryCall(Method method, string? host, CallOptions options, TRequest request) => _callInvoker.BlockingUnaryCall(method, host, options, request); + + /// + public override AsyncUnaryCall AsyncUnaryCall(Method method, string? host, CallOptions options, TRequest request) => _callInvoker.AsyncUnaryCall(method, host, options, request); + + /// + public override AsyncServerStreamingCall AsyncServerStreamingCall(Method method, string? host, CallOptions options, + TRequest request) => + _callInvoker.AsyncServerStreamingCall(method, host, options, request); + + /// + public override AsyncClientStreamingCall AsyncClientStreamingCall(Method method, string? host, CallOptions options) => _callInvoker.AsyncClientStreamingCall(method, host, options); + + /// + public override AsyncDuplexStreamingCall AsyncDuplexStreamingCall(Method method, string? host, CallOptions options) => _callInvoker.AsyncDuplexStreamingCall(method, host, options); +} diff --git a/src/Pinecone.Grpc/PineconeChannel.cs b/src/Pinecone.Grpc/PineconeChannel.cs new file mode 100644 index 0000000..6f26429 --- /dev/null +++ b/src/Pinecone.Grpc/PineconeChannel.cs @@ -0,0 +1,40 @@ +using Grpc.Core; +using Grpc.Net.Client; + +namespace Pinecone.Grpc; + +/// +/// A gRPC channel to Pinecone service. +/// Channels are an abstraction of long-lived connections to remote servers. +/// +public class PineconeChannel : ChannelBase, IDisposable +{ + private readonly GrpcChannel _channel; + private readonly string _apiKey; + + /// + private PineconeChannel(GrpcChannel channel, string apiKey) : base(channel.Target) + { + _channel = channel; + _apiKey = apiKey; + } + + /// + public override CallInvoker CreateCallInvoker() => new PineconeCallInvoker(_channel, _apiKey); + + /// + /// Creates a for the specified configuration and index name + /// + /// the configuration to connect to Pinecone + /// the index name to connect to Pinecone + /// + public static PineconeChannel ForConfiguration(ClientConfiguration configuration, string indexName) + { + var channel = GrpcChannel.ForAddress( + $"https://{indexName}-{configuration.ProjectName}.svc.{configuration.Environment}.pinecone.io"); + return new PineconeChannel(channel, configuration.ApiKey); + } + + /// + public void Dispose() => _channel.Dispose(); +} diff --git a/tests/Pinecone.Grpc.Tests/DescribeIndexStatsTests.cs b/tests/Pinecone.Grpc.Tests/DescribeIndexStatsTests.cs new file mode 100644 index 0000000..ecb6bf7 --- /dev/null +++ b/tests/Pinecone.Grpc.Tests/DescribeIndexStatsTests.cs @@ -0,0 +1,27 @@ +using FluentAssertions; +using Xunit; + +namespace Pinecone.Grpc.Tests; + +[Collection("Pinecone")] +public class DescribeIndexStatsTests +{ + private readonly VectorService.VectorServiceClient? _client; + + public DescribeIndexStatsTests(PineconeFixture fixture) => + _client = fixture.VectorServiceClient; + + [Fact] + public void CanGetIndexStats() + { + var describeIndexStatsResponse = _client!.DescribeIndexStats(new DescribeIndexStatsRequest()); + describeIndexStatsResponse.Dimension.Should().BeGreaterThan(0); + } + + [Fact] + public async Task CanGetIndexStatsAsync() + { + var describeIndexStatsResponse = await _client!.DescribeIndexStatsAsync(new DescribeIndexStatsRequest()); + describeIndexStatsResponse.Dimension.Should().BeGreaterThan(0); + } +} diff --git a/tests/Pinecone.Grpc.Tests/Pinecone.Grpc.Tests.csproj b/tests/Pinecone.Grpc.Tests/Pinecone.Grpc.Tests.csproj new file mode 100644 index 0000000..f612e77 --- /dev/null +++ b/tests/Pinecone.Grpc.Tests/Pinecone.Grpc.Tests.csproj @@ -0,0 +1,27 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + diff --git a/tests/Pinecone.Grpc.Tests/PineconeFixture.cs b/tests/Pinecone.Grpc.Tests/PineconeFixture.cs new file mode 100644 index 0000000..a69dea5 --- /dev/null +++ b/tests/Pinecone.Grpc.Tests/PineconeFixture.cs @@ -0,0 +1,104 @@ +using System.Text.Json; +using Grpc.Core; +using Xunit; + +namespace Pinecone.Grpc.Tests; + +[CollectionDefinition("Pinecone")] +public sealed class PineconeCollection : ICollectionFixture { } + +public sealed class PineconeFixture : IAsyncLifetime +{ + private PineconeChannel _channel = null!; + private const string PineconeProjectname = "PINECONE_PROJECTNAME"; + private const string PineconeIndexname = "PINECONE_INDEXNAME"; + private const string PineconeEnvironment = "PINECONE_ENVIRONMENT"; + private const string PineconeApikey = "PINECONE_APIKEY"; + + private const string ProjectName = "projectName"; + private const string IndexName = "indexName"; + private const string Environment = "environment"; + private const string ApiKey = "apiKey"; + + public ClientConfiguration? Configuration { get; private set; } + + public VectorService.VectorServiceClient? VectorServiceClient { get; private set; } + + public Task InitializeAsync() + { + string? projectName; + string? indexName; + string? environment; + string? apiKey; + + var credentials = Path.Combine(SolutionPaths.Root, "credentials.json"); + if (File.Exists(credentials)) + { + var document = JsonDocument.Parse(File.ReadAllText(credentials)); + if (!document.RootElement.TryGetProperty(ProjectName, out var projectNameElement)) + ThrowCredentialsValueMissing(ProjectName); + + projectName = projectNameElement.GetString(); + if (string.IsNullOrEmpty(projectName)) + ThrowCredentialsValueEmpty(ProjectName); + + if (!document.RootElement.TryGetProperty(IndexName, out var indexNameElement)) + ThrowCredentialsValueMissing(IndexName); + + indexName = indexNameElement.GetString(); + if (string.IsNullOrEmpty(indexName)) + ThrowCredentialsValueEmpty(IndexName); + + if (!document.RootElement.TryGetProperty(Environment, out var environmentElement)) + ThrowCredentialsValueMissing(Environment); + + environment = environmentElement.GetString(); + if (string.IsNullOrEmpty(environment)) + ThrowCredentialsValueEmpty(Environment); + + if (!document.RootElement.TryGetProperty(ApiKey, out var apiKeyElement)) + ThrowCredentialsValueMissing(ApiKey); + + apiKey = apiKeyElement.GetString(); + if (string.IsNullOrEmpty(apiKey)) + ThrowCredentialsValueEmpty(ApiKey); + } + else + { + projectName = System.Environment.GetEnvironmentVariable(PineconeProjectname); + if (string.IsNullOrEmpty(projectName)) + ThrowEnvironmentVariableMissing(PineconeProjectname); + + indexName = System.Environment.GetEnvironmentVariable(PineconeIndexname); + if (string.IsNullOrEmpty(indexName)) + ThrowEnvironmentVariableMissing(PineconeIndexname); + + environment = System.Environment.GetEnvironmentVariable(PineconeEnvironment); + if (string.IsNullOrEmpty(environment)) + ThrowEnvironmentVariableMissing(PineconeEnvironment); + + apiKey = System.Environment.GetEnvironmentVariable(PineconeApikey); + if (string.IsNullOrEmpty(apiKey)) + ThrowEnvironmentVariableMissing(PineconeApikey); + } + + _channel = PineconeChannel.ForConfiguration(new ClientConfiguration(environment!, projectName!, apiKey!), indexName!); + VectorServiceClient = new VectorService.VectorServiceClient(_channel); + + return Task.CompletedTask; + } + + private void ThrowCredentialsValueMissing(string name) => + throw new Exception($"{name} property is missing from credentials.json"); + private void ThrowCredentialsValueEmpty(string name) => + throw new Exception($"{name} property is empty in credentials.json"); + + private void ThrowEnvironmentVariableMissing(string name) => + throw new Exception($"{name} environment variable is missing"); + + public Task DisposeAsync() + { + _channel!.Dispose(); + return Task.CompletedTask; + } +} diff --git a/tests/Pinecone.Grpc.Tests/SolutionPaths.cs b/tests/Pinecone.Grpc.Tests/SolutionPaths.cs new file mode 100644 index 0000000..a02f342 --- /dev/null +++ b/tests/Pinecone.Grpc.Tests/SolutionPaths.cs @@ -0,0 +1,25 @@ +namespace Pinecone.Grpc.Tests; + +public static class SolutionPaths +{ + private static readonly Lazy LazyRoot = new(FindSolutionRoot); + + private static string FindSolutionRoot() + { + var buildBat = "build.bat"; + var startDir = Directory.GetCurrentDirectory(); + var currentDirectory = new DirectoryInfo(startDir); + do + { + if (File.Exists(Path.Combine(currentDirectory.FullName, buildBat))) + return currentDirectory.FullName; + + currentDirectory = currentDirectory.Parent; + } while (currentDirectory != null); + + throw new InvalidOperationException( + $"Could not find solution root directory from the current directory {startDir}"); + } + + public static string Root => LazyRoot.Value; +} diff --git a/tests/Pinecone.Grpc.Tests/VersionTests.cs b/tests/Pinecone.Grpc.Tests/VersionTests.cs new file mode 100644 index 0000000..12d57f0 --- /dev/null +++ b/tests/Pinecone.Grpc.Tests/VersionTests.cs @@ -0,0 +1,18 @@ +using System.Configuration; +using System.Xml.Linq; +using FluentAssertions; +using Xunit; +using static Pinecone.Grpc.VectorService; + +namespace Pinecone.Grpc.Tests; + +public class VersionTests +{ + [Fact] + public void VersionMatchesBuildVersion() + { + var buildXml = XDocument.Load(Path.Combine(SolutionPaths.Root, "Directory.Build.props")); + var version = buildXml.Descendants(XName.Get("PineconeVersion", "http://schemas.microsoft.com/developer/msbuild/2003")).First().Value; + VectorServiceClient.PineconeVersion.Should().Be(version); + } +}