Compare commits
112 Commits
3.2.0
...
3.5.0-alpha.10
| Author | SHA1 | Date | |
|---|---|---|---|
| e9076b7dd3 | |||
| 45d3a08407 | |||
| 399b76e449 | |||
| f8aef65750 | |||
| a797184af8 | |||
| d6f6254a92 | |||
| f60f85b639 | |||
| bcdf73cc70 | |||
| 47e72ee1a7 | |||
| f3de5324db | |||
| 4dd6db886f | |||
| 4b82db8ea2 | |||
| 9e7f26f7a6 | |||
| b19f8c4219 | |||
| c517e61517 | |||
| b3e0623856 | |||
| e5d1ef2448 | |||
| 83c3de05fa | |||
| 507ded7d4a | |||
| e15029bab3 | |||
| a43fd44206 | |||
| 1bcd8ac3a4 | |||
| a8dc93e22b | |||
| 5a0f883b98 | |||
| a5d035671a | |||
| cd6ebad619 | |||
| 33c2e6e1a4 | |||
| b97702adb1 | |||
| 80c4f694ec | |||
| fb5042004f | |||
| c0a9291632 | |||
| b783d2acb6 | |||
| 93539adc1e | |||
| 98005933de | |||
| 50906b172a | |||
| 05f7353925 | |||
| 8328498553 | |||
| 59019bf846 | |||
| 3afaf61a1a | |||
| 424609fad0 | |||
| 46c067308e | |||
| 0e33e8df8f | |||
| bc81c21e9d | |||
| 7f8b59d348 | |||
| 44ba61e4a5 | |||
| 4f7b470901 | |||
| 8c6426d617 | |||
| 5562ce1a2d | |||
| 7019b8d7c6 | |||
| 58a0326060 | |||
| 55f83919d1 | |||
| 46c57b18be | |||
| 7b5ada57cd | |||
| e29b27bcd3 | |||
| ff1b688321 | |||
| 0be143d391 | |||
| c0a66a297a | |||
| aad604e819 | |||
| 48313cb082 | |||
| 0361a6e5b7 | |||
| 0e97782c29 | |||
| 298dedc3af | |||
| 422403d499 | |||
| b652ffa773 | |||
| 68ace02e2d | |||
| b6be7a351f | |||
| 1039e75d0c | |||
| 0f8752d5ab | |||
| 64a93345d6 | |||
| efc38d8f5c | |||
| e3ca75abe1 | |||
| b479d368ad | |||
| 8d3985f93b | |||
| 915a18dc98 | |||
| 5fcb3223d6 | |||
| 21851c06d2 | |||
| 3a9a633d30 | |||
| 7f092d529c | |||
| a4f0e0e4aa | |||
| 227729a0df | |||
| 178085f3f8 | |||
| 9794195e9c | |||
| a61c442930 | |||
| 68a407905d | |||
| 0f2abaf532 | |||
| 07634b6f6a | |||
| e938725d35 | |||
| d3369e3ce5 | |||
| d75a61d775 | |||
| 2ae4003afb | |||
| 24db4c4ae4 | |||
| edf63d4a1b | |||
| b5b0922e7f | |||
| ff390f772d | |||
| d69f0bba2a | |||
| 33c14fc14c | |||
| 536e58aacc | |||
| 88188aace6 | |||
| ad44a7cdbc | |||
| 38449dca9a | |||
| 764eb43838 | |||
| a84e6d89ca | |||
| 377829adae | |||
| a479440b66 | |||
| cc9639b179 | |||
| d44b4fa52b | |||
| ea6ca8c555 | |||
| 113f0fd551 | |||
| bcc4e25970 | |||
| b733ce5f29 | |||
| 1c8b2b82d7 | |||
| 11cd2dc1cb |
@@ -3,18 +3,11 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"csharpier": {
|
||||
"version": "0.30.6",
|
||||
"version": "1.0.2",
|
||||
"commands": [
|
||||
"dotnet-csharpier"
|
||||
],
|
||||
"rollForward": false
|
||||
},
|
||||
"gitversion.tool": {
|
||||
"version": "6.1.0",
|
||||
"commands": [
|
||||
"dotnet-gitversion"
|
||||
"csharpier"
|
||||
],
|
||||
"rollForward": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
Directory.Build.targets
|
||||
Directory.Build.props
|
||||
|
||||
**/bin/*
|
||||
**/obj/*
|
||||
_ReSharper.SharpCompress/
|
||||
bin/
|
||||
*.suo
|
||||
*.user
|
||||
TestArchives/Scratch/
|
||||
TestArchives/Scratch2/
|
||||
TestResults/
|
||||
*.nupkg
|
||||
packages/*/
|
||||
project.lock.json
|
||||
tests/TestArchives/Scratch
|
||||
.vs
|
||||
tools
|
||||
.vscode
|
||||
.idea/
|
||||
|
||||
.DS_Store
|
||||
*.snupkg
|
||||
coverage.xml
|
||||
|
||||
*.received.*
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
printWidth: 120
|
||||
useTabs: false
|
||||
tabWidth: 2
|
||||
indentSize: 2
|
||||
preprocessorSymbolSets:
|
||||
- ""
|
||||
- "DEBUG"
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# Coding standards, domain knowledge, and preferences that AI should follow
|
||||
|
||||
## C# Coding Standards
|
||||
|
||||
- Use the csharpier formatter for formatting C# code.
|
||||
- Use the .editorconfig file for code style settings.
|
||||
- Always use `var` when the type is obvious from the right side of the assignment.
|
||||
- Always add braces for `if`, `else`, `for`, `foreach`, `while`, and `do` statements, even if they are single-line statements.
|
||||
|
||||
## Testing
|
||||
|
||||
- Use xUnit for unit testing.
|
||||
- Use FluentAssertions for assertions in tests.
|
||||
- Use Moq for mocking dependencies in tests.
|
||||
@@ -0,0 +1,22 @@
|
||||
# Git Commit Instructions
|
||||
|
||||
To ensure high-quality and consistent commits, please follow these guidelines:
|
||||
|
||||
1. **Format your code**
|
||||
- Run the `csharpier` formatter on all C# files before committing.
|
||||
- Ensure your code adheres to the `.editorconfig` settings.
|
||||
|
||||
2. **Write clear commit messages**
|
||||
- Use the present tense ("Add feature" not "Added feature").
|
||||
- Start with a short summary (max 72 characters), followed by a blank line and a detailed description if necessary.
|
||||
|
||||
3. **Test your changes**
|
||||
- Run all unit tests before committing.
|
||||
- Add or update xUnit tests as needed.
|
||||
- Use AwesomeAssertions for assertions and Moq for mocking in tests.
|
||||
|
||||
4. **Review your changes**
|
||||
- Double-check for accidental debug code or commented-out code.
|
||||
- Ensure only relevant files are staged.
|
||||
|
||||
Thank you for helping maintain code quality!
|
||||
@@ -9,8 +9,6 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
@@ -21,9 +19,25 @@ jobs:
|
||||
with:
|
||||
path: ~/.nuget/packages
|
||||
key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
|
||||
|
||||
|
||||
- id: set-version
|
||||
name: Set version to output
|
||||
run: |
|
||||
SEMVER="3.0.99.${{ github.run_number }}"
|
||||
FILE_VERSION=$(echo "$SEMVER" | sed -E 's/^([0-9]+\.[0-9]+\.[0-9]+).*/\1/')
|
||||
FILE_VERSION="$FILE_VERSION.${{ github.run_number }}"
|
||||
|
||||
echo "semver=$SEMVER" >> "$GITHUB_OUTPUT"
|
||||
echo "fileVersion=$FILE_VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo $SEMVER
|
||||
echo $FILE_VERSION
|
||||
|
||||
- name: 🔫 Build All
|
||||
run: ./build.sh
|
||||
env:
|
||||
SEMVER: ${{ steps.set-version.outputs.SEMVER }}
|
||||
FILE_VERSION: ${{ steps.set-version.outputs.FILE_VERSION }}
|
||||
|
||||
- name: Upload coverage reports to Codecov with GitHub Action
|
||||
uses: codecov/codecov-action@v5
|
||||
|
||||
@@ -2,7 +2,6 @@ name: .NET Build and Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main", "dev"]
|
||||
tags: ["3.*"]
|
||||
|
||||
jobs:
|
||||
@@ -11,22 +10,40 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 8.x.x
|
||||
|
||||
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.nuget/packages
|
||||
key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
|
||||
|
||||
- id: set-version
|
||||
name: Set version to output
|
||||
run: |
|
||||
TAG=${{ github.ref_name }}
|
||||
if [[ "${{ github.ref }}" != refs/tags/* ]]; then
|
||||
TAG="3.0.99.${{ github.run_number }}"
|
||||
fi
|
||||
SEMVER="${TAG}"
|
||||
FILE_VERSION=$(echo "$TAG" | sed -E 's/^([0-9]+\.[0-9]+\.[0-9]+).*/\1/')
|
||||
FILE_VERSION="$FILE_VERSION.${{ github.run_number }}"
|
||||
|
||||
echo "semver=$SEMVER" >> "$GITHUB_OUTPUT"
|
||||
echo "fileVersion=$FILE_VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo $SEMVER
|
||||
echo $FILE_VERSION
|
||||
|
||||
- name: 🔫 Build and Pack
|
||||
run: ./build.sh pack
|
||||
|
||||
env:
|
||||
SEMVER: ${{ steps.set-version.outputs.SEMVER }}
|
||||
FILE_VERSION: ${{ steps.set-version.outputs.FILE_VERSION }}
|
||||
|
||||
- name: Upload coverage reports to Codecov with GitHub Action
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
|
||||
@@ -15,6 +15,7 @@ tests/TestArchives/Scratch
|
||||
tools
|
||||
.vscode
|
||||
.idea/
|
||||
.volumes/
|
||||
|
||||
.DS_Store
|
||||
*.snupkg
|
||||
|
||||
+8
-21
@@ -1,5 +1,4 @@
|
||||
<Project>
|
||||
|
||||
<PropertyGroup Label="Compiler Properties">
|
||||
<LangVersion>12</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
@@ -7,7 +6,6 @@
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Nugetspec Package Properties">
|
||||
<!-- Defines common Nugetspec properties -->
|
||||
<!-- Inheriting packable projects should define the rest of the nugetspec properties (PackageId, Description) -->
|
||||
@@ -22,18 +20,16 @@
|
||||
<PackageTags>speckle</PackageTags>
|
||||
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Nuget Package Properties">
|
||||
<IsPackable>false</IsPackable> <!--Can be set to true in inheriting .props/.csproj files for projects that should be packed-->
|
||||
<IsPackable>false</IsPackable>
|
||||
<!--Can be set to true in inheriting .props/.csproj files for projects that should be packed-->
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
|
||||
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Analyers">
|
||||
<EnableNetAnalyzers>true</EnableNetAnalyzers>
|
||||
<AnalysisLevel>latest-AllEnabledByDefault</AnalysisLevel>
|
||||
@@ -41,7 +37,6 @@
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<AllowUnsafeBlocks>false</AllowUnsafeBlocks>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
|
||||
<!-- Ingored warnings, some aspirational but too noisy for now, some by design. -->
|
||||
<NoWarn>
|
||||
<!--Disabled by design-->
|
||||
@@ -59,28 +54,20 @@
|
||||
<!-- Aspirational -->
|
||||
CA1502;CA1716;NETSDK1206;
|
||||
$(NoWarn)
|
||||
</NoWarn>
|
||||
</NoWarn
|
||||
>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- Expose the repository root to all projects -->
|
||||
<RepositoryRoot>$(MSBuildThisFileDirectory)</RepositoryRoot>
|
||||
</PropertyGroup>
|
||||
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\..\README.md" Pack="true" PackagePath="\"/>
|
||||
<None
|
||||
Condition="'$(IsPackable)' == 'true'"
|
||||
Include="..\..\logo.png"
|
||||
Pack="true"
|
||||
PackagePath="\"
|
||||
Visible="false"/>
|
||||
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
|
||||
<None Condition="'$(IsPackable)' == 'true'" Include="..\..\logo.png" Pack="true" PackagePath="\" Visible="false" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- This file contains the configuration for some analyzer warnings, such as cyclomatic
|
||||
complexity threshold -->
|
||||
<AdditionalFiles Include="$(RepositoryRoot)CodeMetricsConfig.txt"/>
|
||||
<AdditionalFiles Include="$(RepositoryRoot)CodeMetricsConfig.txt" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
+13
-14
@@ -1,18 +1,17 @@
|
||||
<Project>
|
||||
<PropertyGroup Condition="'$(IsTestProject)' == 'true'">
|
||||
<NoWarn>
|
||||
$(NoWarn);
|
||||
<!-- Things we need to test -->
|
||||
CS0618;CA1034;CA2201;CA1051;CA1040;CA1724;
|
||||
IDE0044;IDE0130;CA1508;
|
||||
<!-- Analysers that provide no tangeable value to a test project -->
|
||||
CA5394;CA2007;CA1852;CA1819;CA1711;CA1063;CA1816;CA2234;CS8618;CA1054;CA1810;CA2208;CA1019;CA1831;
|
||||
</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(IsTestProject)' == 'true'">
|
||||
<NoWarn>
|
||||
<!-- Things we need to test -->
|
||||
CS0618;CA1034;CA2201;CA1051;CA1040;CA1724;
|
||||
IDE0044;IDE0130;CA1508;
|
||||
<!-- Analysers that provide no tangeable value to a test project -->
|
||||
CA5394;CA2007;CA1852;CA1819;CA1711;CA1063;CA1816;CA2234;CS8618;CA1054;CA1810;CA2208;CA1019;CA1831;
|
||||
$(NoWarn);
|
||||
</NoWarn>
|
||||
</PropertyGroup>
|
||||
<Target Name="DeepClean">
|
||||
<Message Text="Deep clean of $(MSBuildProjectName).csproj" Importance="high"/>
|
||||
<RemoveDir Directories="$(BaseIntermediateOutputPath)"/>
|
||||
<RemoveDir Directories="$(BaseOutputPath)"/>
|
||||
<Message Text="Deep clean of $(MSBuildProjectName).csproj" Importance="high" />
|
||||
<RemoveDir Directories="$(BaseIntermediateOutputPath)" />
|
||||
<RemoveDir Directories="$(BaseOutputPath)" />
|
||||
</Target>
|
||||
</Project>
|
||||
|
||||
@@ -11,14 +11,15 @@
|
||||
<PackageVersion Include="Microsoft.CSharp" Version="4.7.0" />
|
||||
<!-- Keep at exactly 7.0.5 for side by side with V2 -->
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="[7.0.5,)" />
|
||||
<PackageVersion Include="Microsoft.Extensions.ObjectPool" Version="9.0.3" />
|
||||
<PackageVersion Include="Microsoft.Extensions.ObjectPool" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="[2.2.0,)" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="[2.2.0,)" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="[2.2.0,)" />
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="[5.0.0,)" />
|
||||
<PackageVersion Include="Moq" Version="4.20.72" />
|
||||
<PackageVersion Include="Open.ChannelExtensions" Version="9.0.0" />
|
||||
<PackageVersion Include="Newtonsoft.Json.Schema" Version="4.0.1" />
|
||||
<PackageVersion Include="Open.ChannelExtensions" Version="9.1.0" />
|
||||
<PackageVersion Include="Polly" Version="7.2.3" />
|
||||
<PackageVersion Include="Polly.Contrib.WaitAndRetry" Version="1.1.1" />
|
||||
<PackageVersion Include="Polly.Extensions.Http" Version="3.0.0" />
|
||||
@@ -26,9 +27,11 @@
|
||||
<PackageVersion Include="Speckle.Newtonsoft.Json" Version="13.0.2" />
|
||||
<PackageVersion Include="Speckle.DoubleNumerics" Version="4.1.0" />
|
||||
<PackageVersion Include="SimpleExec" Version="12.0.0" />
|
||||
<PackageVersion Include="System.Threading.Channels" Version="9.0.2" />
|
||||
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||
<PackageVersion Include="System.Threading.Channels" Version="9.0.4" />
|
||||
<PackageVersion Include="Verify.Quibble" Version="2.1.1" />
|
||||
<PackageVersion Include="Verify.Xunit" Version="29.2.0" />
|
||||
<PackageVersion Include="Verify.Xunit" Version="29.4.0" />
|
||||
<PackageVersion Include="System.Text.Json" Version="8.0.5" />
|
||||
<PackageVersion Include="xunit" Version="2.9.3" />
|
||||
<PackageVersion Include="xunit.assert" Version="2.9.3" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="3.0.2" />
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
workflow: GitFlow/v1
|
||||
next-version: 3.0.0
|
||||
branches:
|
||||
main:
|
||||
prevent-increment:
|
||||
when-current-commit-tagged: true
|
||||
@@ -18,8 +18,9 @@ Speckle | Sharp | SDK
|
||||
This repo is the home of our next-generation Speckle .NET SDK. It uses .NET Standard 2.0 and has been tested on Windows and MacOS.
|
||||
|
||||
- **SDK**
|
||||
- [`Speckle.Sdk`](https://github.com/specklesystems/speckle-sharp-sdk/tree/dev/src/Speckle.Sdk): Transports, serialization, API wrappers, and logging.
|
||||
- [`Speckle.Sdk`](https://github.com/specklesystems/speckle-sharp-sdk/tree/dev/src/Speckle.Sdk): Send/Receive operations, Serialization, API wrappers, and more!.
|
||||
- [`Speckle.Sdk.Dependencies`](https://github.com/specklesystems/speckle-sharp-sdk/tree/dev/src/Speckle.Sdk.Dependencies): Dependencies and code that shouldn't cause conflicts in Host Apps. This uses [IL Repack](https://github.com/gluck/il-repack) to merge together and interalized only to be used by Speckle.
|
||||
- [`Speckle.Automate.Sdk`](https://github.com/specklesystems/speckle-sharp-sdk/tree/dev/src/Speckle.Automate.Sdk): .NET SDK for [Speckle Automate](https://www.speckle.systems/product/automate)
|
||||
- **Speckle Objects**
|
||||
- [`Speckle.Objects`](https://github.com/specklesystems/speckle-sharp-sdk/tree/dev/src/Speckle.Objects): The Speckle Objects classes used for conversions.
|
||||
- **Tests**
|
||||
|
||||
+17
-1
@@ -9,6 +9,9 @@ EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Speckle.Objects.Tests.Unit", "tests\Speckle.Objects.Tests.Unit\Speckle.Objects.Tests.Unit.csproj", "{A0338FC0-3011-498F-AD09-01230FABD3ED}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{5CB96C27-FC5B-4A41-86B6-951AF99B8116}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
src\graphql.config.yml = src\graphql.config.yml
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{35047EE7-AD1D-4741-80A7-8F0E874718E9}"
|
||||
EndProject
|
||||
@@ -20,7 +23,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "config", "config", "{DA2AED
|
||||
Directory.Packages.props = Directory.Packages.props
|
||||
global.json = global.json
|
||||
README.md = README.md
|
||||
GitVersion.yml = GitVersion.yml
|
||||
docker-compose.yml = docker-compose.yml
|
||||
CodeMetricsConfig.txt = CodeMetricsConfig.txt
|
||||
Directory.Build.Targets = Directory.Build.Targets
|
||||
@@ -51,6 +53,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Speckle.Sdk.Testing", "test
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "performance", "performance", "{FFB07238-87E8-463A-AA39-3B38AAAA94C1}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Speckle.Automate.Sdk", "src\Speckle.Automate.Sdk\Speckle.Automate.Sdk.csproj", "{4EB20EFA-5A38-415E-B3FD-29CA3ACD1EF5}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Speckle.Automate.Sdk.Integration", "tests\Speckle.Automate.Sdk.Integration\Speckle.Automate.Sdk.Integration.csproj", "{B6129DC3-F285-4E5F-85E2-6D2533A4005E}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -105,6 +111,14 @@ Global
|
||||
{7B617C0D-2354-415C-993C-5071D4113E27}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7B617C0D-2354-415C-993C-5071D4113E27}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7B617C0D-2354-415C-993C-5071D4113E27}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{4EB20EFA-5A38-415E-B3FD-29CA3ACD1EF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4EB20EFA-5A38-415E-B3FD-29CA3ACD1EF5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4EB20EFA-5A38-415E-B3FD-29CA3ACD1EF5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4EB20EFA-5A38-415E-B3FD-29CA3ACD1EF5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B6129DC3-F285-4E5F-85E2-6D2533A4005E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B6129DC3-F285-4E5F-85E2-6D2533A4005E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B6129DC3-F285-4E5F-85E2-6D2533A4005E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B6129DC3-F285-4E5F-85E2-6D2533A4005E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{A413E196-3696-4F48-B635-04B5F76BF9C9} = {5CB96C27-FC5B-4A41-86B6-951AF99B8116}
|
||||
@@ -119,5 +133,7 @@ Global
|
||||
{7B617C0D-2354-415C-993C-5071D4113E27} = {35047EE7-AD1D-4741-80A7-8F0E874718E9}
|
||||
{FF922B6D-D416-4348-8CB8-0C8B28691070} = {FFB07238-87E8-463A-AA39-3B38AAAA94C1}
|
||||
{870E3396-E6F7-43AE-B120-E651FA4F46BD} = {FFB07238-87E8-463A-AA39-3B38AAAA94C1}
|
||||
{4EB20EFA-5A38-415E-B3FD-29CA3ACD1EF5} = {5CB96C27-FC5B-4A41-86B6-951AF99B8116}
|
||||
{B6129DC3-F285-4E5F-85E2-6D2533A4005E} = {35047EE7-AD1D-4741-80A7-8F0E874718E9}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<Solution>
|
||||
<Folder Name="/build/">
|
||||
<Project Path="build/build.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/config/">
|
||||
<File Path=".config/dotnet-tools.json" />
|
||||
<File Path=".csharpierrc.yaml" />
|
||||
<File Path=".editorconfig" />
|
||||
<File Path="CodeMetricsConfig.txt" />
|
||||
<File Path="Directory.Build.props" />
|
||||
<File Path="Directory.Build.Targets" />
|
||||
<File Path="Directory.Packages.props" />
|
||||
<File Path="docker-compose.yml" />
|
||||
<File Path="global.json" />
|
||||
<File Path="README.md" />
|
||||
<File Path=".github\copilot-instructions.md" />
|
||||
<File Path=".github\git-commit-instructions.md" />
|
||||
</Folder>
|
||||
<Folder Name="/config/workflows/">
|
||||
<File Path=".github/workflows/pr.yml" />
|
||||
<File Path=".github/workflows/release.yml" />
|
||||
</Folder>
|
||||
<Folder Name="/performance/">
|
||||
<Project Path="tests/Speckle.Sdk.Serialization.Testing/Speckle.Sdk.Serialization.Testing.csproj" />
|
||||
<Project Path="tests/Speckle.Sdk.Tests.Performance/Speckle.Sdk.Tests.Performance.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/src/">
|
||||
<Project Path="src/Speckle.Automate.Sdk/Speckle.Automate.Sdk.csproj" />
|
||||
<Project Path="src/Speckle.Objects/Speckle.Objects.csproj" />
|
||||
<Project Path="src/Speckle.Sdk.Dependencies/Speckle.Sdk.Dependencies.csproj" />
|
||||
<Project Path="src/Speckle.Sdk/Speckle.Sdk.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/tests/">
|
||||
<Project Path="tests/Speckle.Sdk.Testing/Speckle.Sdk.Testing.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/tests/integration/">
|
||||
<Project Path="tests/Speckle.Automate.Sdk.Integration/Speckle.Automate.Sdk.Integration.csproj" />
|
||||
<Project Path="tests/Speckle.Sdk.Tests.Integration/Speckle.Sdk.Tests.Integration.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/tests/unit/">
|
||||
<Project Path="tests/Speckle.Objects.Tests.Unit/Speckle.Objects.Tests.Unit.csproj" />
|
||||
<Project Path="tests/Speckle.Sdk.Serialization.Tests/Speckle.Sdk.Serialization.Tests.csproj" />
|
||||
<Project Path="tests/Speckle.Sdk.Tests.Unit/Speckle.Sdk.Tests.Unit.csproj" />
|
||||
</Folder>
|
||||
</Solution>
|
||||
+9
-11
@@ -1,4 +1,3 @@
|
||||
using System.Text.Json;
|
||||
using GlobExpressions;
|
||||
using static Bullseye.Targets;
|
||||
using static SimpleExec.Command;
|
||||
@@ -16,14 +15,13 @@ const string CLEAN_LOCKS = "clean-locks";
|
||||
const string PERF = "perf";
|
||||
const string DEEP_CLEAN = "deep-clean";
|
||||
|
||||
static async Task<(string, string)> GetVersions()
|
||||
static (string semver, string fileVerison) GetVersions()
|
||||
{
|
||||
var (output, _) = await ReadAsync("dotnet", "dotnet-gitversion /output json").ConfigureAwait(false);
|
||||
output = output.Trim();
|
||||
var jDoc = JsonDocument.Parse(output);
|
||||
var version = jDoc.RootElement.GetProperty("FullSemVer").GetString() ?? "3.0.0-localBuild";
|
||||
var fileVersion = jDoc.RootElement.GetProperty("AssemblySemFileVer").GetString() ?? "3.0.0.0";
|
||||
return (version, fileVersion);
|
||||
string semver =
|
||||
Environment.GetEnvironmentVariable("SEMVER") ?? throw new ArgumentException("Expected SEMVER env var");
|
||||
string fileVersion =
|
||||
Environment.GetEnvironmentVariable("FILE_VERSION") ?? throw new ArgumentException("Expected FILE_VERSION env var");
|
||||
return (semver, fileVersion);
|
||||
}
|
||||
|
||||
Target(
|
||||
@@ -68,7 +66,7 @@ Target(
|
||||
|
||||
Target(RESTORE_TOOLS, () => RunAsync("dotnet", "tool restore"));
|
||||
|
||||
Target(FORMAT, dependsOn: [RESTORE_TOOLS], () => RunAsync("dotnet", "csharpier --check ."));
|
||||
Target(FORMAT, dependsOn: [RESTORE_TOOLS], () => RunAsync("dotnet", "csharpier check ."));
|
||||
|
||||
Target(RESTORE, dependsOn: [FORMAT], () => RunAsync("dotnet", "restore Speckle.Sdk.sln --locked-mode"));
|
||||
|
||||
@@ -77,7 +75,7 @@ Target(
|
||||
dependsOn: [RESTORE],
|
||||
async () =>
|
||||
{
|
||||
var (version, fileVersion) = await GetVersions().ConfigureAwait(false);
|
||||
var (version, fileVersion) = GetVersions();
|
||||
Console.WriteLine($"Version: {version} & {fileVersion}");
|
||||
await RunAsync(
|
||||
"dotnet",
|
||||
@@ -174,7 +172,7 @@ Target(
|
||||
async () =>
|
||||
{
|
||||
{
|
||||
var (version, fileVersion) = await GetVersions().ConfigureAwait(false);
|
||||
var (version, fileVersion) = GetVersions();
|
||||
Console.WriteLine($"Version: {version} & {fileVersion}");
|
||||
await RunAsync("dotnet", $"pack Speckle.Sdk.sln -c Release -o output --no-build -p:Version={version}")
|
||||
.ConfigureAwait(false);
|
||||
|
||||
+1
-2
@@ -1,10 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Bullseye" />
|
||||
<PackageReference Include="Glob" />
|
||||
<PackageReference Include="SimpleExec" />
|
||||
|
||||
+15
-6
@@ -1,4 +1,3 @@
|
||||
version: "3.9"
|
||||
name: "speckle-server"
|
||||
|
||||
services:
|
||||
@@ -22,7 +21,7 @@ services:
|
||||
retries: 30
|
||||
|
||||
redis:
|
||||
image: "redis:6.0-alpine"
|
||||
image: "valkey/valkey:8.1-alpine@sha256:0d27f0bca0249f61d060029a6aaf2e16b2c417d68d02a508e1dfb763fa2948b4"
|
||||
restart: always
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
@@ -38,6 +37,9 @@ services:
|
||||
restart: always
|
||||
volumes:
|
||||
- minio-data:/data
|
||||
ports:
|
||||
- '127.0.0.1:9000:9000'
|
||||
- '127.0.0.1:9001:9001'
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
@@ -53,11 +55,11 @@ services:
|
||||
image: speckle/speckle-server:latest
|
||||
restart: always
|
||||
healthcheck:
|
||||
test:
|
||||
test:
|
||||
- CMD
|
||||
- /nodejs/bin/node
|
||||
- -e
|
||||
- "try { require('node:http').request({headers: {'Content-Type': 'application/json'}, port:3000, hostname:'127.0.0.1', path:'/readiness', method: 'GET', timeout: 2000 }, (res) => { body = ''; res.on('data', (chunk) => {body += chunk;}); res.on('end', () => {process.exit(res.statusCode != 200 || body.toLowerCase().includes('error'));}); }).end(); } catch { process.exit(1); }"
|
||||
- "try { require('node:http').request({headers: {'Content-Type': 'application/json'}, port:3000, hostname:'127.0.0.1', path:'/readiness', method: 'GET', timeout: 2000 }, (res) => { body = ''; res.on('data', (chunk) => {body += chunk;}); res.on('end', () => {process.exit(Number(res.statusCode != 200 || body.toLowerCase().includes('error')));}); }).end(); } catch { process.exit(1); }"
|
||||
interval: 10s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
@@ -79,8 +81,9 @@ services:
|
||||
|
||||
# TODO: Change thvolumes:
|
||||
REDIS_URL: "redis://redis"
|
||||
|
||||
|
||||
S3_ENDPOINT: "http://minio:9000"
|
||||
S3_PUBLIC_ENDPOINT: 'http://127.0.0.1:9000'
|
||||
S3_ACCESS_KEY: "minioadmin"
|
||||
S3_SECRET_KEY: "minioadmin"
|
||||
S3_BUCKET: "speckle-server"
|
||||
@@ -100,6 +103,12 @@ services:
|
||||
POSTGRES_PASSWORD: "speckle"
|
||||
POSTGRES_DB: "speckle"
|
||||
ENABLE_MP: "false"
|
||||
|
||||
LOG_PRETTY: "true"
|
||||
|
||||
FF_NEXT_GEN_FILE_IMPORTER_ENABLED: "true"
|
||||
FF_LARGE_FILE_IMPORTS_ENABLED: "true"
|
||||
|
||||
|
||||
networks:
|
||||
default:
|
||||
@@ -108,4 +117,4 @@ networks:
|
||||
volumes:
|
||||
postgres-data:
|
||||
redis-data:
|
||||
minio-data:
|
||||
minio-data:
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using GraphQL;
|
||||
using GraphQL.Client.Http;
|
||||
using Speckle.Automate.Sdk.Schema;
|
||||
using Speckle.InterfaceGenerator;
|
||||
using Speckle.Sdk.Api;
|
||||
using Speckle.Sdk.Credentials;
|
||||
|
||||
namespace Speckle.Automate.Sdk;
|
||||
|
||||
[GenerateAutoInterface(VisibilityModifier = "public")]
|
||||
internal sealed class AutomationContextFactory(
|
||||
IClientFactory clientFactory,
|
||||
IAccountFactory accountFactory,
|
||||
IOperations operations
|
||||
) : IAutomationContextFactory
|
||||
{
|
||||
private static readonly JsonSerializerOptions s_jsonSerializerSettings = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
};
|
||||
|
||||
/// <inheritdoc cref="Initialize(AutomationRunData, string)"/>
|
||||
public async Task<IAutomationContext> Initialize(string automationRunData, string speckleToken)
|
||||
{
|
||||
var runData = JsonSerializer.Deserialize<AutomationRunData>(automationRunData, s_jsonSerializerSettings);
|
||||
|
||||
return await Initialize(runData, speckleToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Initialize(AutomationRunData, Account)"/>
|
||||
/// <exception cref="GraphQLHttpRequestException">Request failed on the HTTP layer (received a non-successful response code)</exception>
|
||||
/// <exception cref="AggregateException"><inheritdoc cref="Speckle.Sdk.Api.GraphQL.GraphQLErrorHandler.EnsureGraphQLSuccess(IGraphQLResponse)"/></exception>
|
||||
public async Task<IAutomationContext> Initialize(AutomationRunData automationRunData, string speckleToken)
|
||||
{
|
||||
Account account = await accountFactory
|
||||
.CreateAccount(automationRunData.SpeckleServerUrl, speckleToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return Initialize(automationRunData, account);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="AutomationContext"/> from the provided data
|
||||
/// </summary>
|
||||
public IAutomationContext Initialize(AutomationRunData automationRunData, Account account)
|
||||
{
|
||||
IClient client = clientFactory.Create(account);
|
||||
Stopwatch initTime = Stopwatch.StartNew();
|
||||
|
||||
return new AutomationContext(operations)
|
||||
{
|
||||
AutomationRunData = automationRunData,
|
||||
SpeckleClient = client,
|
||||
_speckleToken = account.token,
|
||||
_initTime = initTime,
|
||||
AutomationResult = new AutomationResult(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using GraphQL;
|
||||
using Speckle.Automate.Sdk.Schema;
|
||||
using Speckle.Automate.Sdk.Schema.Triggers;
|
||||
using Speckle.InterfaceGenerator;
|
||||
using Speckle.Newtonsoft.Json;
|
||||
using Speckle.Sdk;
|
||||
using Speckle.Sdk.Api;
|
||||
using Speckle.Sdk.Api.GraphQL.Inputs;
|
||||
using Speckle.Sdk.Api.GraphQL.Models;
|
||||
using Speckle.Sdk.Common;
|
||||
using Speckle.Sdk.Models;
|
||||
using Version = Speckle.Sdk.Api.GraphQL.Models.Version;
|
||||
|
||||
namespace Speckle.Automate.Sdk;
|
||||
|
||||
[GenerateAutoInterface(VisibilityModifier = "public")]
|
||||
internal sealed class AutomationContext(IOperations operations) : IAutomationContext
|
||||
{
|
||||
public AutomationRunData AutomationRunData { get; set; }
|
||||
public string? ContextView
|
||||
{
|
||||
get => AutomationResult.ResultView;
|
||||
private set => AutomationResult.ResultView = value;
|
||||
}
|
||||
public required IClient SpeckleClient { get; init; }
|
||||
|
||||
public required string _speckleToken { get; init; }
|
||||
|
||||
// added for performance measuring
|
||||
public required Stopwatch _initTime { get; init; }
|
||||
|
||||
public required AutomationResult AutomationResult { get; init; }
|
||||
|
||||
public string RunStatus => AutomationResult.RunStatus;
|
||||
|
||||
public string? StatusMessage => AutomationResult.StatusMessage;
|
||||
public TimeSpan Elapsed => _initTime.Elapsed;
|
||||
|
||||
/// <summary>
|
||||
/// Receive version for automation.
|
||||
/// </summary>
|
||||
/// <returns> Commit object. </returns>
|
||||
/// <exception cref="SpeckleException">Throws if commit object is null.</exception>
|
||||
public async Task<Base> ReceiveVersion(CancellationToken cancellationToken = default)
|
||||
{
|
||||
// TODO: this is a quick hack to keep implementation consistency. Move to proper receive many versions
|
||||
if (AutomationRunData.Triggers.First() is not VersionCreationTrigger trigger)
|
||||
{
|
||||
throw new SpeckleException("Processed automation run data without any triggers");
|
||||
}
|
||||
var versionId = trigger.Payload.VersionId;
|
||||
|
||||
var version = await SpeckleClient
|
||||
.Version.Get(versionId, AutomationRunData.ProjectId, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (version.referencedObject == null)
|
||||
{
|
||||
throw new SpeckleException(
|
||||
"The requested speckle model version has exceeded workspace version history limits or the reference object is otherwise null"
|
||||
);
|
||||
}
|
||||
|
||||
Base? rootObject = await operations
|
||||
.Receive2(
|
||||
SpeckleClient.ServerUrl,
|
||||
AutomationRunData.ProjectId,
|
||||
version.referencedObject,
|
||||
SpeckleClient.Account.token,
|
||||
null,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
Console.WriteLine($"It took {Elapsed.TotalSeconds} seconds to receive the speckle version {versionId}");
|
||||
return rootObject;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates new version in the project.
|
||||
/// </summary>
|
||||
/// <param name="rootObject">Object to send to project.</param>
|
||||
/// <param name="model">The model to create the version under</param>
|
||||
/// <param name="versionMessage">Version message.</param>
|
||||
/// <param name="cancellationToken">Version message.</param>
|
||||
/// <returns>Version id.</returns>
|
||||
/// <exception cref="SpeckleException"> Throws if given model name is as same as with model name in automation run data.
|
||||
/// The reason is to prevent circular run loop in automation.</exception>
|
||||
public async Task<Version> CreateNewVersionInProject(
|
||||
Base rootObject,
|
||||
Model model,
|
||||
string versionMessage = "",
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
// Confirm target branch is not the same as source branch
|
||||
foreach (var trigger in AutomationRunData.Triggers)
|
||||
{
|
||||
if (trigger.Payload.ModelId == model.id)
|
||||
{
|
||||
throw new SpeckleException(
|
||||
$"""
|
||||
The target model: {model.name} ({model.id}) cannot match the model
|
||||
that triggered this automation:
|
||||
{trigger.Payload.ModelId}
|
||||
"""
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
var (rootObjectId, _) = await operations
|
||||
.Send2(
|
||||
SpeckleClient.ServerUrl,
|
||||
AutomationRunData.ProjectId,
|
||||
SpeckleClient.Account.token,
|
||||
rootObject,
|
||||
null,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var newVersion = await SpeckleClient
|
||||
.Version.Create(
|
||||
new CreateVersionInput(rootObjectId, model.id, AutomationRunData.ProjectId, versionMessage),
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
AutomationResult.ResultVersions.Add(newVersion.id);
|
||||
|
||||
return newVersion;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set context view for automation result view.
|
||||
/// </summary>
|
||||
/// <param name="resourceIds"> Resource contexts to bind into view.</param>
|
||||
/// <param name="includeSourceModelVersion"> Whether bind source version into result view or not.</param>
|
||||
/// <exception cref="SpeckleException"> Throws if there is no context to create result view.</exception>
|
||||
[MemberNotNull(nameof(ContextView))]
|
||||
[AutoInterfaceIgnore] //Ignore so we can explicitly add the MemberNotNull attibute to the interface method
|
||||
public void SetContextView(IReadOnlyCollection<string>? resourceIds = null, bool includeSourceModelVersion = true)
|
||||
{
|
||||
List<string> linkResources = new();
|
||||
if (includeSourceModelVersion)
|
||||
{
|
||||
foreach (var trigger in AutomationRunData.Triggers)
|
||||
{
|
||||
switch (trigger)
|
||||
{
|
||||
case VersionCreationTrigger versionCreationTrigger:
|
||||
{
|
||||
linkResources.Add($"{versionCreationTrigger.Payload.ModelId}@{versionCreationTrigger.Payload.VersionId}");
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
throw new SpeckleException($"Could not link resource specified by {trigger.TriggerType} trigger");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (resourceIds is not null)
|
||||
{
|
||||
linkResources.AddRange(resourceIds);
|
||||
}
|
||||
|
||||
if (linkResources.Count == 0)
|
||||
{
|
||||
throw new SpeckleException("We do not have enough resource ids to compose a context view");
|
||||
}
|
||||
|
||||
ContextView = $"/projects/{AutomationRunData.ProjectId}/models/{string.Join(",", linkResources)}";
|
||||
}
|
||||
|
||||
public async Task ReportRunStatus()
|
||||
{
|
||||
ObjectResults? objectResults = null;
|
||||
if (RunStatus is "SUCCEEDED" or "FAILED")
|
||||
{
|
||||
objectResults = new ObjectResults
|
||||
{
|
||||
Version = 2,
|
||||
Values = new ObjectResultValues
|
||||
{
|
||||
BlobIds = AutomationResult.Blobs,
|
||||
ObjectResults = AutomationResult.ObjectResults,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
mutation AutomateFunctionRunStatusReport($projectId: String!, $functionRunId: String!, $status: AutomateRunStatus!, $statusMessage: String, $results: JSONObject, $contextView: String) {
|
||||
automateFunctionRunStatusReport(
|
||||
input: {projectId: $projectId, functionRunId: $functionRunId, status: $status, statusMessage: $statusMessage, contextView: $contextView, results: $results}
|
||||
)
|
||||
}
|
||||
""";
|
||||
GraphQLRequest request = new()
|
||||
{
|
||||
Query = QUERY,
|
||||
Variables = new
|
||||
{
|
||||
projectId = AutomationRunData.ProjectId,
|
||||
functionRunId = AutomationRunData.FunctionRunId,
|
||||
status = RunStatus,
|
||||
statusMessage = AutomationResult.StatusMessage,
|
||||
contextView = ContextView,
|
||||
results = objectResults,
|
||||
},
|
||||
};
|
||||
await SpeckleClient.ExecuteGraphQLRequest<Dictionary<string, object>>(request).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores result file in automation result. It will be available to download on Frontend if added.
|
||||
/// </summary>
|
||||
/// <param name="filePath"> File path to store.</param>
|
||||
/// <exception cref="FileNotFoundException"> Throws if given file path is not exist.</exception>
|
||||
/// <exception cref="SpeckleException"> Throws if upload requests return no result.</exception>
|
||||
public async Task StoreFileResult(string filePath)
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
throw new FileNotFoundException("The given file path doesn't exist", fileName: filePath);
|
||||
}
|
||||
|
||||
using MultipartFormDataContent formData = new();
|
||||
FileStream fileStream = new(filePath, FileMode.Open, FileAccess.Read);
|
||||
using StreamContent streamContent = new(fileStream);
|
||||
formData.Add(streamContent, "files", Path.GetFileName(filePath));
|
||||
HttpResponseMessage request = await SpeckleClient
|
||||
.GQLClient.HttpClient.PostAsync(
|
||||
new Uri(AutomationRunData.SpeckleServerUrl, $"api/stream/{AutomationRunData.ProjectId}/blob"),
|
||||
formData
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
request.EnsureSuccessStatusCode();
|
||||
string responseString = await request.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
Console.WriteLine("RESPONSE - " + responseString);
|
||||
BlobUploadResponse uploadResponse = JsonConvert.DeserializeObject<BlobUploadResponse>(responseString);
|
||||
if (uploadResponse.UploadResults.Count != 1)
|
||||
{
|
||||
throw new SpeckleException("Expected one upload result.");
|
||||
}
|
||||
|
||||
AutomationResult.Blobs.AddRange(uploadResponse.UploadResults.Select(r => r.BlobId));
|
||||
}
|
||||
|
||||
private void MarkRun(AutomationStatus status, string? statusMessage)
|
||||
{
|
||||
double duration = Elapsed.TotalSeconds;
|
||||
AutomationResult.StatusMessage = statusMessage;
|
||||
string statusValue = AutomationStatusMapping.Get(status);
|
||||
AutomationResult.RunStatus = statusValue;
|
||||
AutomationResult.Elapsed = duration;
|
||||
|
||||
string msg = $"Automation run {statusValue} after {duration} seconds.";
|
||||
if (statusMessage is not null)
|
||||
{
|
||||
msg += $"\n{statusMessage}";
|
||||
}
|
||||
|
||||
Console.WriteLine(msg);
|
||||
}
|
||||
|
||||
public void MarkRunFailed(string statusMessage) => MarkRun(AutomationStatus.Failed, statusMessage);
|
||||
|
||||
public void MarkRunException(string? statusMessage) => MarkRun(AutomationStatus.Exception, statusMessage);
|
||||
|
||||
public void MarkRunSuccess(string? statusMessage) => MarkRun(AutomationStatus.Succeeded, statusMessage);
|
||||
|
||||
/// <summary>
|
||||
/// Add a new error case to the run results.
|
||||
/// </summary>
|
||||
/// <param name="category">A short tag for the error type.</param>
|
||||
/// <param name="affectedObjects">A list of objects that are causing the result.</param>
|
||||
/// <param name="message">Optional error message.</param>
|
||||
/// <param name="metadata">User provided metadata key value pairs.</param>
|
||||
/// <param name="visualOverrides">Case specific 3D visual overrides.</param>
|
||||
/// <exception cref="ArgumentException">Throws if the provided <paramref name="affectedObjects"/> input is empty.</exception>
|
||||
public void AttachErrorToObjects(
|
||||
string category,
|
||||
IReadOnlyCollection<Base> affectedObjects,
|
||||
string? message = null,
|
||||
Dictionary<string, object>? metadata = null,
|
||||
Dictionary<string, object>? visualOverrides = null
|
||||
) => AttachResultToObjects(ObjectResultLevel.Error, category, affectedObjects, message, metadata, visualOverrides);
|
||||
|
||||
/// <summary>
|
||||
/// Add a new warning case to the run results.
|
||||
/// </summary>
|
||||
/// <inheritdoc cref="AttachErrorToObjects"/>
|
||||
public void AttachWarningToObjects(
|
||||
string category,
|
||||
IReadOnlyCollection<Base> affectedObjects,
|
||||
string? message = null,
|
||||
Dictionary<string, object>? metadata = null,
|
||||
Dictionary<string, object>? visualOverrides = null
|
||||
) => AttachResultToObjects(ObjectResultLevel.Warning, category, affectedObjects, message, metadata, visualOverrides);
|
||||
|
||||
/// <summary>
|
||||
/// Add a new info case to the run results.
|
||||
/// </summary>
|
||||
/// <inheritdoc cref="AttachErrorToObjects"/>
|
||||
public void AttachInfoToObjects(
|
||||
string category,
|
||||
IReadOnlyCollection<Base> affectedObjects,
|
||||
string? message = null,
|
||||
Dictionary<string, object>? metadata = null,
|
||||
Dictionary<string, object>? visualOverrides = null
|
||||
) => AttachResultToObjects(ObjectResultLevel.Info, category, affectedObjects, message, metadata, visualOverrides);
|
||||
|
||||
/// <summary>
|
||||
/// Add a new success case to the run results.
|
||||
/// </summary>
|
||||
/// <inheritdoc cref="AttachErrorToObjects"/>
|
||||
public void AttachSuccessToObjects(
|
||||
string category,
|
||||
IReadOnlyCollection<Base> affectedObjects,
|
||||
string? message = null,
|
||||
Dictionary<string, object>? metadata = null,
|
||||
Dictionary<string, object>? visualOverrides = null
|
||||
) => AttachResultToObjects(ObjectResultLevel.Success, category, affectedObjects, message, metadata, visualOverrides);
|
||||
|
||||
/// <summary>
|
||||
/// Add a new case to the run results.
|
||||
/// </summary>
|
||||
/// <param name="level">The level assigned to this result.</param>
|
||||
/// <inheritdoc cref="AttachErrorToObjects"/>
|
||||
public void AttachResultToObjects(
|
||||
ObjectResultLevel level,
|
||||
string category,
|
||||
IReadOnlyCollection<Base> affectedObjects,
|
||||
string? message = null,
|
||||
Dictionary<string, object>? metadata = null,
|
||||
Dictionary<string, object>? visualOverrides = null
|
||||
)
|
||||
{
|
||||
if (affectedObjects.Count == 0)
|
||||
{
|
||||
throw new ArgumentException($"Need at least one affected object to report a(n) {level}");
|
||||
}
|
||||
|
||||
string levelString = ObjectResultLevelMapping.Get(level);
|
||||
Dictionary<string, string?> ids = affectedObjects.ToDictionary(
|
||||
x => x.id.NotNull($"You can only attach {level} results to objects with an id"),
|
||||
x => x.applicationId
|
||||
);
|
||||
|
||||
Console.WriteLine($"Created new {levelString.ToUpper()} category: {category} caused by: {message}");
|
||||
|
||||
ResultCase resultCase = new()
|
||||
{
|
||||
Category = category,
|
||||
Level = levelString,
|
||||
ObjectAppIds = ids,
|
||||
Message = message,
|
||||
Metadata = metadata,
|
||||
VisualOverrides = visualOverrides,
|
||||
};
|
||||
|
||||
AutomationResult.ObjectResults.Add(resultCase);
|
||||
}
|
||||
}
|
||||
|
||||
public partial interface IAutomationContext
|
||||
{
|
||||
[MemberNotNull(nameof(ContextView))]
|
||||
public void SetContextView(IReadOnlyCollection<string>? resourceIds = null, bool includeSourceModelVersion = true);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Speckle.Automate.Sdk.DataAnnotations;
|
||||
|
||||
/// <summary>
|
||||
/// If specified, the given function input will be redacted in all contexts.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.All)]
|
||||
public sealed class SecretAttribute : Attribute { }
|
||||
@@ -0,0 +1,195 @@
|
||||
using System.CommandLine;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Schema;
|
||||
using Newtonsoft.Json.Schema.Generation;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using Speckle.Automate.Sdk.DataAnnotations;
|
||||
using Speckle.Automate.Sdk.Schema;
|
||||
using Speckle.InterfaceGenerator;
|
||||
using Speckle.Sdk;
|
||||
|
||||
namespace Speckle.Automate.Sdk;
|
||||
|
||||
/// <summary>
|
||||
/// Provides mechanisms to execute any function that conforms to the AutomateFunction "interface"
|
||||
/// </summary>
|
||||
[GenerateAutoInterface(VisibilityModifier = "public")]
|
||||
internal class AutomationRunner(IAutomationContextFactory contextFactory) : IAutomationRunner
|
||||
{
|
||||
[SuppressMessage("Design", "CA1031:Do not catch general exception types")]
|
||||
public async Task<IAutomationContext> RunFunction<TInput>(
|
||||
Func<IAutomationContext, TInput, Task> automateFunction,
|
||||
AutomationRunData automationRunData,
|
||||
string speckleToken,
|
||||
TInput inputs
|
||||
)
|
||||
where TInput : struct
|
||||
{
|
||||
var automationContext = await contextFactory.Initialize(automationRunData, speckleToken).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
await automateFunction.Invoke(automationContext, inputs).ConfigureAwait(false);
|
||||
if (automationContext.RunStatus is not ("FAILED" or "SUCCEEDED"))
|
||||
{
|
||||
automationContext.MarkRunSuccess(
|
||||
"WARNING: Automate assumed a success status, but it was not marked as so by the function."
|
||||
);
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (!ex.IsFatal())
|
||||
{
|
||||
Console.WriteLine(ex.ToString());
|
||||
automationContext.MarkRunException("Function error. Check the automation run logs for details.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (automationContext.ContextView is null)
|
||||
{
|
||||
automationContext.SetContextView();
|
||||
}
|
||||
|
||||
await automationContext.ReportRunStatus().ConfigureAwait(false);
|
||||
}
|
||||
return automationContext;
|
||||
}
|
||||
|
||||
public async Task<IAutomationContext> RunFunction(
|
||||
Func<IAutomationContext, Task> automateFunction,
|
||||
AutomationRunData automationRunData,
|
||||
string speckleToken
|
||||
) =>
|
||||
await RunFunction(
|
||||
async (context, _) => await automateFunction(context).ConfigureAwait(false),
|
||||
automationRunData,
|
||||
speckleToken,
|
||||
new Fake()
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
private struct Fake { }
|
||||
|
||||
/// <summary>
|
||||
/// Main entrypoint to execute an Automate function with no input data
|
||||
/// </summary>
|
||||
/// <param name="args">The command line arguments passed into the function by automate</param>
|
||||
/// <param name="automateFunction">The automate function to execute</param>
|
||||
/// <remarks>This should always be called in your own functions, as it contains the logic to trigger the function automatically.</remarks>
|
||||
public async Task<int> Main(string[] args, Func<IAutomationContext, Task> automateFunction)
|
||||
{
|
||||
return await Main(
|
||||
args,
|
||||
async (IAutomationContext context, Fake _) => await automateFunction(context).ConfigureAwait(false)
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Main entrypoint to execute an Automate function with input data of type <typeparamref name="TInput"/>.
|
||||
/// </summary>
|
||||
/// <param name="args">The command line arguments passed into the function by automate</param>
|
||||
/// <param name="automateFunction">The automate function to execute</param>
|
||||
/// <typeparam name="TInput">The provided input data</typeparam>
|
||||
/// <remarks>This should always be called in your own functions, as it contains the logic to trigger the function automatically.</remarks>
|
||||
public async Task<int> Main<TInput>(string[] args, Func<IAutomationContext, TInput, Task> automateFunction)
|
||||
where TInput : struct
|
||||
{
|
||||
Argument<string> pathArg = new(name: "Input Path", description: "A file path to retrieve function inputs");
|
||||
RootCommand rootCommand = new();
|
||||
|
||||
// a stupid hack to be able to exit with a specific integer exit code
|
||||
// read more at https://github.com/dotnet/command-line-api/issues/1570
|
||||
var exitCode = 0;
|
||||
|
||||
rootCommand.AddArgument(pathArg);
|
||||
rootCommand.SetHandler(
|
||||
async inputPath =>
|
||||
{
|
||||
try
|
||||
{
|
||||
FunctionRunData<TInput> data = FunctionRunDataParser.FromPath<TInput>(inputPath);
|
||||
|
||||
var context = await RunFunction(
|
||||
automateFunction,
|
||||
data.AutomationRunData,
|
||||
data.SpeckleToken,
|
||||
data.FunctionInputs
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (context.RunStatus is "EXCEPTION")
|
||||
{
|
||||
exitCode = 1;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
exitCode = 1;
|
||||
throw;
|
||||
}
|
||||
},
|
||||
pathArg
|
||||
);
|
||||
|
||||
Argument<string> schemaFilePathArg = new(
|
||||
name: "Function inputs file path",
|
||||
description: "A token to talk to the Speckle server with"
|
||||
);
|
||||
|
||||
Command generateSchemaCommand = new("generate-schema", "Generate JSON schema for the function inputs");
|
||||
generateSchemaCommand.AddArgument(schemaFilePathArg);
|
||||
generateSchemaCommand.SetHandler(
|
||||
schemaFilePath =>
|
||||
{
|
||||
try
|
||||
{
|
||||
JSchemaGenerator generator = new() { ContractResolver = new CamelCasePropertyNamesContractResolver() };
|
||||
generator.GenerationProviders.Add(new SpeckleSecretProvider());
|
||||
JSchema schema = generator.Generate(typeof(TInput));
|
||||
schema.ToString(SchemaVersion.Draft2019_09);
|
||||
File.WriteAllText(schemaFilePath, schema.ToString());
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
exitCode = 1;
|
||||
throw;
|
||||
}
|
||||
},
|
||||
schemaFilePathArg
|
||||
);
|
||||
rootCommand.Add(generateSchemaCommand);
|
||||
|
||||
await rootCommand.InvokeAsync(args).ConfigureAwait(false);
|
||||
|
||||
// if we've gotten this far, the execution should technically be completed as expected
|
||||
// thus exiting with 0 is the semantically correct thing to do
|
||||
return exitCode;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class SpeckleSecretProvider : JSchemaGenerationProvider
|
||||
{
|
||||
public override JSchema? GetSchema(JSchemaTypeGenerationContext context)
|
||||
{
|
||||
var attributes = context.MemberProperty?.AttributeProvider?.GetAttributes(false) ?? new List<Attribute>();
|
||||
var isSecretString = attributes.Any(att => att is SecretAttribute);
|
||||
|
||||
if (isSecretString)
|
||||
{
|
||||
return CreateSchemaWithWriteOnly(context.ObjectType, context.Required);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static JSchema CreateSchemaWithWriteOnly(Type type, Required required)
|
||||
{
|
||||
JSchemaGenerator generator = new();
|
||||
JSchema schema = generator.Generate(type, required != Required.Always);
|
||||
|
||||
schema.WriteOnly = true;
|
||||
|
||||
return schema;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Speckle.Automate.Sdk.Schema;
|
||||
|
||||
public class AutomationResult
|
||||
{
|
||||
public double Elapsed { get; set; }
|
||||
public string? ResultView { get; set; }
|
||||
public List<string> ResultVersions { get; set; } = new();
|
||||
public List<string> Blobs { get; set; } = new();
|
||||
public string RunStatus { get; set; } = AutomationStatusMapping.Get(AutomationStatus.Running);
|
||||
public string? StatusMessage { get; set; }
|
||||
public List<ResultCase> ObjectResults { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using Speckle.Automate.Sdk.Schema.Triggers;
|
||||
|
||||
namespace Speckle.Automate.Sdk.Schema;
|
||||
|
||||
///<summary>
|
||||
/// Values of the project, model and automation that triggered this function run.
|
||||
///</summary>
|
||||
public readonly struct AutomationRunData
|
||||
{
|
||||
[JsonRequired]
|
||||
public required string ProjectId { get; init; }
|
||||
|
||||
[JsonRequired]
|
||||
public required Uri SpeckleServerUrl { get; init; }
|
||||
|
||||
[JsonRequired]
|
||||
public required string AutomationId { get; init; }
|
||||
|
||||
[JsonRequired]
|
||||
public required string AutomationRunId { get; init; }
|
||||
|
||||
[JsonRequired]
|
||||
public required string FunctionRunId { get; init; }
|
||||
|
||||
[JsonRequired]
|
||||
public required List<VersionCreationTrigger> Triggers { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Speckle.Automate.Sdk.Schema;
|
||||
|
||||
///<summary>
|
||||
/// Set the status of the automation.
|
||||
///</summary>
|
||||
public enum AutomationStatus
|
||||
{
|
||||
Initializing,
|
||||
Running,
|
||||
Failed,
|
||||
Succeeded,
|
||||
Exception,
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace Speckle.Automate.Sdk.Schema;
|
||||
|
||||
public abstract class AutomationStatusMapping
|
||||
{
|
||||
private const string INITIALIZING = "INITIALIZING";
|
||||
private const string RUNNING = "RUNNING";
|
||||
private const string FAILED = "FAILED";
|
||||
private const string SUCCEEDED = "SUCCEEDED";
|
||||
private const string EXCEPTION = "EXCEPTION";
|
||||
|
||||
public static string Get(AutomationStatus status) =>
|
||||
status switch
|
||||
{
|
||||
AutomationStatus.Running => RUNNING,
|
||||
AutomationStatus.Failed => FAILED,
|
||||
AutomationStatus.Succeeded => SUCCEEDED,
|
||||
AutomationStatus.Initializing => INITIALIZING,
|
||||
AutomationStatus.Exception => EXCEPTION,
|
||||
_ => throw new ArgumentOutOfRangeException($"Not valid value for enum {status}"),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Speckle.Automate.Sdk.Schema;
|
||||
|
||||
public readonly struct BlobUploadResponse
|
||||
{
|
||||
public required List<UploadResult> UploadResults { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Speckle.Automate.Sdk.Schema;
|
||||
|
||||
/// <summary>
|
||||
/// Required data to run a function.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"> Type for <see cref="FunctionInputs"/>.</typeparam>
|
||||
public sealed class FunctionRunData<T>
|
||||
{
|
||||
[JsonRequired]
|
||||
public required string SpeckleToken { get; init; }
|
||||
|
||||
[JsonRequired]
|
||||
public required AutomationRunData AutomationRunData { get; init; }
|
||||
|
||||
public required T? FunctionInputs { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Speckle.Automate.Sdk.Schema;
|
||||
|
||||
public static class FunctionRunDataParser
|
||||
{
|
||||
private static readonly JsonSerializerOptions s_jsonSerializerSettings = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Function run data parser from json file path./>
|
||||
/// </summary>
|
||||
/// <param name="inputLocation"> Path to retrieve function run data.</param>
|
||||
/// <typeparam name="T"> Type for function inputs.</typeparam>
|
||||
/// <returns>The data to be able to run function.</returns>
|
||||
/// <exception cref="JsonException">Json was not valid</exception>
|
||||
/// <exception cref="FileNotFoundException"> Throws unless file exists.</exception>
|
||||
public static FunctionRunData<T> FromPath<T>(string inputLocation)
|
||||
{
|
||||
string inputJsonString = ReadInputData(inputLocation);
|
||||
//It's important to use System.Text.Json here. The template FunctionInputs are decorated with STJ attributes
|
||||
FunctionRunData<T>? functionRunData = JsonSerializer.Deserialize<FunctionRunData<T>>(
|
||||
inputJsonString,
|
||||
s_jsonSerializerSettings
|
||||
);
|
||||
|
||||
if (functionRunData is null)
|
||||
{
|
||||
throw new JsonException($"Function run data couldn't deserialized at {inputLocation}");
|
||||
}
|
||||
|
||||
return functionRunData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read text from file.
|
||||
/// </summary>
|
||||
/// <param name="inputLocation"> Path to check file is exist.</param>
|
||||
/// <returns>Text in file.</returns>
|
||||
/// <exception cref="FileNotFoundException"> Throws unless file exists.</exception>
|
||||
private static string ReadInputData(string inputLocation)
|
||||
{
|
||||
if (!File.Exists(inputLocation))
|
||||
{
|
||||
throw new FileNotFoundException($"Cannot find the function inputs file at {inputLocation}");
|
||||
}
|
||||
|
||||
return File.ReadAllText(inputLocation);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Speckle.Automate.Sdk.Schema;
|
||||
|
||||
public enum ObjectResultLevel
|
||||
{
|
||||
Success,
|
||||
Info,
|
||||
Warning,
|
||||
Error,
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace Speckle.Automate.Sdk.Schema;
|
||||
|
||||
public abstract class ObjectResultLevelMapping
|
||||
{
|
||||
private const string SUCCESS = "SUCCESS";
|
||||
private const string INFO = "INFO";
|
||||
private const string WARNING = "WARNING";
|
||||
private const string ERROR = "ERROR";
|
||||
|
||||
public static string Get(ObjectResultLevel level) =>
|
||||
level switch
|
||||
{
|
||||
ObjectResultLevel.Error => ERROR,
|
||||
ObjectResultLevel.Warning => WARNING,
|
||||
ObjectResultLevel.Info => INFO,
|
||||
ObjectResultLevel.Success => SUCCESS,
|
||||
_ => throw new ArgumentOutOfRangeException($"Not valid value for enum {level}"),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Speckle.Automate.Sdk.Schema;
|
||||
|
||||
public readonly struct ObjectResultValues
|
||||
{
|
||||
public required List<ResultCase> ObjectResults { get; init; }
|
||||
public required List<string> BlobIds { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Speckle.Automate.Sdk.Schema;
|
||||
|
||||
public readonly struct ObjectResults
|
||||
{
|
||||
public int Version { get; init; }
|
||||
public ObjectResultValues Values { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Speckle.Automate.Sdk.Schema;
|
||||
|
||||
public readonly struct ResultCase
|
||||
{
|
||||
public required string Category { get; init; }
|
||||
public required string Level { get; init; }
|
||||
public required Dictionary<string, string?> ObjectAppIds { get; init; }
|
||||
public required string? Message { get; init; }
|
||||
public required Dictionary<string, object>? Metadata { get; init; }
|
||||
public required Dictionary<string, object>? VisualOverrides { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Speckle.Automate.Sdk.Schema.Triggers;
|
||||
|
||||
public abstract class AutomationRunTriggerBase
|
||||
{
|
||||
public required string TriggerType { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Speckle.Automate.Sdk.Schema.Triggers;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a single version creation trigger for the automation run.
|
||||
/// </summary>
|
||||
public sealed class VersionCreationTrigger : AutomationRunTriggerBase
|
||||
{
|
||||
public const string VERSION_CREATION_TRIGGER_TYPE = "versionCreation";
|
||||
|
||||
[JsonRequired]
|
||||
public required VersionCreationTriggerPayload Payload { get; init; }
|
||||
|
||||
public VersionCreationTrigger() { }
|
||||
|
||||
[SetsRequiredMembers]
|
||||
public VersionCreationTrigger(string modelId, string versionId)
|
||||
{
|
||||
Payload = new() { ModelId = modelId, VersionId = versionId };
|
||||
TriggerType = VERSION_CREATION_TRIGGER_TYPE;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the version creation trigger payload.
|
||||
/// </summary>
|
||||
public sealed record VersionCreationTriggerPayload
|
||||
{
|
||||
[JsonRequired]
|
||||
public required string ModelId { get; init; }
|
||||
|
||||
[JsonRequired]
|
||||
public required string VersionId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Speckle.Automate.Sdk.Schema;
|
||||
|
||||
public readonly struct UploadResult
|
||||
{
|
||||
public required string BlobId { get; init; }
|
||||
public required string FileName { get; init; }
|
||||
public required int UploadStatus { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using System.Reflection;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Speckle.Objects.Geometry;
|
||||
using Speckle.Sdk;
|
||||
using Speckle.Sdk.Models;
|
||||
using Speckle.Sdk.Serialisation.V2;
|
||||
|
||||
namespace Speckle.Automate.Sdk;
|
||||
|
||||
public static class ServiceRegistration
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets-up the serviceCollection with all the services in Speckle.Automate.Sdk and Speckle.Sdk
|
||||
/// </summary>
|
||||
/// <param name="serviceCollection"></param>
|
||||
/// <returns></returns>
|
||||
public static IServiceCollection AddAutomateSdk(this IServiceCollection serviceCollection)
|
||||
{
|
||||
var executingAssembly = Assembly.GetExecutingAssembly().GetName();
|
||||
var speckleAssembly = typeof(Base).Assembly.GetName();
|
||||
AddAutomateSdk(
|
||||
serviceCollection,
|
||||
new SpeckleSdkOptions(
|
||||
new(executingAssembly.FullName, "automatefunction"),
|
||||
executingAssembly.Version?.ToString() ?? "Unknown",
|
||||
speckleAssembly.Version?.ToString(),
|
||||
[typeof(Base).Assembly, typeof(Point).Assembly]
|
||||
)
|
||||
);
|
||||
|
||||
return serviceCollection;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddAutomateSdk(
|
||||
this IServiceCollection serviceCollection,
|
||||
SpeckleSdkOptions speckleSdkOptions
|
||||
)
|
||||
{
|
||||
serviceCollection.AddSpeckleSdk(speckleSdkOptions);
|
||||
|
||||
//Overwrite the SDK's default IDeserializeProcessFactory to ensure SQLite is not used to cache objects
|
||||
serviceCollection.AddTransient<IDeserializeProcessFactory, DeserializeProcessFactoryNoCache>();
|
||||
|
||||
//Add automate assembly services
|
||||
serviceCollection.AddTransient<IAutomationContextFactory, AutomationContextFactory>();
|
||||
serviceCollection.AddTransient<IAutomationRunner, AutomationRunner>();
|
||||
|
||||
return serviceCollection;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup Label="Compiler Properties">
|
||||
<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Label="Nugetspec Package Properties">
|
||||
<PackageId>Speckle.Automate.Sdk</PackageId>
|
||||
<Description>Speckle Automate SDK</Description>
|
||||
<PackageTags>$(PackageTags) speckle automation</PackageTags>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Label="Nuget Package Properties">
|
||||
<IsPackable>true</IsPackable>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Expose internals to test projects">
|
||||
<InternalsVisibleTo Include="Speckle.Automate.Sdk.Tests.Integration" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Newtonsoft.Json.Schema" />
|
||||
<PackageReference Include="System.CommandLine" NoWarn="NU5104" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0'">
|
||||
<PackageReference Include="System.Text.Json" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Project References">
|
||||
<ProjectReference Include="..\Speckle.Objects\Speckle.Objects.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,79 @@
|
||||
using Speckle.Newtonsoft.Json;
|
||||
using Speckle.Sdk;
|
||||
|
||||
namespace Speckle.Automate.Sdk.Test;
|
||||
|
||||
public class TestAppSettings
|
||||
{
|
||||
public string? SpeckleToken { get; set; }
|
||||
public string? SpeckleServerUrl { get; set; }
|
||||
public string? SpeckleProjectId { get; set; }
|
||||
public string? SpeckleAutomationId { get; set; }
|
||||
}
|
||||
|
||||
public static class TestAutomateEnvironment
|
||||
{
|
||||
public static TestAppSettings? AppSettings { get; private set; }
|
||||
|
||||
private static string GetEnvironmentVariable(string environmentVariableName)
|
||||
{
|
||||
var value = TryGetEnvironmentVariable(environmentVariableName);
|
||||
|
||||
if (value is null)
|
||||
{
|
||||
throw new SpeckleException($"Cannot run tests without a {environmentVariableName} environment variable");
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private static string? TryGetEnvironmentVariable(string environmentVariableName)
|
||||
{
|
||||
return Environment.GetEnvironmentVariable(environmentVariableName);
|
||||
}
|
||||
|
||||
private static TestAppSettings? GetAppSettings()
|
||||
{
|
||||
if (AppSettings != null)
|
||||
{
|
||||
return AppSettings;
|
||||
}
|
||||
|
||||
var path = "./appsettings.json";
|
||||
var json = File.ReadAllText(path);
|
||||
|
||||
var appSettings = JsonConvert.DeserializeObject<TestAppSettings>(json);
|
||||
|
||||
AppSettings = appSettings;
|
||||
|
||||
return AppSettings;
|
||||
}
|
||||
|
||||
public static string GetSpeckleToken()
|
||||
{
|
||||
return GetAppSettings()?.SpeckleToken ?? GetEnvironmentVariable("SPECKLE_TOKEN");
|
||||
}
|
||||
|
||||
public static Uri GetSpeckleServerUrl()
|
||||
{
|
||||
var urlString =
|
||||
GetAppSettings()?.SpeckleServerUrl ?? TryGetEnvironmentVariable("SPECKLE_SERVER_URL") ?? "http://127.0.0.1:3000";
|
||||
|
||||
return new Uri(urlString);
|
||||
}
|
||||
|
||||
public static string GetSpeckleProjectId()
|
||||
{
|
||||
return GetAppSettings()?.SpeckleProjectId ?? GetEnvironmentVariable("SPECKLE_PROJECT_ID");
|
||||
}
|
||||
|
||||
public static string GetSpeckleAutomationId()
|
||||
{
|
||||
return GetAppSettings()?.SpeckleAutomationId ?? GetEnvironmentVariable("SPECKLE_AUTOMATION_ID");
|
||||
}
|
||||
|
||||
public static void Clear()
|
||||
{
|
||||
AppSettings = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
using GraphQL;
|
||||
using Speckle.Automate.Sdk.Schema;
|
||||
using Speckle.Automate.Sdk.Schema.Triggers;
|
||||
using Speckle.Newtonsoft.Json;
|
||||
using Speckle.Sdk.Api;
|
||||
using Speckle.Sdk.Api.GraphQL.Models.Responses;
|
||||
|
||||
namespace Speckle.Automate.Sdk.Test;
|
||||
|
||||
internal class TestAutomationRun
|
||||
{
|
||||
[JsonRequired]
|
||||
public required string AutomationRunId { get; init; }
|
||||
|
||||
[JsonRequired]
|
||||
public required string FunctionRunId { get; init; }
|
||||
|
||||
[JsonRequired]
|
||||
public required IReadOnlyList<TestAutomationRunTrigger> Triggers { get; init; }
|
||||
}
|
||||
|
||||
internal class TestAutomationRunTrigger : AutomationRunTriggerBase
|
||||
{
|
||||
/// <remarks>This should really be a TestAutomationRunTriggerPayload, but right now, they look the samee</remarks>
|
||||
public required VersionCreationTriggerPayload Payload { get; init; }
|
||||
}
|
||||
|
||||
public static class TestAutomateUtils
|
||||
{
|
||||
public static async Task<AutomationRunData> CreateTestRun(
|
||||
IClient speckleClient,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
mutation Mutation($projectId: ID!, $automationId: ID!) {
|
||||
data:projectMutations {
|
||||
data:automationMutations(projectId: $projectId) {
|
||||
data:createTestAutomationRun(automationId: $automationId) {
|
||||
automationRunId
|
||||
functionRunId
|
||||
triggers {
|
||||
payload {
|
||||
modelId
|
||||
versionId
|
||||
}
|
||||
triggerType
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
GraphQLRequest request = new(
|
||||
query: QUERY,
|
||||
variables: new
|
||||
{
|
||||
automationId = TestAutomateEnvironment.GetSpeckleAutomationId(),
|
||||
projectId = TestAutomateEnvironment.GetSpeckleProjectId(),
|
||||
}
|
||||
);
|
||||
|
||||
var res = await speckleClient
|
||||
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<RequiredResponse<TestAutomationRun>>>>(
|
||||
request,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var runData = res.data.data.data;
|
||||
var triggerData = runData.Triggers[0].Payload;
|
||||
|
||||
string modelId = triggerData.ModelId;
|
||||
string versionId = triggerData.VersionId;
|
||||
|
||||
var data = new AutomationRunData()
|
||||
{
|
||||
ProjectId = TestAutomateEnvironment.GetSpeckleProjectId(),
|
||||
SpeckleServerUrl = TestAutomateEnvironment.GetSpeckleServerUrl(),
|
||||
AutomationId = TestAutomateEnvironment.GetSpeckleAutomationId(),
|
||||
AutomationRunId = runData.AutomationRunId,
|
||||
FunctionRunId = runData.FunctionRunId,
|
||||
Triggers = [new(modelId: modelId, versionId: versionId)],
|
||||
};
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,618 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
".NETStandard,Version=v2.0": {
|
||||
"Microsoft.SourceLink.GitHub": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.0.0, )",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Build.Tasks.Git": "8.0.0",
|
||||
"Microsoft.SourceLink.Common": "8.0.0"
|
||||
}
|
||||
},
|
||||
"NETStandard.Library": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.0.3, )",
|
||||
"resolved": "2.0.3",
|
||||
"contentHash": "st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0"
|
||||
}
|
||||
},
|
||||
"Newtonsoft.Json.Schema": {
|
||||
"type": "Direct",
|
||||
"requested": "[4.0.1, )",
|
||||
"resolved": "4.0.1",
|
||||
"contentHash": "rbHUKp5WTIbqmLEeJ21nTTDGcfR0LA7bVMzm0bYc3yx6NFKiCIHzzvYbwA4Sqgs7+wNldc5nBlkbithWj8IZig==",
|
||||
"dependencies": {
|
||||
"Newtonsoft.Json": "13.0.3"
|
||||
}
|
||||
},
|
||||
"PolySharp": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.15.0, )",
|
||||
"resolved": "1.15.0",
|
||||
"contentHash": "FbU0El+EEjdpuIX4iDbeS7ki1uzpJPx8vbqOzEtqnl1GZeAGJfq+jCbxeJL2y0EPnUNk8dRnnqR2xnYXg9Tf+g=="
|
||||
},
|
||||
"Speckle.InterfaceGenerator": {
|
||||
"type": "Direct",
|
||||
"requested": "[0.9.6, )",
|
||||
"resolved": "0.9.6",
|
||||
"contentHash": "HKH7tYrYYlCK1ct483hgxERAdVdMtl7gUKW9ijWXxA1UsYR4Z+TrRHYmzZ9qmpu1NnTycSrp005NYM78GDKV1w=="
|
||||
},
|
||||
"System.CommandLine": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.0.0-beta4.22272.1, )",
|
||||
"resolved": "2.0.0-beta4.22272.1",
|
||||
"contentHash": "1uqED/q2H0kKoLJ4+hI2iPSBSEdTuhfCYADeJrAqERmiGQ2NNacYKRNEQ+gFbU4glgVyK8rxI+ZOe1onEtr/Pg==",
|
||||
"dependencies": {
|
||||
"System.Memory": "4.5.4"
|
||||
}
|
||||
},
|
||||
"System.Text.Json": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.0.5, )",
|
||||
"resolved": "8.0.5",
|
||||
"contentHash": "0f1B50Ss7rqxXiaBJyzUu9bWFOO2/zSlifZ/UNMdiIpDYe4cY4LQQicP4nirK1OS31I43rn062UIJ1Q9bpmHpg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Bcl.AsyncInterfaces": "8.0.0",
|
||||
"System.Buffers": "4.5.1",
|
||||
"System.Memory": "4.5.5",
|
||||
"System.Runtime.CompilerServices.Unsafe": "6.0.0",
|
||||
"System.Text.Encodings.Web": "8.0.0",
|
||||
"System.Threading.Tasks.Extensions": "4.5.4"
|
||||
}
|
||||
},
|
||||
"GraphQL.Client.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "h7uzWFORHZ+CCjwr/ThAyXMr0DPpzEANDa4Uo54wqCQ+j7qUKwqYTgOrb1W40sqbvNaZm9v/X7It31SUw0maHA==",
|
||||
"dependencies": {
|
||||
"GraphQL.Primitives": "6.0.0"
|
||||
}
|
||||
},
|
||||
"GraphQL.Client.Abstractions.Websocket": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "Nr9bPf8gIOvLuXpqEpqr9z9jslYFJOvd0feHth3/kPqeR3uMbjF5pjiwh4jxyMcxHdr8Pb6QiXkV3hsSyt0v7A==",
|
||||
"dependencies": {
|
||||
"GraphQL.Client.Abstractions": "6.0.0"
|
||||
}
|
||||
},
|
||||
"GraphQL.Primitives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "yg72rrYDapfsIUrul7aF6wwNnTJBOFvuA9VdDTQpPa8AlAriHbufeXYLBcodKjfUdkCnaiggX1U/nEP08Zb5GA=="
|
||||
},
|
||||
"Microsoft.Build.Tasks.Git": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
|
||||
},
|
||||
"Microsoft.Data.Sqlite.Core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "7.0.5",
|
||||
"contentHash": "FTerRmQPqHrCrnoUzhBu+E+1DNGwyrAMLqHkAqOOOu5pGfyMOj8qQUBxI/gDtWtG11p49UxSfWmBzRNlwZqfUg==",
|
||||
"dependencies": {
|
||||
"SQLitePCLRaw.core": "2.1.4"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "nOP8R1mVb/6mZtm2qgAJXn/LFm/2kMjHDAg/QJLFG6CuWYJtaD3p1BwQhufBVvRzL9ceJ/xF0SQ0qsI2GkDQAA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "2.2.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "65MrmXCziWaQFrI0UHkQbesrX5wTwf9XPjY5yFm/VkgJKFJ5gqvXRoXjIZcf2wLi5ZlwGz/oMYfyURVCWbM5iw==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Primitives": "2.2.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration.Binder": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "vJ9xvOZCnUAIHcGC3SU35r3HKmHTVIeHzo6u/qzlHAqD8m6xv92MLin4oJntTvkpKxVX3vI1GFFkIQtU3AdlsQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration": "2.2.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Logging.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "B2WqEox8o+4KUOpL7rZPyh6qYjik8tHi2tN8Z9jZkHzED8ElYgZa/h6K+xliB435SqUcWT290Fr2aa8BtZjn8A=="
|
||||
},
|
||||
"Microsoft.Extensions.Options": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "UpZLNLBpIZ0GTebShui7xXYh6DmBHjWM8NxGxZbdQh/bPZ5e6YswqI+bru6BnEL5eWiOdodsXtEz3FROcgi/qg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "2.2.0",
|
||||
"Microsoft.Extensions.Primitives": "2.2.0",
|
||||
"System.ComponentModel.Annotations": "4.5.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Primitives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "azyQtqbm4fSaDzZHD/J+V6oWMFaf2tWP4WEGIYePLCMw3+b2RQdj9ybgbQyjCshcitQKQ4lEDOZjmSlTTrHxUg==",
|
||||
"dependencies": {
|
||||
"System.Memory": "4.5.1",
|
||||
"System.Runtime.CompilerServices.Unsafe": "4.5.1"
|
||||
}
|
||||
},
|
||||
"Microsoft.NETCore.Platforms": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.1.0",
|
||||
"contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A=="
|
||||
},
|
||||
"Microsoft.NETCore.Targets": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.1.0",
|
||||
"contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg=="
|
||||
},
|
||||
"Microsoft.SourceLink.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
|
||||
},
|
||||
"Newtonsoft.Json": {
|
||||
"type": "Transitive",
|
||||
"resolved": "13.0.3",
|
||||
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
|
||||
},
|
||||
"SQLitePCLRaw.bundle_e_sqlite3": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.4",
|
||||
"contentHash": "EWI1olKDjFEBMJu0+3wuxwziIAdWDVMYLhuZ3Qs84rrz+DHwD00RzWPZCa+bLnHCf3oJwuFZIRsHT5p236QXww==",
|
||||
"dependencies": {
|
||||
"SQLitePCLRaw.lib.e_sqlite3": "2.1.4",
|
||||
"SQLitePCLRaw.provider.e_sqlite3": "2.1.4"
|
||||
}
|
||||
},
|
||||
"SQLitePCLRaw.core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.4",
|
||||
"contentHash": "inBjvSHo9UDKneGNzfUfDjK08JzlcIhn1+SP5Y3m6cgXpCxXKCJDy6Mka7LpgSV+UZmKSnC8rTwB0SQ0xKu5pA==",
|
||||
"dependencies": {
|
||||
"System.Memory": "4.5.3"
|
||||
}
|
||||
},
|
||||
"SQLitePCLRaw.lib.e_sqlite3": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.4",
|
||||
"contentHash": "2C9Q9eX7CPLveJA0rIhf9RXAvu+7nWZu1A2MdG6SD/NOu26TakGgL1nsbc0JAspGijFOo3HoN79xrx8a368fBg=="
|
||||
},
|
||||
"SQLitePCLRaw.provider.e_sqlite3": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.4",
|
||||
"contentHash": "CSlb5dUp1FMIkez9Iv5EXzpeq7rHryVNqwJMWnpq87j9zWZexaEMdisDktMsnnrzKM6ahNrsTkjqNodTBPBxtQ==",
|
||||
"dependencies": {
|
||||
"SQLitePCLRaw.core": "2.1.4"
|
||||
}
|
||||
},
|
||||
"System.Buffers": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.5.1",
|
||||
"contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg=="
|
||||
},
|
||||
"System.ComponentModel.Annotations": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.5.0",
|
||||
"contentHash": "UxYQ3FGUOtzJ7LfSdnYSFd7+oEv6M8NgUatatIN2HxNtDdlcvFAf+VIq4Of9cDMJEJC0aSRv/x898RYhB4Yppg=="
|
||||
},
|
||||
"System.Memory": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.5.5",
|
||||
"contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==",
|
||||
"dependencies": {
|
||||
"System.Buffers": "4.5.1",
|
||||
"System.Numerics.Vectors": "4.4.0",
|
||||
"System.Runtime.CompilerServices.Unsafe": "4.5.3"
|
||||
}
|
||||
},
|
||||
"System.Numerics.Vectors": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.4.0",
|
||||
"contentHash": "UiLzLW+Lw6HLed1Hcg+8jSRttrbuXv7DANVj0DkL9g6EnnzbL75EB7EWsw5uRbhxd/4YdG8li5XizGWepmG3PQ=="
|
||||
},
|
||||
"System.Reactive": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.0.0",
|
||||
"contentHash": "erBZjkQHWL9jpasCE/0qKAryzVBJFxGHVBAvgRN1bzM0q2s1S4oYREEEL0Vb+1kA/6BKb5FjUZMp5VXmy+gzkQ==",
|
||||
"dependencies": {
|
||||
"System.Runtime.InteropServices.WindowsRuntime": "4.3.0",
|
||||
"System.Threading.Tasks.Extensions": "4.5.4"
|
||||
}
|
||||
},
|
||||
"System.Runtime": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0"
|
||||
}
|
||||
},
|
||||
"System.Runtime.CompilerServices.Unsafe": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg=="
|
||||
},
|
||||
"System.Runtime.InteropServices.WindowsRuntime": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "J4GUi3xZQLUBasNwZnjrffN8i5wpHrBtZoLG+OhRyGo/+YunMRWWtwoMDlUAIdmX0uRfpHIBDSV6zyr3yf00TA==",
|
||||
"dependencies": {
|
||||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Text.Encodings.Web": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ==",
|
||||
"dependencies": {
|
||||
"System.Buffers": "4.5.1",
|
||||
"System.Memory": "4.5.5",
|
||||
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
|
||||
}
|
||||
},
|
||||
"System.Threading.Tasks.Extensions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.5.4",
|
||||
"contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==",
|
||||
"dependencies": {
|
||||
"System.Runtime.CompilerServices.Unsafe": "4.5.3"
|
||||
}
|
||||
},
|
||||
"speckle.objects": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"Speckle.Sdk": "[1.0.0, )"
|
||||
}
|
||||
},
|
||||
"speckle.sdk": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"GraphQL.Client": "[6.0.0, )",
|
||||
"Microsoft.Bcl.AsyncInterfaces": "[5.0.0, )",
|
||||
"Microsoft.CSharp": "[4.7.0, )",
|
||||
"Microsoft.Data.Sqlite": "[7.0.5, )",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "[2.2.0, )",
|
||||
"Microsoft.Extensions.Logging": "[2.2.0, )",
|
||||
"Speckle.DoubleNumerics": "[4.1.0, )",
|
||||
"Speckle.Newtonsoft.Json": "[13.0.2, )",
|
||||
"Speckle.Sdk.Dependencies": "[1.0.0, )"
|
||||
}
|
||||
},
|
||||
"speckle.sdk.dependencies": {
|
||||
"type": "Project"
|
||||
},
|
||||
"GraphQL.Client": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[6.0.0, )",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "8yPNBbuVBpTptivyAlak4GZvbwbUcjeQTL4vN1HKHRuOykZ4r7l5fcLS6vpyPyLn0x8FsL31xbOIKyxbmR9rbA==",
|
||||
"dependencies": {
|
||||
"GraphQL.Client.Abstractions": "6.0.0",
|
||||
"GraphQL.Client.Abstractions.Websocket": "6.0.0",
|
||||
"System.Reactive": "5.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Bcl.AsyncInterfaces": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[5.0.0, )",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "3WA9q9yVqJp222P3x1wYIGDAkpjAku0TMUaaQV22g6L67AI0LdOIrVS7Ht2vJfLHGSPVuqN94vIr15qn+HEkHw==",
|
||||
"dependencies": {
|
||||
"System.Threading.Tasks.Extensions": "4.5.4"
|
||||
}
|
||||
},
|
||||
"Microsoft.CSharp": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[4.7.0, )",
|
||||
"resolved": "4.7.0",
|
||||
"contentHash": "pTj+D3uJWyN3My70i2Hqo+OXixq3Os2D1nJ2x92FFo6sk8fYS1m1WLNTs0Dc1uPaViH0YvEEwvzddQ7y4rhXmA=="
|
||||
},
|
||||
"Microsoft.Data.Sqlite": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[7.0.5, )",
|
||||
"resolved": "7.0.5",
|
||||
"contentHash": "KGxbPeWsQMnmQy43DSBxAFtHz3l2JX8EWBSGUCvT3CuZ8KsuzbkqMIJMDOxWtG8eZSoCDI04aiVQjWuuV8HmSw==",
|
||||
"dependencies": {
|
||||
"Microsoft.Data.Sqlite.Core": "7.0.5",
|
||||
"SQLitePCLRaw.bundle_e_sqlite3": "2.1.4"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.2.0, )",
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "f9hstgjVmr6rmrfGSpfsVOl2irKAgr1QjrSi3FgnS7kulxband50f2brRLwySAQTADPZeTdow0mpSMcoAdadCw=="
|
||||
},
|
||||
"Microsoft.Extensions.Logging": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.2.0, )",
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "Nxqhadc9FCmFHzU+fz3oc8sFlE6IadViYg8dfUdGzJZ2JUxnCsRghBhhOWdM4B2zSZqEc+0BjliBh/oNdRZuig==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Binder": "2.2.0",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "2.2.0",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "2.2.0",
|
||||
"Microsoft.Extensions.Options": "2.2.0"
|
||||
}
|
||||
},
|
||||
"Speckle.DoubleNumerics": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[4.1.0, )",
|
||||
"resolved": "4.1.0",
|
||||
"contentHash": "20DtS+FsDRsOD9+AU3TwNFZ0qrKo5f6f7B5ZR9wStsIHHHC9k7DpjbCvuNtmnSjx54MD+TJC7wV2f5iyGVPj1A=="
|
||||
},
|
||||
"Speckle.Newtonsoft.Json": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[13.0.2, )",
|
||||
"resolved": "13.0.2",
|
||||
"contentHash": "g1BejUZwax5PRfL6xHgLEK23sqHWOgOj9hE7RvfRRlN00AGt8GnPYt8HedSK7UB3HiRW8zCA9Pn0iiYxCK24BA=="
|
||||
}
|
||||
},
|
||||
"net8.0": {
|
||||
"Microsoft.SourceLink.GitHub": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.0.0, )",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Build.Tasks.Git": "8.0.0",
|
||||
"Microsoft.SourceLink.Common": "8.0.0"
|
||||
}
|
||||
},
|
||||
"Newtonsoft.Json.Schema": {
|
||||
"type": "Direct",
|
||||
"requested": "[4.0.1, )",
|
||||
"resolved": "4.0.1",
|
||||
"contentHash": "rbHUKp5WTIbqmLEeJ21nTTDGcfR0LA7bVMzm0bYc3yx6NFKiCIHzzvYbwA4Sqgs7+wNldc5nBlkbithWj8IZig==",
|
||||
"dependencies": {
|
||||
"Newtonsoft.Json": "13.0.3"
|
||||
}
|
||||
},
|
||||
"PolySharp": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.15.0, )",
|
||||
"resolved": "1.15.0",
|
||||
"contentHash": "FbU0El+EEjdpuIX4iDbeS7ki1uzpJPx8vbqOzEtqnl1GZeAGJfq+jCbxeJL2y0EPnUNk8dRnnqR2xnYXg9Tf+g=="
|
||||
},
|
||||
"Speckle.InterfaceGenerator": {
|
||||
"type": "Direct",
|
||||
"requested": "[0.9.6, )",
|
||||
"resolved": "0.9.6",
|
||||
"contentHash": "HKH7tYrYYlCK1ct483hgxERAdVdMtl7gUKW9ijWXxA1UsYR4Z+TrRHYmzZ9qmpu1NnTycSrp005NYM78GDKV1w=="
|
||||
},
|
||||
"System.CommandLine": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.0.0-beta4.22272.1, )",
|
||||
"resolved": "2.0.0-beta4.22272.1",
|
||||
"contentHash": "1uqED/q2H0kKoLJ4+hI2iPSBSEdTuhfCYADeJrAqERmiGQ2NNacYKRNEQ+gFbU4glgVyK8rxI+ZOe1onEtr/Pg=="
|
||||
},
|
||||
"GraphQL.Client.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "h7uzWFORHZ+CCjwr/ThAyXMr0DPpzEANDa4Uo54wqCQ+j7qUKwqYTgOrb1W40sqbvNaZm9v/X7It31SUw0maHA==",
|
||||
"dependencies": {
|
||||
"GraphQL.Primitives": "6.0.0"
|
||||
}
|
||||
},
|
||||
"GraphQL.Client.Abstractions.Websocket": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "Nr9bPf8gIOvLuXpqEpqr9z9jslYFJOvd0feHth3/kPqeR3uMbjF5pjiwh4jxyMcxHdr8Pb6QiXkV3hsSyt0v7A==",
|
||||
"dependencies": {
|
||||
"GraphQL.Client.Abstractions": "6.0.0"
|
||||
}
|
||||
},
|
||||
"GraphQL.Primitives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "yg72rrYDapfsIUrul7aF6wwNnTJBOFvuA9VdDTQpPa8AlAriHbufeXYLBcodKjfUdkCnaiggX1U/nEP08Zb5GA=="
|
||||
},
|
||||
"Microsoft.Build.Tasks.Git": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
|
||||
},
|
||||
"Microsoft.Data.Sqlite.Core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "7.0.5",
|
||||
"contentHash": "FTerRmQPqHrCrnoUzhBu+E+1DNGwyrAMLqHkAqOOOu5pGfyMOj8qQUBxI/gDtWtG11p49UxSfWmBzRNlwZqfUg==",
|
||||
"dependencies": {
|
||||
"SQLitePCLRaw.core": "2.1.4"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "nOP8R1mVb/6mZtm2qgAJXn/LFm/2kMjHDAg/QJLFG6CuWYJtaD3p1BwQhufBVvRzL9ceJ/xF0SQ0qsI2GkDQAA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "2.2.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "65MrmXCziWaQFrI0UHkQbesrX5wTwf9XPjY5yFm/VkgJKFJ5gqvXRoXjIZcf2wLi5ZlwGz/oMYfyURVCWbM5iw==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Primitives": "2.2.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration.Binder": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "vJ9xvOZCnUAIHcGC3SU35r3HKmHTVIeHzo6u/qzlHAqD8m6xv92MLin4oJntTvkpKxVX3vI1GFFkIQtU3AdlsQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration": "2.2.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Logging.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "B2WqEox8o+4KUOpL7rZPyh6qYjik8tHi2tN8Z9jZkHzED8ElYgZa/h6K+xliB435SqUcWT290Fr2aa8BtZjn8A=="
|
||||
},
|
||||
"Microsoft.Extensions.Options": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "UpZLNLBpIZ0GTebShui7xXYh6DmBHjWM8NxGxZbdQh/bPZ5e6YswqI+bru6BnEL5eWiOdodsXtEz3FROcgi/qg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "2.2.0",
|
||||
"Microsoft.Extensions.Primitives": "2.2.0",
|
||||
"System.ComponentModel.Annotations": "4.5.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Primitives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "azyQtqbm4fSaDzZHD/J+V6oWMFaf2tWP4WEGIYePLCMw3+b2RQdj9ybgbQyjCshcitQKQ4lEDOZjmSlTTrHxUg==",
|
||||
"dependencies": {
|
||||
"System.Memory": "4.5.1",
|
||||
"System.Runtime.CompilerServices.Unsafe": "4.5.1"
|
||||
}
|
||||
},
|
||||
"Microsoft.SourceLink.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
|
||||
},
|
||||
"Newtonsoft.Json": {
|
||||
"type": "Transitive",
|
||||
"resolved": "13.0.3",
|
||||
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
|
||||
},
|
||||
"SQLitePCLRaw.bundle_e_sqlite3": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.4",
|
||||
"contentHash": "EWI1olKDjFEBMJu0+3wuxwziIAdWDVMYLhuZ3Qs84rrz+DHwD00RzWPZCa+bLnHCf3oJwuFZIRsHT5p236QXww==",
|
||||
"dependencies": {
|
||||
"SQLitePCLRaw.lib.e_sqlite3": "2.1.4",
|
||||
"SQLitePCLRaw.provider.e_sqlite3": "2.1.4"
|
||||
}
|
||||
},
|
||||
"SQLitePCLRaw.core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.4",
|
||||
"contentHash": "inBjvSHo9UDKneGNzfUfDjK08JzlcIhn1+SP5Y3m6cgXpCxXKCJDy6Mka7LpgSV+UZmKSnC8rTwB0SQ0xKu5pA==",
|
||||
"dependencies": {
|
||||
"System.Memory": "4.5.3"
|
||||
}
|
||||
},
|
||||
"SQLitePCLRaw.lib.e_sqlite3": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.4",
|
||||
"contentHash": "2C9Q9eX7CPLveJA0rIhf9RXAvu+7nWZu1A2MdG6SD/NOu26TakGgL1nsbc0JAspGijFOo3HoN79xrx8a368fBg=="
|
||||
},
|
||||
"SQLitePCLRaw.provider.e_sqlite3": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.4",
|
||||
"contentHash": "CSlb5dUp1FMIkez9Iv5EXzpeq7rHryVNqwJMWnpq87j9zWZexaEMdisDktMsnnrzKM6ahNrsTkjqNodTBPBxtQ==",
|
||||
"dependencies": {
|
||||
"SQLitePCLRaw.core": "2.1.4"
|
||||
}
|
||||
},
|
||||
"System.ComponentModel.Annotations": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.5.0",
|
||||
"contentHash": "UxYQ3FGUOtzJ7LfSdnYSFd7+oEv6M8NgUatatIN2HxNtDdlcvFAf+VIq4Of9cDMJEJC0aSRv/x898RYhB4Yppg=="
|
||||
},
|
||||
"System.Memory": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.5.3",
|
||||
"contentHash": "3oDzvc/zzetpTKWMShs1AADwZjQ/36HnsufHRPcOjyRAAMLDlu2iD33MBI2opxnezcVUtXyqDXXjoFMOU9c7SA=="
|
||||
},
|
||||
"System.Reactive": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.0.0",
|
||||
"contentHash": "erBZjkQHWL9jpasCE/0qKAryzVBJFxGHVBAvgRN1bzM0q2s1S4oYREEEL0Vb+1kA/6BKb5FjUZMp5VXmy+gzkQ=="
|
||||
},
|
||||
"System.Runtime.CompilerServices.Unsafe": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.5.1",
|
||||
"contentHash": "Zh8t8oqolRaFa9vmOZfdQm/qKejdqz0J9kr7o2Fu0vPeoH3BL1EOXipKWwkWtLT1JPzjByrF19fGuFlNbmPpiw=="
|
||||
},
|
||||
"speckle.objects": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"Speckle.Sdk": "[1.0.0, )"
|
||||
}
|
||||
},
|
||||
"speckle.sdk": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"GraphQL.Client": "[6.0.0, )",
|
||||
"Microsoft.Data.Sqlite": "[7.0.5, )",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "[2.2.0, )",
|
||||
"Microsoft.Extensions.Logging": "[2.2.0, )",
|
||||
"Speckle.DoubleNumerics": "[4.1.0, )",
|
||||
"Speckle.Newtonsoft.Json": "[13.0.2, )",
|
||||
"Speckle.Sdk.Dependencies": "[1.0.0, )"
|
||||
}
|
||||
},
|
||||
"speckle.sdk.dependencies": {
|
||||
"type": "Project"
|
||||
},
|
||||
"GraphQL.Client": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[6.0.0, )",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "8yPNBbuVBpTptivyAlak4GZvbwbUcjeQTL4vN1HKHRuOykZ4r7l5fcLS6vpyPyLn0x8FsL31xbOIKyxbmR9rbA==",
|
||||
"dependencies": {
|
||||
"GraphQL.Client.Abstractions": "6.0.0",
|
||||
"GraphQL.Client.Abstractions.Websocket": "6.0.0",
|
||||
"System.Reactive": "5.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Data.Sqlite": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[7.0.5, )",
|
||||
"resolved": "7.0.5",
|
||||
"contentHash": "KGxbPeWsQMnmQy43DSBxAFtHz3l2JX8EWBSGUCvT3CuZ8KsuzbkqMIJMDOxWtG8eZSoCDI04aiVQjWuuV8HmSw==",
|
||||
"dependencies": {
|
||||
"Microsoft.Data.Sqlite.Core": "7.0.5",
|
||||
"SQLitePCLRaw.bundle_e_sqlite3": "2.1.4"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.2.0, )",
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "f9hstgjVmr6rmrfGSpfsVOl2irKAgr1QjrSi3FgnS7kulxband50f2brRLwySAQTADPZeTdow0mpSMcoAdadCw=="
|
||||
},
|
||||
"Microsoft.Extensions.Logging": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.2.0, )",
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "Nxqhadc9FCmFHzU+fz3oc8sFlE6IadViYg8dfUdGzJZ2JUxnCsRghBhhOWdM4B2zSZqEc+0BjliBh/oNdRZuig==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Binder": "2.2.0",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "2.2.0",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "2.2.0",
|
||||
"Microsoft.Extensions.Options": "2.2.0"
|
||||
}
|
||||
},
|
||||
"Speckle.DoubleNumerics": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[4.1.0, )",
|
||||
"resolved": "4.1.0",
|
||||
"contentHash": "20DtS+FsDRsOD9+AU3TwNFZ0qrKo5f6f7B5ZR9wStsIHHHC9k7DpjbCvuNtmnSjx54MD+TJC7wV2f5iyGVPj1A=="
|
||||
},
|
||||
"Speckle.Newtonsoft.Json": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[13.0.2, )",
|
||||
"resolved": "13.0.2",
|
||||
"contentHash": "g1BejUZwax5PRfL6xHgLEK23sqHWOgOj9hE7RvfRRlN00AGt8GnPYt8HedSK7UB3HiRW8zCA9Pn0iiYxCK24BA=="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using Speckle.Objects.Geometry;
|
||||
using Speckle.Sdk.Models;
|
||||
|
||||
namespace Speckle.Objects.Annotation;
|
||||
|
||||
/// <summary>
|
||||
/// Text class for representation in the viewer
|
||||
/// </summary>
|
||||
[SpeckleType("Objects.Annotation.Text")]
|
||||
public class Text : Base
|
||||
{
|
||||
/// <summary>
|
||||
/// Plain text, without formatting
|
||||
/// </summary>
|
||||
public required string value { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Height in linear units or pixels (if Units.None)
|
||||
/// </summary>
|
||||
public required double height { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Units will be 'Units.None' if the text size is defined in pixels (stays the same size
|
||||
/// independently of zooming the model). Default height in pixels is 17px (used for Viewer measurements)
|
||||
/// </summary>
|
||||
public required string units { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If true, the text is oriented to face the screen (camera-aligned).
|
||||
/// </summary>
|
||||
public required bool screenOriented { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Horizontal alignment: Left, Center or Right
|
||||
/// </summary>
|
||||
public AlignmentHorizontal alignmentH { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Vertical alignment: Top, Center or Bottom
|
||||
/// </summary>
|
||||
public AlignmentVertical alignmentV { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Plane axis vectors will be ignored if screenOriented is true
|
||||
/// </summary>
|
||||
public required Plane plane { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum width of the text field (in 'units').
|
||||
/// Text will be split into lines (wrapped) to fit into the width.
|
||||
/// null, if text should not be wrapped.
|
||||
/// </summary>
|
||||
public double? maxWidth { get; set; }
|
||||
}
|
||||
|
||||
public enum AlignmentHorizontal
|
||||
{
|
||||
Left,
|
||||
Center,
|
||||
Right,
|
||||
}
|
||||
|
||||
public enum AlignmentVertical
|
||||
{
|
||||
Top,
|
||||
Center,
|
||||
Bottom,
|
||||
}
|
||||
@@ -68,7 +68,6 @@ public class Plane : Base, ITransformable<Plane>
|
||||
/// Returns the values of this <see cref="Plane"/> as a list of numbers
|
||||
/// </summary>
|
||||
/// <returns>A list of values representing the Plane.</returns>
|
||||
|
||||
public List<double> ToList()
|
||||
{
|
||||
var list = new List<double>();
|
||||
|
||||
@@ -147,7 +147,6 @@ public class Surface : Base, IHasBoundingBox, IHasArea, ITransformable<Surface>
|
||||
/// </summary>
|
||||
/// <returns>A 2-dimensional array representing this <see cref="Surface"/>s control points.</returns>
|
||||
/// <remarks>The ControlPoints will be ordered following directions "[u][v]"</remarks>
|
||||
|
||||
public List<List<ControlPoint>> GetControlPoints()
|
||||
{
|
||||
var matrix = new List<List<ControlPoint>>();
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
using Speckle.Objects.Data;
|
||||
using Speckle.Sdk.Models;
|
||||
using Speckle.Sdk.Models.Proxies;
|
||||
|
||||
namespace Speckle.Objects.Other;
|
||||
|
||||
/// <summary>
|
||||
/// Proxy for levels as DataObject value.
|
||||
/// <remarks> These proxy lives in Objects library because it depends on DataObject</remarks>
|
||||
/// </summary>
|
||||
[SpeckleType("Objects.Other.LevelProxy")]
|
||||
public class LevelProxy : Base, IProxyCollection
|
||||
{
|
||||
/// <summary>
|
||||
/// The list of application ids of objects that use this level
|
||||
/// </summary>
|
||||
public required List<string> objects { get; set; }
|
||||
|
||||
public required DataObject value { get; set; }
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
using System.Drawing;
|
||||
using Speckle.Newtonsoft.Json;
|
||||
using Speckle.Sdk.Models;
|
||||
using Speckle.Sdk.Models.Proxies;
|
||||
|
||||
namespace Speckle.Objects.Other;
|
||||
|
||||
@@ -39,20 +38,3 @@ public class RenderMaterial : Base
|
||||
set => diffuse = value.ToArgb();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to store render material to object relationships in root collections
|
||||
/// </summary>
|
||||
[SpeckleType("Objects.Other.RenderMaterialProxy")]
|
||||
public class RenderMaterialProxy : Base, IProxyCollection
|
||||
{
|
||||
/// <summary>
|
||||
/// The list of application ids of objects that use this render material
|
||||
/// </summary>
|
||||
public required List<string> objects { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The render material used by <see cref="objects"/>
|
||||
/// </summary>
|
||||
public required RenderMaterial value { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
using Speckle.Sdk.Models;
|
||||
using Speckle.Sdk.Models.Proxies;
|
||||
|
||||
namespace Speckle.Objects.Other;
|
||||
|
||||
/// <summary>
|
||||
/// Used to store render material to object relationships in root collections
|
||||
/// <remarks> These proxy lives in Objects library because it depends on RenderMaterial</remarks>
|
||||
/// </summary>
|
||||
[SpeckleType("Objects.Other.RenderMaterialProxy")]
|
||||
public class RenderMaterialProxy : Base, IProxyCollection
|
||||
{
|
||||
/// <summary>
|
||||
/// The list of application ids of objects that use this render material
|
||||
/// </summary>
|
||||
public required List<string> objects { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The render material used by <see cref="objects"/>
|
||||
/// </summary>
|
||||
public required RenderMaterial value { get; set; }
|
||||
}
|
||||
@@ -1,37 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup Label="Compiler Properties">
|
||||
<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
|
||||
<PolySharpExcludeGeneratedTypes>System.Runtime.CompilerServices.RequiresLocationAttribute</PolySharpExcludeGeneratedTypes>
|
||||
<Configurations>Debug;Release;Local</Configurations>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Nugetspec Package Properties">
|
||||
<PackageId>Speckle.Objects</PackageId>
|
||||
<Description>Objects is the default object model for Speckle</Description>
|
||||
<PackageTags>$(PackageTags) objects</PackageTags>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Nuget Package Properties">
|
||||
<IsPackable>true</IsPackable>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Analyers">
|
||||
<NoWarn>
|
||||
$(NoWarn);
|
||||
CA1819;CA1008;CA2225;
|
||||
</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup Label="Expose internals to test projects">
|
||||
<InternalsVisibleTo Include="Speckle.Objects.Tests.Unit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Label="Project References">
|
||||
<ProjectReference Include="..\Speckle.Sdk\Speckle.Sdk.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -472,7 +472,6 @@
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"GraphQL.Client": "[6.0.0, )",
|
||||
"Microsoft.CSharp": "[4.7.0, )",
|
||||
"Microsoft.Data.Sqlite": "[7.0.5, )",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "[2.2.0, )",
|
||||
"Microsoft.Extensions.Logging": "[2.2.0, )",
|
||||
@@ -495,12 +494,6 @@
|
||||
"System.Reactive": "5.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.CSharp": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[4.7.0, )",
|
||||
"resolved": "4.7.0",
|
||||
"contentHash": "pTj+D3uJWyN3My70i2Hqo+OXixq3Os2D1nJ2x92FFo6sk8fYS1m1WLNTs0Dc1uPaViH0YvEEwvzddQ7y4rhXmA=="
|
||||
},
|
||||
"Microsoft.Data.Sqlite": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[7.0.5, )",
|
||||
|
||||
@@ -22,23 +22,4 @@ public static class Collections
|
||||
public static class EnumerableExtensions
|
||||
{
|
||||
public static IEnumerable<int> RangeFrom(int from, int to) => Enumerable.Range(from, to - from + 1);
|
||||
|
||||
#if NETSTANDARD2_0
|
||||
public static IEnumerable<TSource> DistinctBy<TSource, TKey>(
|
||||
this IEnumerable<TSource> source,
|
||||
Func<TSource, TKey> keySelector
|
||||
)
|
||||
{
|
||||
var keys = new HashSet<TKey>();
|
||||
foreach (var element in source)
|
||||
{
|
||||
if (keys.Contains(keySelector(element)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
keys.Add(keySelector(element));
|
||||
yield return element;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace Speckle.Sdk.Dependencies.Serialization;
|
||||
|
||||
public abstract class ChannelLoader<T>(CancellationToken cancellationToken)
|
||||
{
|
||||
private const int RECEIVE_CAPACITY = 5000;
|
||||
private const int RECEIVE_CAPACITY = 10000;
|
||||
|
||||
private const int HTTP_GET_CHUNK_SIZE = 500;
|
||||
private const int MAX_PARALLELISM_HTTP = 4;
|
||||
@@ -109,6 +109,9 @@ public abstract class ChannelLoader<T>(CancellationToken cancellationToken)
|
||||
Exception = ex;
|
||||
_channel.Writer.TryComplete(ex);
|
||||
//cancel everything!
|
||||
_cts.Cancel();
|
||||
if (!_cts.IsCancellationRequested)
|
||||
{
|
||||
_cts.Cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,13 +8,13 @@ namespace Speckle.Sdk.Dependencies.Serialization;
|
||||
public abstract class ChannelSaver<T>
|
||||
where T : IHasByteSize
|
||||
{
|
||||
private const int SEND_CAPACITY = 500;
|
||||
private const int SEND_CAPACITY = 10000;
|
||||
private const int HTTP_SEND_CHUNK_SIZE = 25_000_000; //bytes
|
||||
private static readonly TimeSpan HTTP_BATCH_TIMEOUT = TimeSpan.FromSeconds(2);
|
||||
private const int MAX_PARALLELISM_HTTP = 4;
|
||||
private const int HTTP_CAPACITY = 500;
|
||||
private const int MAX_CACHE_WRITE_PARALLELISM = 4;
|
||||
private const int MAX_CACHE_BATCH = 500;
|
||||
private const int MAX_CACHE_WRITE_PARALLELISM = 1;
|
||||
private const int MAX_CACHE_BATCH = 1000;
|
||||
|
||||
private readonly Channel<T> _checkCacheChannel = Channel.CreateBounded<T>(
|
||||
new BoundedChannelOptions(SEND_CAPACITY)
|
||||
@@ -28,19 +28,24 @@ public abstract class ChannelSaver<T>
|
||||
_ => throw new NotImplementedException("Dropping items not supported.")
|
||||
);
|
||||
|
||||
public Task Start(CancellationToken cancellationToken) =>
|
||||
public Task Start(
|
||||
int? maxParallelism,
|
||||
int? httpBatchSize,
|
||||
int? cacheBatchSize,
|
||||
CancellationToken cancellationToken
|
||||
) =>
|
||||
_checkCacheChannel
|
||||
.Reader.BatchByByteSize(HTTP_SEND_CHUNK_SIZE)
|
||||
.Reader.BatchByByteSize(httpBatchSize ?? HTTP_SEND_CHUNK_SIZE)
|
||||
.WithTimeout(HTTP_BATCH_TIMEOUT)
|
||||
.PipeAsync(
|
||||
MAX_PARALLELISM_HTTP,
|
||||
maxParallelism ?? MAX_PARALLELISM_HTTP,
|
||||
async x => await SendToServer(x).ConfigureAwait(false),
|
||||
HTTP_CAPACITY,
|
||||
false,
|
||||
cancellationToken
|
||||
)
|
||||
.Join()
|
||||
.Batch(MAX_CACHE_BATCH)
|
||||
.Batch(cacheBatchSize ?? MAX_CACHE_BATCH, singleReader: true)
|
||||
.WithTimeout(HTTP_BATCH_TIMEOUT)
|
||||
.ReadAllConcurrently(MAX_CACHE_WRITE_PARALLELISM, SaveToCache, cancellationToken)
|
||||
.ContinueWith(
|
||||
@@ -63,14 +68,15 @@ public abstract class ChannelSaver<T>
|
||||
TaskScheduler.Current
|
||||
);
|
||||
|
||||
public void Save(T item, CancellationToken cancellationToken)
|
||||
public async Task SaveAsync(T item, CancellationToken cancellationToken)
|
||||
{
|
||||
if (Exception is not null || cancellationToken.IsCancellationRequested)
|
||||
if (Exception is not null)
|
||||
{
|
||||
return; //don't save if we're already done through an error
|
||||
}
|
||||
// ReSharper disable once MethodSupportsCancellation
|
||||
_checkCacheChannel.Writer.TryWrite(item);
|
||||
//can switch to check then try pattern when back pressure is needed or exceptions are too much
|
||||
//the trees don't need to respond to back pressure
|
||||
await _checkCacheChannel.Writer.WriteAsync(item, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<IMemoryOwner<T>> SendToServer(IMemoryOwner<T> batch)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup Label="Compiler Properties">
|
||||
<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
|
||||
<Configurations>Debug;Release;Local</Configurations>
|
||||
@@ -7,30 +6,26 @@
|
||||
<ILRepackRenameInternalized>true</ILRepackRenameInternalized>
|
||||
<ILRepackMergeDebugSymbols>true</ILRepackMergeDebugSymbols>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Nugetspec Package Properties">
|
||||
<PackageId>Speckle.Sdk.Dependencies</PackageId>
|
||||
<Description>The .NET SDK for Speckle</Description>
|
||||
<PackageTags>$(PackageTags) core sdk</PackageTags>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Nuget Package Properties">
|
||||
<IsPackable>true</IsPackable>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="ILRepack.FullAuto">
|
||||
<PackageReference Include="ILRepack.FullAuto">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.ObjectPool" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.Extensions.ObjectPool" PrivateAssets="all" />
|
||||
<PackageReference Include="Polly" PrivateAssets="all" />
|
||||
<PackageReference Include="Polly.Contrib.WaitAndRetry" PrivateAssets="all" />
|
||||
<PackageReference Include="Polly.Extensions.Http" PrivateAssets="all" />
|
||||
<PackageReference Include="Open.ChannelExtensions" PrivateAssets="all" />
|
||||
<PackageReference Include="System.Threading.Channels" PrivateAssets="all" />
|
||||
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -41,13 +41,7 @@ internal sealed class SpeckleHttpClientHandler : DelegatingHandler
|
||||
activity?.InjectHeaders((k, v) => request.Headers.TryAddWithoutValidation(k, v));
|
||||
|
||||
var policyResult = await _resiliencePolicy
|
||||
.ExecuteAndCaptureAsync(
|
||||
ctx =>
|
||||
{
|
||||
return base.SendAsync(request, cancellationToken);
|
||||
},
|
||||
context
|
||||
)
|
||||
.ExecuteAndCaptureAsync(ctx => base.SendAsync(request, cancellationToken), context)
|
||||
.ConfigureAwait(false);
|
||||
context.TryGetValue("retryCount", out var retryCount);
|
||||
activity?.SetTag("retryCount", retryCount);
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
},
|
||||
"Microsoft.Extensions.ObjectPool": {
|
||||
"type": "Direct",
|
||||
"requested": "[9.0.3, )",
|
||||
"resolved": "9.0.3",
|
||||
"contentHash": "4uPdnj9hLRrb3ZSeVEDtwIm9nNrrT9vAXYC9o1/yTW8lGOPwTyI2QlkcICwYEGM1LESGTFidcPMFACznUZKbIQ=="
|
||||
"requested": "[9.0.4, )",
|
||||
"resolved": "9.0.4",
|
||||
"contentHash": "G7p1k2xVZ+2aVANz0JdSiafr+AHDHeS1kF8+Y0ABbIsByd0erOL59IDXBs9vcdJf3pPV/murO0mbtr4k40QxWw=="
|
||||
},
|
||||
"Microsoft.SourceLink.GitHub": {
|
||||
"type": "Direct",
|
||||
@@ -38,13 +38,13 @@
|
||||
},
|
||||
"Open.ChannelExtensions": {
|
||||
"type": "Direct",
|
||||
"requested": "[9.0.0, )",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "DP+l5S6G46wcuY4I4kNXE+RDOmJr0DKuMienOdt0mMBN9z7vmLSC8YQbqCyb9i9LNjXj1tgCx5LyitJiRr/v7g==",
|
||||
"requested": "[9.1.0, )",
|
||||
"resolved": "9.1.0",
|
||||
"contentHash": "D6c24vMGy1oZ06vmkD2/FNzWHK7ZIihuv2spDgYEeaUp+eobrILQnrNQKRoASFXD4JGfZ7nfvTM0e+AX79dt8Q==",
|
||||
"dependencies": {
|
||||
"Microsoft.Bcl.AsyncInterfaces": "9.0.0",
|
||||
"System.Collections.Immutable": "9.0.0",
|
||||
"System.Threading.Channels": "9.0.0"
|
||||
"Microsoft.Bcl.AsyncInterfaces": "9.0.4",
|
||||
"System.Collections.Immutable": "9.0.4",
|
||||
"System.Threading.Channels": "9.0.4"
|
||||
}
|
||||
},
|
||||
"Polly": {
|
||||
@@ -82,11 +82,11 @@
|
||||
},
|
||||
"System.Threading.Channels": {
|
||||
"type": "Direct",
|
||||
"requested": "[9.0.2, )",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "pUmqkuBS9OxWHOlfNad09Oxc8gRbxgN9UQtsqHPst4jfcgZRxQetNcsT2oe+VnUpEFAtBy1FZcJZiOscrBmA7g==",
|
||||
"requested": "[9.0.4, )",
|
||||
"resolved": "9.0.4",
|
||||
"contentHash": "4qBn2H6/aXBpE/Pm3wY5yusY/pEvQz99NlWHrTUji0qCmOdbhhjaALcpmbfW2ksxlPM6i6S+QFLkpOQdyfeKYQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Bcl.AsyncInterfaces": "9.0.2",
|
||||
"Microsoft.Bcl.AsyncInterfaces": "9.0.4",
|
||||
"System.Threading.Tasks.Extensions": "4.5.4"
|
||||
}
|
||||
},
|
||||
@@ -117,8 +117,8 @@
|
||||
},
|
||||
"System.Collections.Immutable": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "QhkXUl2gNrQtvPmtBTQHb0YsUrDiDQ2QS09YbtTTiSjGcf7NBqtYbrG/BE06zcBPCKEwQGzIv13IVdXNOSub2w==",
|
||||
"resolved": "9.0.4",
|
||||
"contentHash": "wfm2NgK22MmBe5qJjp52qzpkeDZKb4l9LbdubhZSehY1z4LS+lld6R+B+UQNb2AZRHu/QJlHxEUcRst5hIEejg==",
|
||||
"dependencies": {
|
||||
"System.Memory": "4.5.5",
|
||||
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
|
||||
@@ -155,8 +155,8 @@
|
||||
"Microsoft.Bcl.AsyncInterfaces": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[5.0.0, )",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "1CED0BGD7dCKsbe7tDhzpPB2Qdi9x35QChu6zkBEI4s0T5bDkkttGReqQnOeOfRNSxtP2WvpX6Ik/0O93XDuMw==",
|
||||
"resolved": "9.0.4",
|
||||
"contentHash": "9VGI5kxIvrNG2mqLQZnUR6y/3fcnygD8eNpHR+CqfbnIXvea6nehnYknDKQTxZVPMpzpNca+7DxLBmpdB3q0Bw==",
|
||||
"dependencies": {
|
||||
"System.Threading.Tasks.Extensions": "4.5.4"
|
||||
}
|
||||
@@ -174,9 +174,9 @@
|
||||
},
|
||||
"Microsoft.Extensions.ObjectPool": {
|
||||
"type": "Direct",
|
||||
"requested": "[9.0.3, )",
|
||||
"resolved": "9.0.3",
|
||||
"contentHash": "4uPdnj9hLRrb3ZSeVEDtwIm9nNrrT9vAXYC9o1/yTW8lGOPwTyI2QlkcICwYEGM1LESGTFidcPMFACznUZKbIQ=="
|
||||
"requested": "[9.0.4, )",
|
||||
"resolved": "9.0.4",
|
||||
"contentHash": "G7p1k2xVZ+2aVANz0JdSiafr+AHDHeS1kF8+Y0ABbIsByd0erOL59IDXBs9vcdJf3pPV/murO0mbtr4k40QxWw=="
|
||||
},
|
||||
"Microsoft.SourceLink.GitHub": {
|
||||
"type": "Direct",
|
||||
@@ -190,9 +190,9 @@
|
||||
},
|
||||
"Open.ChannelExtensions": {
|
||||
"type": "Direct",
|
||||
"requested": "[9.0.0, )",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "DP+l5S6G46wcuY4I4kNXE+RDOmJr0DKuMienOdt0mMBN9z7vmLSC8YQbqCyb9i9LNjXj1tgCx5LyitJiRr/v7g=="
|
||||
"requested": "[9.1.0, )",
|
||||
"resolved": "9.1.0",
|
||||
"contentHash": "D6c24vMGy1oZ06vmkD2/FNzWHK7ZIihuv2spDgYEeaUp+eobrILQnrNQKRoASFXD4JGfZ7nfvTM0e+AX79dt8Q=="
|
||||
},
|
||||
"Polly": {
|
||||
"type": "Direct",
|
||||
@@ -229,9 +229,9 @@
|
||||
},
|
||||
"System.Threading.Channels": {
|
||||
"type": "Direct",
|
||||
"requested": "[9.0.2, )",
|
||||
"resolved": "9.0.2",
|
||||
"contentHash": "pUmqkuBS9OxWHOlfNad09Oxc8gRbxgN9UQtsqHPst4jfcgZRxQetNcsT2oe+VnUpEFAtBy1FZcJZiOscrBmA7g=="
|
||||
"requested": "[9.0.4, )",
|
||||
"resolved": "9.0.4",
|
||||
"contentHash": "4qBn2H6/aXBpE/Pm3wY5yusY/pEvQz99NlWHrTUji0qCmOdbhhjaALcpmbfW2ksxlPM6i6S+QFLkpOQdyfeKYQ=="
|
||||
},
|
||||
"ILRepack": {
|
||||
"type": "Transitive",
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using Speckle.InterfaceGenerator;
|
||||
using Speckle.Newtonsoft.Json;
|
||||
using Speckle.Sdk.Api.GraphQL.Resources;
|
||||
using Speckle.Sdk.Common;
|
||||
using Speckle.Sdk.Credentials;
|
||||
using Speckle.Sdk.Helpers;
|
||||
using Speckle.Sdk.Logging;
|
||||
using Speckle.Sdk.Transports;
|
||||
using Speckle.Sdk.Transports.ServerUtils;
|
||||
|
||||
namespace Speckle.Sdk.Api.Blob;
|
||||
|
||||
public partial interface IBlobApi : IDisposable;
|
||||
|
||||
/// <summary>
|
||||
/// Low level access to the blob API
|
||||
/// </summary>
|
||||
/// <seealso cref="FileImportResource"/>
|
||||
/// <seealso cref="ServerApi"/>
|
||||
[GenerateAutoInterface]
|
||||
public sealed class BlobApi : IBlobApi
|
||||
{
|
||||
public const int DEFAULT_TIMEOUT_SECONDS = SpeckleHttp.DEFAULT_TIMEOUT_SECONDS;
|
||||
private static readonly string[] s_filenameSeparator = ["filename="];
|
||||
|
||||
private readonly ISdkActivityFactory _activityFactory;
|
||||
|
||||
/// <summary>
|
||||
/// HTTP client for communicating with Speckle Server with auth token header
|
||||
/// </summary>
|
||||
private readonly HttpClient _authedClient;
|
||||
|
||||
/// <summary>
|
||||
/// HTTP client for communicating with pre-signed s3 url
|
||||
/// </summary>
|
||||
private readonly HttpClient _unauthedClient;
|
||||
|
||||
public BlobApi(
|
||||
ISpeckleHttp speckleHttp,
|
||||
ISdkActivityFactory activityFactory,
|
||||
Account account,
|
||||
int timeoutSeconds = DEFAULT_TIMEOUT_SECONDS
|
||||
)
|
||||
{
|
||||
_activityFactory = activityFactory;
|
||||
_authedClient = speckleHttp.CreateHttpClient(
|
||||
new HttpClientHandler { AutomaticDecompression = DecompressionMethods.GZip },
|
||||
timeoutSeconds: timeoutSeconds,
|
||||
authorizationToken: account.token
|
||||
);
|
||||
_authedClient.BaseAddress = new(account.serverInfo.url);
|
||||
|
||||
_unauthedClient = speckleHttp.CreateHttpClient(
|
||||
new HttpClientHandler { AutomaticDecompression = DecompressionMethods.GZip },
|
||||
timeoutSeconds: timeoutSeconds
|
||||
);
|
||||
}
|
||||
|
||||
private static string GetBlobDownloadPath(string blobId, HttpResponseMessage response)
|
||||
{
|
||||
response.Content.Headers.TryGetValues("Content-Disposition", out IEnumerable<string>? cdHeaderValues);
|
||||
var cdHeader = (cdHeaderValues?.FirstOrDefault()).NotNull(
|
||||
"Expected response from server to contain attachment header"
|
||||
);
|
||||
string fileName = cdHeader.Split(s_filenameSeparator, StringSplitOptions.None)[1].TrimStart('"').TrimEnd('"');
|
||||
return Path.Combine(
|
||||
SpecklePathProvider.BlobStoragePath(),
|
||||
$"{blobId[..Models.Blob.LocalHashPrefixLength]}-{fileName}"
|
||||
);
|
||||
}
|
||||
|
||||
/// <param name="blobId">The ID of the blob to download</param>
|
||||
/// <param name="progress"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <exception cref="HttpRequestException">Request for the blob fails</exception>
|
||||
/// <exception cref="OperationCanceledException"></exception>
|
||||
/// <returns>File Path of the downloaded file</returns>
|
||||
public async Task<string> DownloadBlob(
|
||||
string projectId,
|
||||
string blobId,
|
||||
string? pathOverride = null,
|
||||
IProgress<ProgressArgs>? progress = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
using var _ = _activityFactory.Start();
|
||||
|
||||
var url = new Uri($"api/stream/{projectId}/blob/{blobId}", UriKind.Relative);
|
||||
|
||||
using var response = await _authedClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
string fileLocation = pathOverride ?? GetBlobDownloadPath(blobId, response);
|
||||
using var source = new ProgressStream(
|
||||
#if NET5_0_OR_GREATER
|
||||
await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false),
|
||||
#else
|
||||
await response.Content.ReadAsStreamAsync().ConfigureAwait(false),
|
||||
#endif
|
||||
response.Content.Headers.ContentLength,
|
||||
progress,
|
||||
true
|
||||
);
|
||||
|
||||
using var fs = new FileStream(fileLocation, FileMode.OpenOrCreate);
|
||||
#if NET5_0_OR_GREATER
|
||||
await source.CopyToAsync(fs, cancellationToken).ConfigureAwait(false);
|
||||
#else
|
||||
await source.CopyToAsync(fs).ConfigureAwait(false);
|
||||
#endif
|
||||
return fileLocation;
|
||||
}
|
||||
|
||||
/// <summary>Queries the server for diff of the given <paramref name="blobIds"/></summary>
|
||||
/// <param name="blobIds"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns>A list of blob ids that the server doesn't have</returns>
|
||||
/// <exception cref="HttpRequestException">Request for the blob fails</exception>
|
||||
/// <exception cref="OperationCanceledException"></exception>
|
||||
/// <exception cref="ArgumentNullException"></exception>
|
||||
public async Task<List<string>> HasBlobs(
|
||||
string projectId,
|
||||
IReadOnlyCollection<string> blobIds,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
using var _ = _activityFactory.Start();
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var payload = JsonConvert.SerializeObject(blobIds);
|
||||
|
||||
var url = new Uri($"/api/stream/{projectId}/blob/diff", UriKind.Relative);
|
||||
|
||||
using StringContent stringContent = new(payload, Encoding.UTF8, "application/json");
|
||||
|
||||
using var response = await _authedClient.PostAsync(url, stringContent, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
#if NET5_0_OR_GREATER
|
||||
var responseString = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
#else
|
||||
var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
#endif
|
||||
var parsed = JsonConvert
|
||||
.DeserializeObject<List<string>>(responseString)
|
||||
.NotNull($"Failed to deserialize successful response {response.Content}");
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uploads a single file to the given S3 url.
|
||||
/// This method should be used together with the <see cref="FileImportResource"/> <see cref="FileImportResource.GenerateUploadUrl"/> method,
|
||||
/// which generates a pre-signed S3 url, that can be used to upload the file to.
|
||||
/// </summary>
|
||||
/// <param name="filePath"></param>
|
||||
/// <param name="url"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns>etag header</returns>
|
||||
/// <seealso cref="FileImportResource"/>
|
||||
/// <exception cref="HttpRequestException"></exception>
|
||||
/// <exception cref="ArgumentException">Unexpected response header the server</exception>
|
||||
/// <exception cref="FileNotFoundException"><paramref name="filePath"/> does not point to a file</exception>
|
||||
/// <exception cref="OperationCanceledException"></exception>
|
||||
public async Task<string> UploadFile(
|
||||
string filePath,
|
||||
Uri url,
|
||||
IProgress<ProgressArgs>? progress = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
using var _ = _activityFactory.Start();
|
||||
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
throw new FileNotFoundException("File not found.", filePath);
|
||||
}
|
||||
|
||||
var fileInfo = new FileInfo(filePath);
|
||||
|
||||
using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
|
||||
using var requestMessage = new HttpRequestMessage(HttpMethod.Put, url);
|
||||
requestMessage.Content = progress is null
|
||||
? new StreamContent(fileStream)
|
||||
: new ProgressContent(new StreamContent(fileStream), progress);
|
||||
|
||||
requestMessage.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
|
||||
requestMessage.Content.Headers.ContentLength = fileInfo.Length;
|
||||
|
||||
using var response = await _unauthedClient.SendAsync(requestMessage, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
return ParseEtagHeader(response.Headers);
|
||||
}
|
||||
|
||||
private static string ParseEtagHeader(HttpResponseHeaders headers)
|
||||
{
|
||||
if (!headers.TryGetValues("ETag", out var etagValues))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Response does not have an ETag attached to it, cannot use this as an upload",
|
||||
nameof(headers)
|
||||
);
|
||||
}
|
||||
|
||||
var etagValuesArray = etagValues.ToArray();
|
||||
|
||||
if (etagValuesArray.Length != 1)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"Expected Etag header to have a single value but got {etagValuesArray.Length}",
|
||||
nameof(headers)
|
||||
);
|
||||
}
|
||||
|
||||
return etagValuesArray[0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uploads blobs via the <c>"/api/stream/:streamId/blob"</c> endpoint
|
||||
/// </summary>
|
||||
/// <param name="projectId"></param>
|
||||
/// <param name="blobPaths"></param>
|
||||
/// <param name="progress"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
public async Task UploadBlobs(
|
||||
string projectId,
|
||||
IReadOnlyCollection<(string id, string filePath)> blobPaths,
|
||||
IProgress<ProgressArgs>? progress,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
using var _ = _activityFactory.Start();
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (blobPaths.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var multipartFormDataContent = new MultipartFormDataContent();
|
||||
foreach (var (id, filePath) in blobPaths)
|
||||
{
|
||||
var fileName = Path.GetFileName(filePath);
|
||||
|
||||
var stream = File.OpenRead(filePath);
|
||||
var fsc = new StreamContent(stream);
|
||||
multipartFormDataContent.Add(fsc, $"hash:{id}", fileName);
|
||||
}
|
||||
|
||||
using HttpContent content = progress is null
|
||||
? multipartFormDataContent
|
||||
: new ProgressContent(multipartFormDataContent, progress);
|
||||
|
||||
var url = new Uri($"/api/stream/{projectId}/blob", UriKind.Relative);
|
||||
|
||||
using var response = await _authedClient.PostAsync(url, content, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
[AutoInterfaceIgnore]
|
||||
public void Dispose()
|
||||
{
|
||||
_activityFactory.Dispose();
|
||||
_authedClient.Dispose();
|
||||
_unauthedClient.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using Speckle.InterfaceGenerator;
|
||||
using Speckle.Sdk.Credentials;
|
||||
using Speckle.Sdk.Helpers;
|
||||
using Speckle.Sdk.Logging;
|
||||
|
||||
namespace Speckle.Sdk.Api.Blob;
|
||||
|
||||
[GenerateAutoInterface]
|
||||
public sealed class BlobApiFactory(ISpeckleHttp speckleHttp, ISdkActivityFactory activityFactory) : IBlobApiFactory
|
||||
{
|
||||
public IBlobApi Create(Account account, int timeoutSeconds = BlobApi.DEFAULT_TIMEOUT_SECONDS) =>
|
||||
new BlobApi(speckleHttp, activityFactory, account, timeoutSeconds);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Speckle.Sdk.Api.GraphQL;
|
||||
using Speckle.Sdk.Api.GraphQL.Models;
|
||||
|
||||
namespace Speckle.Sdk.Api;
|
||||
|
||||
@@ -96,3 +97,43 @@ public sealed class SpeckleGraphQLInvalidQueryException : SpeckleGraphQLExceptio
|
||||
public SpeckleGraphQLInvalidQueryException(string? message, Exception? innerException)
|
||||
: base(message, innerException) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a <c>WORKSPACES_MODULE_DISABLED_ERROR</c> GraphQL error as an exception
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A GraphQL request for workspace resources was made to a server that does not have the <c>FF_WORKSPACES_MODULE_ENABLED</c> feature flag enabled
|
||||
/// </remarks>
|
||||
public sealed class SpeckleGraphQLWorkspaceNotEnabledException : SpeckleGraphQLException
|
||||
{
|
||||
public SpeckleGraphQLWorkspaceNotEnabledException() { }
|
||||
|
||||
public SpeckleGraphQLWorkspaceNotEnabledException(string? message)
|
||||
: base(message) { }
|
||||
|
||||
public SpeckleGraphQLWorkspaceNotEnabledException(string? message, Exception? innerException)
|
||||
: base(message, innerException) { }
|
||||
}
|
||||
|
||||
/// <seealso cref="PermissionCheckResult"/>
|
||||
public sealed class WorkspacePermissionException : SpeckleGraphQLException
|
||||
{
|
||||
public WorkspacePermissionException() { }
|
||||
|
||||
public WorkspacePermissionException(string? message)
|
||||
: base(message) { }
|
||||
|
||||
public WorkspacePermissionException(string? message, Exception? innerException)
|
||||
: base(message, innerException) { }
|
||||
}
|
||||
|
||||
public sealed class CannotCreateCommitException : SpeckleGraphQLException
|
||||
{
|
||||
public CannotCreateCommitException() { }
|
||||
|
||||
public CannotCreateCommitException(string? message)
|
||||
: base(message) { }
|
||||
|
||||
public CannotCreateCommitException(string? message, Exception? innerException)
|
||||
: base(message, innerException) { }
|
||||
}
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net.WebSockets;
|
||||
using System.Reflection;
|
||||
using GraphQL;
|
||||
using GraphQL.Client.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Speckle.InterfaceGenerator;
|
||||
using Speckle.Newtonsoft.Json;
|
||||
using Speckle.Newtonsoft.Json.Serialization;
|
||||
using Speckle.Sdk.Api.Blob;
|
||||
using Speckle.Sdk.Api.GraphQL;
|
||||
using Speckle.Sdk.Api.GraphQL.Resources;
|
||||
using Speckle.Sdk.Api.GraphQL.Serializer;
|
||||
using Speckle.Sdk.Credentials;
|
||||
using Speckle.Sdk.Dependencies;
|
||||
using Speckle.Sdk.Helpers;
|
||||
using Speckle.Sdk.Logging;
|
||||
|
||||
namespace Speckle.Sdk.Api;
|
||||
@@ -36,14 +32,15 @@ public sealed class Client : ISpeckleGraphQLClient, IClient
|
||||
public ProjectInviteResource ProjectInvite { get; }
|
||||
public CommentResource Comment { get; }
|
||||
public SubscriptionResource Subscription { get; }
|
||||
public WorkspaceResource Workspace { get; }
|
||||
public ServerResource Server { get; }
|
||||
public FileImportResource FileImport { get; }
|
||||
|
||||
public Uri ServerUrl => new(Account.serverInfo.url);
|
||||
|
||||
[JsonIgnore]
|
||||
public Account Account { get; }
|
||||
|
||||
private HttpClient HttpClient { get; }
|
||||
|
||||
[AutoInterfaceIgnore]
|
||||
public GraphQLHttpClient GQLClient { get; }
|
||||
|
||||
@@ -52,14 +49,16 @@ public sealed class Client : ISpeckleGraphQLClient, IClient
|
||||
public Client(
|
||||
ILogger<Client> logger,
|
||||
ISdkActivityFactory activityFactory,
|
||||
ISpeckleApplication application,
|
||||
ISpeckleHttp speckleHttp,
|
||||
Account account
|
||||
IGraphQLClientFactory graphqlClientFactory,
|
||||
IBlobApiFactory blobApiFactory,
|
||||
[NotNull] Account? account
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
_activityFactory = activityFactory;
|
||||
|
||||
Account = account ?? throw new ArgumentException("Provided account is null.");
|
||||
GQLClient = graphqlClientFactory.CreateGraphQLClient(account);
|
||||
|
||||
Project = new(this);
|
||||
Model = new(this);
|
||||
@@ -69,10 +68,9 @@ public sealed class Client : ISpeckleGraphQLClient, IClient
|
||||
ProjectInvite = new(this);
|
||||
Comment = new(this);
|
||||
Subscription = new(this);
|
||||
|
||||
HttpClient = CreateHttpClient(application, speckleHttp, account);
|
||||
|
||||
GQLClient = CreateGraphQLClient(account, HttpClient);
|
||||
Workspace = new(this);
|
||||
Server = new(this);
|
||||
FileImport = new(this, blobApiFactory.Create(account));
|
||||
}
|
||||
|
||||
[AutoInterfaceIgnore]
|
||||
@@ -80,6 +78,7 @@ public sealed class Client : ISpeckleGraphQLClient, IClient
|
||||
{
|
||||
try
|
||||
{
|
||||
FileImport.Dispose();
|
||||
Subscription.Dispose();
|
||||
GQLClient.Dispose();
|
||||
}
|
||||
@@ -105,7 +104,7 @@ public sealed class Client : ISpeckleGraphQLClient, IClient
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}" />
|
||||
public async Task<T> ExecuteGraphQLRequest<T>(GraphQLRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var activity = _activityFactory.Start();
|
||||
@@ -133,10 +132,10 @@ public sealed class Client : ISpeckleGraphQLClient, IClient
|
||||
activity?.SetStatus(SdkActivityStatusCode.Ok);
|
||||
return ret;
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception)
|
||||
{
|
||||
activity?.SetStatus(SdkActivityStatusCode.Error);
|
||||
activity?.RecordException(ex);
|
||||
// Don't record exception as it's rethrown.
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@@ -186,62 +185,4 @@ public sealed class Client : ISpeckleGraphQLClient, IClient
|
||||
throw new SpeckleGraphQLException($"Subscription for {typeof(T)} failed to start", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static GraphQLHttpClient CreateGraphQLClient(Account account, HttpClient httpClient)
|
||||
{
|
||||
var gQLClient = new GraphQLHttpClient(
|
||||
new GraphQLHttpClientOptions
|
||||
{
|
||||
EndPoint = new Uri(new Uri(account.serverInfo.url), "/graphql"),
|
||||
UseWebSocketForQueriesAndMutations = false,
|
||||
WebSocketProtocol = "graphql-ws",
|
||||
ConfigureWebSocketConnectionInitPayload = _ =>
|
||||
{
|
||||
return SpeckleHttp.CanAddAuth(account.token, out string? authValue)
|
||||
? new { Authorization = authValue }
|
||||
: null;
|
||||
},
|
||||
},
|
||||
new NewtonsoftJsonSerializer(
|
||||
new JsonSerializerSettings()
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver { IgnoreIsSpecifiedMembers = true }, //(Default)
|
||||
MissingMemberHandling = MissingMemberHandling.Error, //(not default) If you query for a member that doesn't exist, this will throw (except websocket responses see https://github.com/graphql-dotnet/graphql-client/issues/660)
|
||||
Converters =
|
||||
{
|
||||
new ConstantCaseEnumConverter(),
|
||||
} //(Default) enums will be serialized using the GraphQL const case standard
|
||||
,
|
||||
}
|
||||
),
|
||||
httpClient
|
||||
);
|
||||
|
||||
gQLClient.WebSocketReceiveErrors.Subscribe(e =>
|
||||
{
|
||||
if (e is WebSocketException we)
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"WebSocketException: {we.Message} (WebSocketError {we.WebSocketErrorCode}, ErrorCode {we.ErrorCode}, NativeErrorCode {we.NativeErrorCode}"
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"Exception in websocket receive stream: {e}");
|
||||
}
|
||||
});
|
||||
return gQLClient;
|
||||
}
|
||||
|
||||
private static HttpClient CreateHttpClient(ISpeckleApplication application, ISpeckleHttp speckleHttp, Account account)
|
||||
{
|
||||
var httpClient = speckleHttp.CreateHttpClient(timeoutSeconds: 30, authorizationToken: account.token);
|
||||
|
||||
httpClient.DefaultRequestHeaders.Add("apollographql-client-name", application.ApplicationAndVersion);
|
||||
httpClient.DefaultRequestHeaders.Add(
|
||||
"apollographql-client-version",
|
||||
Assembly.GetExecutingAssembly().GetName().Version?.ToString()
|
||||
);
|
||||
return httpClient;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Speckle.InterfaceGenerator;
|
||||
using Speckle.Sdk.Api.Blob;
|
||||
using Speckle.Sdk.Credentials;
|
||||
using Speckle.Sdk.Helpers;
|
||||
using Speckle.Sdk.Logging;
|
||||
|
||||
namespace Speckle.Sdk.Api;
|
||||
@@ -10,10 +10,10 @@ namespace Speckle.Sdk.Api;
|
||||
public class ClientFactory(
|
||||
ILoggerFactory loggerFactory,
|
||||
ISdkActivityFactory activityFactory,
|
||||
ISpeckleApplication application,
|
||||
ISpeckleHttp speckleHttp
|
||||
IGraphQLClientFactory graphQLClientFactory,
|
||||
IBlobApiFactory blobApiFactory
|
||||
) : IClientFactory
|
||||
{
|
||||
public IClient Create(Account account) =>
|
||||
new Client(loggerFactory.CreateLogger<Client>(), activityFactory, application, speckleHttp, account);
|
||||
new Client(loggerFactory.CreateLogger<Client>(), activityFactory, graphQLClientFactory, blobApiFactory, account);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
||||
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
||||
|
||||
public enum ProjectVisibility
|
||||
{
|
||||
Private = 0,
|
||||
Private,
|
||||
Public,
|
||||
|
||||
[Obsolete("Use Unlisted instead", true)]
|
||||
Public = 1,
|
||||
Unlisted = 2,
|
||||
[Obsolete("Use Public instead")]
|
||||
Unlisted,
|
||||
Workspace,
|
||||
}
|
||||
|
||||
@@ -32,6 +32,8 @@ internal static class GraphQLErrorHandler
|
||||
"STREAM_NOT_FOUND" => new SpeckleGraphQLStreamNotFoundException(message),
|
||||
"BAD_USER_INPUT" => new SpeckleGraphQLBadInputException(message),
|
||||
"INTERNAL_SERVER_ERROR" => new SpeckleGraphQLInternalErrorException(message),
|
||||
"WORKSPACES_MODULE_DISABLED_ERROR" => new SpeckleGraphQLWorkspaceNotEnabledException(message),
|
||||
"COMMIT_CREATE_ERROR" => new CannotCreateCommitException(message),
|
||||
_ => new SpeckleGraphQLException(message),
|
||||
};
|
||||
exceptions.Add(ex);
|
||||
|
||||
@@ -40,7 +40,8 @@ public static class GraphQLHttpClientExtensions
|
||||
response.EnsureGraphQLSuccess();
|
||||
|
||||
string versionString = response.Data.data.data;
|
||||
if (versionString == "dev")
|
||||
//Local server builds will have a non-numerical version string
|
||||
if (versionString == "dev" || versionString == "custom")
|
||||
{
|
||||
return new Version(999, 999, 999);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
namespace Speckle.Sdk.Api.GraphQL.Inputs;
|
||||
|
||||
internal sealed record CommentContentInput(IReadOnlyCollection<string>? blobIds, object? doc);
|
||||
internal record CommentContentInput(IReadOnlyCollection<string>? blobIds, object? doc);
|
||||
|
||||
internal sealed record CreateCommentInput(
|
||||
internal record CreateCommentInput(
|
||||
CommentContentInput content,
|
||||
string projectId,
|
||||
string resourceIdString,
|
||||
@@ -10,10 +10,10 @@ internal sealed record CreateCommentInput(
|
||||
object? viewerState
|
||||
);
|
||||
|
||||
internal sealed record EditCommentInput(CommentContentInput content, string commentId, string projectId);
|
||||
internal record EditCommentInput(CommentContentInput content, string commentId, string projectId);
|
||||
|
||||
internal sealed record CreateCommentReplyInput(CommentContentInput content, string threadId, string projectId);
|
||||
internal record CreateCommentReplyInput(CommentContentInput content, string threadId, string projectId);
|
||||
|
||||
public sealed record MarkCommentViewedInput(string commentId, string projectId);
|
||||
public record MarkCommentViewedInput(string commentId, string projectId);
|
||||
|
||||
public sealed record ArchiveCommentInput(string commentId, string projectId, bool archived = true);
|
||||
public record ArchiveCommentInput(string commentId, string projectId, bool archived = true);
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
namespace Speckle.Sdk.Api.GraphQL.Inputs;
|
||||
|
||||
public record GenerateFileUploadUrlInput(string projectId, string fileName);
|
||||
|
||||
public record StartFileImportInput(string projectId, string modelId, string fileId, string etag);
|
||||
|
||||
public record FileImportResult(
|
||||
double durationSeconds,
|
||||
double downloadDurationSeconds,
|
||||
double parseDurationSeconds,
|
||||
string parser,
|
||||
string? versionId
|
||||
);
|
||||
|
||||
public abstract class FileImportInputBase
|
||||
{
|
||||
public required string projectId { get; init; }
|
||||
public required string jobId { get; init; }
|
||||
public required IReadOnlyCollection<string> warnings { get; init; }
|
||||
public required FileImportResult result { get; init; }
|
||||
}
|
||||
|
||||
#pragma warning disable CA1822 //Mark members as static
|
||||
|
||||
public sealed class FileImportSuccessInput() : FileImportInputBase()
|
||||
{
|
||||
public const string TYPE_STATUS = "success";
|
||||
|
||||
public string status => TYPE_STATUS;
|
||||
}
|
||||
|
||||
public sealed class FileImportErrorInput() : FileImportInputBase()
|
||||
{
|
||||
public const string TYPE_STATUS = "error";
|
||||
|
||||
public string status => TYPE_STATUS;
|
||||
public required string reason { get; init; }
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
namespace Speckle.Sdk.Api.GraphQL.Inputs;
|
||||
|
||||
public sealed record CreateModelInput(string name, string? description, string projectId);
|
||||
public record CreateModelInput(string name, string? description, string projectId);
|
||||
|
||||
public sealed record DeleteModelInput(string id, string projectId);
|
||||
public record DeleteModelInput(string id, string projectId);
|
||||
|
||||
public sealed record UpdateModelInput(string id, string? name, string? description, string projectId);
|
||||
public record UpdateModelInput(string id, string? name, string? description, string projectId);
|
||||
|
||||
public sealed record ModelVersionsFilter(IReadOnlyList<string> priorityIds, bool? priorityIdsOnly);
|
||||
public record ModelVersionsFilter(IReadOnlyList<string> priorityIds, bool? priorityIdsOnly);
|
||||
|
||||
@@ -2,15 +2,22 @@
|
||||
|
||||
namespace Speckle.Sdk.Api.GraphQL.Inputs;
|
||||
|
||||
public sealed record ProjectCommentsFilter(bool? includeArchived, bool? loadedVersionsOnly, string? resourceIdString);
|
||||
public record ProjectCommentsFilter(bool? includeArchived, bool? loadedVersionsOnly, string? resourceIdString);
|
||||
|
||||
public sealed record ProjectCreateInput(string? name, string? description, ProjectVisibility? visibility);
|
||||
public record ProjectCreateInput(string? name, string? description, ProjectVisibility? visibility);
|
||||
|
||||
public sealed record ProjectInviteCreateInput(string? email, string? role, string? serverRole, string? userId);
|
||||
public record WorkspaceProjectCreateInput(
|
||||
string? name,
|
||||
string? description,
|
||||
ProjectVisibility? visibility,
|
||||
string workspaceId
|
||||
);
|
||||
|
||||
public sealed record ProjectInviteUseInput(bool accept, string projectId, string token);
|
||||
public record ProjectInviteCreateInput(string? email, string? role, string? serverRole, string? userId);
|
||||
|
||||
public sealed record ProjectModelsFilter(
|
||||
public record ProjectInviteUseInput(bool accept, string projectId, string token);
|
||||
|
||||
public record ProjectModelsFilter(
|
||||
IReadOnlyList<string>? contributors = null,
|
||||
IReadOnlyList<string>? excludeIds = null,
|
||||
IReadOnlyList<string>? ids = null,
|
||||
@@ -19,7 +26,7 @@ public sealed record ProjectModelsFilter(
|
||||
IReadOnlyList<string>? sourceApps = null
|
||||
);
|
||||
|
||||
public sealed record ProjectUpdateInput(
|
||||
public record ProjectUpdateInput(
|
||||
string id,
|
||||
string? name = null,
|
||||
string? description = null,
|
||||
@@ -27,6 +34,6 @@ public sealed record ProjectUpdateInput(
|
||||
ProjectVisibility? visibility = null
|
||||
);
|
||||
|
||||
public sealed record ProjectUpdateRoleInput(string userId, string projectId, string? role);
|
||||
public record ProjectUpdateRoleInput(string userId, string projectId, string? role);
|
||||
|
||||
public sealed record UserProjectsFilter(string search, IReadOnlyList<string>? onlyWithRoles = null);
|
||||
public record WorkspaceProjectsFilter(string? search, bool? withProjectRoleOnly);
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
namespace Speckle.Sdk.Api.GraphQL.Inputs;
|
||||
|
||||
public sealed record ViewerUpdateTrackingTarget(
|
||||
string projectId,
|
||||
string resourceIdString,
|
||||
bool? loadedVersionsOnly = null
|
||||
);
|
||||
public record ViewerUpdateTrackingTarget(string projectId, string resourceIdString, bool? loadedVersionsOnly = null);
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
namespace Speckle.Sdk.Api.GraphQL.Inputs;
|
||||
|
||||
public sealed record UserUpdateInput(
|
||||
string? avatar = null,
|
||||
string? bio = null,
|
||||
string? company = null,
|
||||
string? name = null
|
||||
public record UserUpdateInput(string? avatar = null, string? bio = null, string? company = null, string? name = null);
|
||||
|
||||
public record UserProjectsFilter(
|
||||
string? search = null,
|
||||
IReadOnlyList<string>? onlyWithRoles = null,
|
||||
string? workspaceId = null,
|
||||
bool? personalOnly = null,
|
||||
bool? includeImplicitAccess = null
|
||||
);
|
||||
|
||||
public record UserWorkspacesFilter(string? search);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
namespace Speckle.Sdk.Api.GraphQL.Inputs;
|
||||
|
||||
public sealed record UpdateVersionInput(string versionId, string projectId, string? message);
|
||||
public record UpdateVersionInput(string versionId, string projectId, string? message);
|
||||
|
||||
public sealed record MoveVersionsInput(string projectId, string targetModelName, IReadOnlyList<string> versionIds);
|
||||
public record MoveVersionsInput(string projectId, string targetModelName, IReadOnlyList<string> versionIds);
|
||||
|
||||
public sealed record DeleteVersionsInput(IReadOnlyList<string> versionIds, string projectId);
|
||||
public record DeleteVersionsInput(IReadOnlyList<string> versionIds, string projectId);
|
||||
|
||||
public sealed record CreateVersionInput(
|
||||
public record CreateVersionInput(
|
||||
string objectId,
|
||||
string modelId,
|
||||
string projectId,
|
||||
@@ -16,7 +16,7 @@ public sealed record CreateVersionInput(
|
||||
IReadOnlyList<string>? parents = null
|
||||
);
|
||||
|
||||
public sealed record MarkReceivedVersionInput(
|
||||
public record MarkReceivedVersionInput(
|
||||
string versionId,
|
||||
string projectId,
|
||||
string sourceApplication,
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Speckle.Sdk.Api.GraphQL.Models;
|
||||
|
||||
public sealed class FileImport
|
||||
{
|
||||
public string id { get; init; }
|
||||
public string projectId { get; init; }
|
||||
public string? convertedVersionId { get; init; }
|
||||
public string userId { get; init; }
|
||||
public int convertedStatus { get; init; }
|
||||
public string? convertedMessage { get; init; }
|
||||
public string? modelId { get; init; }
|
||||
public DateTime updatedAt { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Speckle.Sdk.Api.GraphQL.Models;
|
||||
|
||||
public sealed class FileUploadUrl
|
||||
{
|
||||
public Uri url { get; init; }
|
||||
public string fileId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Speckle.Sdk.Api.GraphQL.Models;
|
||||
|
||||
public sealed class PermissionCheckResult
|
||||
{
|
||||
public bool authorized { get; init; }
|
||||
public string code { get; init; }
|
||||
public string message { get; init; }
|
||||
|
||||
/// <exception cref="SpeckleException">Throws when <see cref="PermissionCheckResult.authorized"/> is <see langword="false"/></exception>
|
||||
public void EnsureAuthorised()
|
||||
{
|
||||
if (!authorized)
|
||||
{
|
||||
throw new WorkspacePermissionException(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,3 +26,8 @@ public sealed class ProjectWithTeam : Project
|
||||
public List<PendingStreamCollaborator> invitedTeam { get; init; }
|
||||
public List<ProjectCollaborator> team { get; init; }
|
||||
}
|
||||
|
||||
public sealed class ProjectWithPermissions : Project
|
||||
{
|
||||
public ProjectPermissionChecks permissions { get; init; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Speckle.Sdk.Api.GraphQL.Models;
|
||||
|
||||
public sealed class ProjectPermissionChecks
|
||||
{
|
||||
public PermissionCheckResult canCreateModel { get; init; }
|
||||
public PermissionCheckResult canDelete { get; init; }
|
||||
public PermissionCheckResult canLoad { get; init; }
|
||||
public PermissionCheckResult canPublish { get; init; }
|
||||
}
|
||||
@@ -9,10 +9,10 @@ namespace Speckle.Sdk.Api.GraphQL.Models.Responses;
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
internal record RequiredResponse<T>([property: JsonProperty(Required = Required.Always)] T data);
|
||||
public record RequiredResponse<T>([property: JsonProperty(Required = Required.Always)] T data);
|
||||
|
||||
/// <inheritdoc cref="RequiredResponse{T}"/>
|
||||
internal record NullableResponse<T>([property: JsonProperty(Required = Required.AllowNull)] T? data);
|
||||
public record NullableResponse<T>([property: JsonProperty(Required = Required.AllowNull)] T? data);
|
||||
|
||||
//TODO: replace with RequiredResponse{T}
|
||||
internal record ServerInfoResponse([property: JsonProperty(Required = Required.Always)] ServerInfo serverInfo);
|
||||
|
||||
@@ -16,12 +16,8 @@ public sealed class ServerInfo
|
||||
public string? version { get; init; }
|
||||
public string? description { get; init; }
|
||||
|
||||
/// <remarks>
|
||||
/// This field is not returned from the GQL API,
|
||||
/// it should be populated after construction from the response headers.
|
||||
/// see <see cref="Speckle.Sdk.Credentials.AccountManager"/>
|
||||
/// </remarks>
|
||||
public bool frontend2 { get; set; }
|
||||
[Obsolete("Don't use")]
|
||||
public bool frontend2 { get; set; } = true;
|
||||
|
||||
/// <remarks>
|
||||
/// This field is not returned from the GQL API,
|
||||
|
||||
@@ -7,6 +7,8 @@ public sealed class Version
|
||||
public string id { get; init; }
|
||||
public string? message { get; init; }
|
||||
public Uri previewUrl { get; init; }
|
||||
public string referencedObject { get; init; }
|
||||
|
||||
/// <remarks>May be <see langword="null"/> if workspaces version history limit has been exceeded</remarks>
|
||||
public string? referencedObject { get; init; }
|
||||
public string? sourceApplication { get; init; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
namespace Speckle.Sdk.Api.GraphQL.Models;
|
||||
|
||||
public class LimitedWorkspace
|
||||
{
|
||||
public string id { get; init; }
|
||||
public string name { get; init; }
|
||||
public string? role { get; init; }
|
||||
public string slug { get; init; }
|
||||
public string? logo { get; init; }
|
||||
public string? description { get; init; }
|
||||
}
|
||||
|
||||
public class Workspace : LimitedWorkspace
|
||||
{
|
||||
public DateTime createdAt { get; init; }
|
||||
public DateTime updatedAt { get; init; }
|
||||
public bool readOnly { get; init; }
|
||||
public WorkspacePermissionChecks permissions { get; init; }
|
||||
public WorkspaceCreationState? creationState { get; init; }
|
||||
}
|
||||
|
||||
public sealed class WorkspaceCreationState
|
||||
{
|
||||
public bool completed { get; init; }
|
||||
}
|
||||
|
||||
public sealed class WorkspacePermissionChecks
|
||||
{
|
||||
public PermissionCheckResult canCreateProject { get; init; }
|
||||
}
|
||||
@@ -85,6 +85,7 @@ public sealed class ActiveUserResource
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||
/// <exception cref="SpeckleException">The ActiveUser could not be found (e.g. the client is not authenticated)</exception>
|
||||
public async Task<ResourceCollection<Project>> GetProjects(
|
||||
int limit = ServerLimits.DEFAULT_PAGINATION_REQUEST,
|
||||
string? cursor = null,
|
||||
@@ -135,7 +136,7 @@ public sealed class ActiveUserResource
|
||||
|
||||
if (response.data is null)
|
||||
{
|
||||
throw new SpeckleGraphQLException("GraphQL response indicated that the ActiveUser could not be found");
|
||||
throw new SpeckleException("GraphQL response indicated that the ActiveUser could not be found");
|
||||
}
|
||||
|
||||
return response.data.data;
|
||||
@@ -199,8 +200,237 @@ public sealed class ActiveUserResource
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="GetProjectInvites"/>
|
||||
[Obsolete($"Renamed to {nameof(GetProjectInvites)}")]
|
||||
public async Task<List<PendingStreamCollaborator>> ProjectInvites(CancellationToken cancellationToken = default) =>
|
||||
await GetProjectInvites(cancellationToken).ConfigureAwait(false);
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||
/// <exception cref="SpeckleException">The ActiveUser could not be found (e.g. the client is not authenticated)</exception>
|
||||
public async Task<PermissionCheckResult> CanCreatePersonalProjects(CancellationToken cancellationToken = default)
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
query CanCreatePersonalProject {
|
||||
data:activeUser {
|
||||
data:permissions {
|
||||
data:canCreatePersonalProject {
|
||||
authorized
|
||||
code
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var request = new GraphQLRequest { Query = QUERY };
|
||||
|
||||
var response = await _client
|
||||
.ExecuteGraphQLRequest<NullableResponse<RequiredResponse<RequiredResponse<PermissionCheckResult>>?>>(
|
||||
request,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (response.data is null)
|
||||
{
|
||||
throw new SpeckleException("GraphQL response indicated that the ActiveUser could not be found");
|
||||
}
|
||||
|
||||
return response.data.data.data;
|
||||
}
|
||||
|
||||
/// <remarks>This feature is only available on Workspace enabled servers (e.g. app.speckle.systems)</remarks>
|
||||
/// <param name="limit"></param>
|
||||
/// <param name="cursor"></param>
|
||||
/// <param name="filter"></param>
|
||||
/// <returns></returns>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||
/// <exception cref="SpeckleException">The ActiveUser could not be found (e.g. the client is not authenticated)</exception>
|
||||
public async Task<ResourceCollection<Workspace>> GetWorkspaces(
|
||||
int limit = ServerLimits.DEFAULT_PAGINATION_REQUEST,
|
||||
string? cursor = null,
|
||||
UserWorkspacesFilter? filter = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
query ActiveUser($limit: Int!, $cursor: String, $filter: UserWorkspacesFilter) {
|
||||
data:activeUser {
|
||||
data:workspaces(limit: $limit, cursor: $cursor, filter: $filter) {
|
||||
cursor
|
||||
totalCount
|
||||
items {
|
||||
id
|
||||
name
|
||||
role
|
||||
slug
|
||||
logo
|
||||
createdAt
|
||||
updatedAt
|
||||
readOnly
|
||||
description
|
||||
creationState
|
||||
{
|
||||
completed
|
||||
}
|
||||
permissions {
|
||||
canCreateProject {
|
||||
authorized
|
||||
code
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var request = new GraphQLRequest
|
||||
{
|
||||
Query = QUERY,
|
||||
Variables = new
|
||||
{
|
||||
limit,
|
||||
cursor,
|
||||
filter,
|
||||
},
|
||||
};
|
||||
|
||||
var response = await _client
|
||||
.ExecuteGraphQLRequest<NullableResponse<RequiredResponse<ResourceCollection<Workspace>>?>>(
|
||||
request,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (response.data is null)
|
||||
{
|
||||
throw new SpeckleException("GraphQL response indicated that the ActiveUser could not be found");
|
||||
}
|
||||
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns>The active (last selected) workspace</returns>
|
||||
/// <remarks>note this returns a <see cref="LimitedWorkspace"/>, because it may be a workspace the user is not a member of</remarks>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||
/// <exception cref="SpeckleException">The ActiveUser could not be found (e.g. the client is not authenticated)</exception>
|
||||
public async Task<LimitedWorkspace?> GetActiveWorkspace(CancellationToken cancellationToken = default)
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
query ActiveUser {
|
||||
data:activeUser {
|
||||
data:activeWorkspace {
|
||||
id
|
||||
name
|
||||
role
|
||||
slug
|
||||
logo
|
||||
description
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var request = new GraphQLRequest { Query = QUERY };
|
||||
|
||||
var response = await _client
|
||||
.ExecuteGraphQLRequest<NullableResponse<NullableResponse<LimitedWorkspace?>?>>(request, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (response.data is null)
|
||||
{
|
||||
throw new SpeckleException("GraphQL response indicated that the ActiveUser could not be found");
|
||||
}
|
||||
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
/// <param name="limit">Max number of projects to fetch</param>
|
||||
/// <param name="cursor">Optional cursor for pagination</param>
|
||||
/// <param name="filter">Optional filter</param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||
/// <exception cref="SpeckleException">The ActiveUser could not be found (e.g. the client is not authenticated)</exception>
|
||||
public async Task<ResourceCollection<ProjectWithPermissions>> GetProjectsWithPermissions(
|
||||
int limit = ServerLimits.DEFAULT_PAGINATION_REQUEST,
|
||||
string? cursor = null,
|
||||
UserProjectsFilter? filter = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
query User($limit: Int!, $cursor: String, $filter: UserProjectsFilter) {
|
||||
data: activeUser {
|
||||
data: projects(limit: $limit, cursor: $cursor, filter: $filter) {
|
||||
totalCount
|
||||
cursor
|
||||
items {
|
||||
id
|
||||
name
|
||||
description
|
||||
visibility
|
||||
allowPublicComments
|
||||
role
|
||||
createdAt
|
||||
updatedAt
|
||||
sourceApps
|
||||
workspaceId
|
||||
permissions {
|
||||
canCreateModel {
|
||||
code
|
||||
authorized
|
||||
message
|
||||
}
|
||||
canDelete {
|
||||
code
|
||||
authorized
|
||||
message
|
||||
}
|
||||
canLoad {
|
||||
code
|
||||
authorized
|
||||
message
|
||||
}
|
||||
canPublish {
|
||||
code
|
||||
authorized
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
var request = new GraphQLRequest
|
||||
{
|
||||
Query = QUERY,
|
||||
Variables = new
|
||||
{
|
||||
limit,
|
||||
cursor,
|
||||
filter,
|
||||
},
|
||||
};
|
||||
|
||||
var response = await _client
|
||||
.ExecuteGraphQLRequest<NullableResponse<RequiredResponse<ResourceCollection<ProjectWithPermissions>>?>>(
|
||||
request,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (response.data is null)
|
||||
{
|
||||
throw new SpeckleException("GraphQL response indicated that the ActiveUser could not be found");
|
||||
}
|
||||
|
||||
return response.data.data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
using System.Diagnostics;
|
||||
using GraphQL;
|
||||
using Speckle.Sdk.Api.Blob;
|
||||
using Speckle.Sdk.Api.GraphQL.Inputs;
|
||||
using Speckle.Sdk.Api.GraphQL.Models;
|
||||
using Speckle.Sdk.Api.GraphQL.Models.Responses;
|
||||
using Speckle.Sdk.Transports;
|
||||
|
||||
namespace Speckle.Sdk.Api.GraphQL.Resources;
|
||||
|
||||
public sealed class FileImportResource : IDisposable
|
||||
{
|
||||
private readonly ISpeckleGraphQLClient _client;
|
||||
private readonly IBlobApi _blobApi;
|
||||
|
||||
internal FileImportResource(ISpeckleGraphQLClient client, IBlobApi blobApi)
|
||||
{
|
||||
_client = client;
|
||||
_blobApi = blobApi;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is mostly an internal api, that marks a file import job finished.
|
||||
/// </summary>
|
||||
/// <param name="input">Either <see cref="FileImportSuccessInput"/> or <see cref="FileImportErrorInput"/></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||
/// <remarks>
|
||||
/// Only use this if you are writing a file importer, that is responsible for
|
||||
/// processing file import jobs.
|
||||
/// Only works on servers version >=2.25.8
|
||||
/// </remarks>
|
||||
public async Task<bool> FinishFileImportJob(FileImportInputBase input, CancellationToken cancellationToken)
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
mutation FinishFileImport($input: FinishFileImportInput!) {
|
||||
data:fileUploadMutations {
|
||||
data:finishFileImport(input: $input)
|
||||
}
|
||||
}
|
||||
""";
|
||||
var request = new GraphQLRequest { Query = QUERY, Variables = new { input } };
|
||||
|
||||
var response = await _client
|
||||
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<bool>>>(request, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||
/// <remarks>Only works on servers version >=2.25.8</remarks>
|
||||
public async Task<FileImport> StartFileImportJob(
|
||||
StartFileImportInput input,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
mutation StartFileImport($input: StartFileImportInput!) {
|
||||
data:fileUploadMutations {
|
||||
data:startFileImport(input: $input) {
|
||||
id
|
||||
projectId
|
||||
convertedVersionId
|
||||
userId
|
||||
convertedStatus
|
||||
convertedMessage
|
||||
modelId
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
var request = new GraphQLRequest { Query = QUERY, Variables = new { input } };
|
||||
|
||||
var response = await _client
|
||||
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<FileImport>>>(request, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a file upload url from the Speckle server.
|
||||
/// This method asks the server to create a pre-signed S3 url,
|
||||
/// which can be used as a short term authenticated route, to put a file to the server.
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||
/// <remarks>Only works on servers version >=2.25.8</remarks>
|
||||
public async Task<FileUploadUrl> GenerateUploadUrl(
|
||||
GenerateFileUploadUrlInput input,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
mutation GenerateUploadUrl($input: GenerateFileUploadUrlInput!) {
|
||||
data:fileUploadMutations {
|
||||
data:generateUploadUrl(input: $input) {
|
||||
fileId
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
var request = new GraphQLRequest { Query = QUERY, Variables = new { input } };
|
||||
|
||||
var response = await _client
|
||||
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<FileUploadUrl>>>(request, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Blob.BlobApi.UploadFile"/>
|
||||
[DebuggerStepThrough]
|
||||
public Task<string> UploadFile(
|
||||
string filePath,
|
||||
Uri url,
|
||||
IProgress<ProgressArgs>? progress = null,
|
||||
CancellationToken cancellationToken = default
|
||||
) => _blobApi.UploadFile(filePath, url, progress, cancellationToken);
|
||||
|
||||
/// <inheritdoc cref="Blob.BlobApi.DownloadBlob"/>
|
||||
[DebuggerStepThrough]
|
||||
public Task DownloadFile(
|
||||
string projectId,
|
||||
string fileId,
|
||||
string targetFile,
|
||||
IProgress<ProgressArgs>? progress = null,
|
||||
CancellationToken cancellationToken = default
|
||||
) => _blobApi.DownloadBlob(projectId, fileId, targetFile, progress, cancellationToken);
|
||||
|
||||
/// <param name="projectId"></param>
|
||||
/// <param name="modelId"></param>
|
||||
/// <param name="limit"></param>
|
||||
/// <param name="cursor"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||
/// <remarks>Only works on servers version >=2.25.8</remarks>
|
||||
public async Task<ResourceCollection<FileImport>> GetModelFileImportJobs(
|
||||
string projectId,
|
||||
string modelId,
|
||||
int limit = ServerLimits.DEFAULT_PAGINATION_REQUEST,
|
||||
string? cursor = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
query ModelFileImportJobs(
|
||||
$projectId: String!,
|
||||
$modelId: String!,
|
||||
$input: GetModelUploadsInput
|
||||
) {
|
||||
data:project(id: $projectId) {
|
||||
data:model(id: $modelId) {
|
||||
data:uploads(input: $input) {
|
||||
totalCount
|
||||
cursor
|
||||
items {
|
||||
id
|
||||
projectId
|
||||
convertedVersionId
|
||||
userId
|
||||
convertedStatus
|
||||
convertedMessage
|
||||
modelId
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
var request = new GraphQLRequest
|
||||
{
|
||||
Query = QUERY,
|
||||
Variables = new
|
||||
{
|
||||
projectId,
|
||||
modelId,
|
||||
input = new { limit, cursor },
|
||||
},
|
||||
};
|
||||
|
||||
var response = await _client
|
||||
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<RequiredResponse<ResourceCollection<FileImport>>>>>(
|
||||
request,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return response.data.data.data;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_blobApi.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,52 @@ public sealed class ProjectResource
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/// <param name="projectId"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||
public async Task<ProjectPermissionChecks> GetPermissions(
|
||||
string projectId,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
query Project($projectId: String!) {
|
||||
data:project(id: $projectId) {
|
||||
data:permissions {
|
||||
canCreateModel {
|
||||
authorized
|
||||
code
|
||||
message
|
||||
}
|
||||
canDelete {
|
||||
authorized
|
||||
code
|
||||
message
|
||||
}
|
||||
canLoad {
|
||||
authorized
|
||||
code
|
||||
message
|
||||
}
|
||||
canPublish {
|
||||
authorized
|
||||
code
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
GraphQLRequest request = new() { Query = QUERY, Variables = new { projectId } };
|
||||
|
||||
var response = await _client
|
||||
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<ProjectPermissionChecks>>>(request, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
/// <param name="projectId"></param>
|
||||
/// <param name="modelsLimit">Max number of models to fetch</param>
|
||||
/// <param name="modelsCursor">Optional cursor for pagination</param>
|
||||
@@ -186,6 +232,10 @@ public sealed class ProjectResource
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a non-workspace project (aka Personal Project)<br/>
|
||||
/// See <see cref="ActiveUserResource.CanCreatePersonalProjects"/> to see if the user has permission
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
@@ -219,6 +269,49 @@ public sealed class ProjectResource
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a workspace project.<br/>
|
||||
/// This feature is only supported on Workspace Enabled Servers (e.g. app.speckle.systems)
|
||||
/// See <see cref="ActiveUserResource.CanCreatePersonalProjects"/> to see if the user has permission
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||
public async Task<Project> CreateInWorkspace(
|
||||
WorkspaceProjectCreateInput input,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
mutation WorkspaceProjectCreate($input: WorkspaceProjectCreateInput!) {
|
||||
data:workspaceMutations {
|
||||
data:projects {
|
||||
data:create(input: $input) {
|
||||
id
|
||||
name
|
||||
description
|
||||
visibility
|
||||
allowPublicComments
|
||||
role
|
||||
createdAt
|
||||
updatedAt
|
||||
sourceApps
|
||||
workspaceId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
GraphQLRequest request = new() { Query = QUERY, Variables = new { input } };
|
||||
|
||||
var response = await _client
|
||||
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<RequiredResponse<Project>>>>(request, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
return response.data.data.data;
|
||||
}
|
||||
|
||||
/// <param name="input"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
using GraphQL;
|
||||
using Speckle.Sdk.Api.GraphQL.Models.Responses;
|
||||
|
||||
namespace Speckle.Sdk.Api.GraphQL.Resources;
|
||||
|
||||
public sealed class ServerResource
|
||||
{
|
||||
private readonly ISpeckleGraphQLClient _client;
|
||||
|
||||
internal ServerResource(ISpeckleGraphQLClient client)
|
||||
{
|
||||
_client = client;
|
||||
}
|
||||
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns><see langword="null"/> if server is workspaces enabled</returns>
|
||||
/// <returns>the requested user, or null if <see cref="Client"/> was initialised with an unauthenticated account</returns>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||
public async Task<bool> IsWorkspaceEnabled(CancellationToken cancellationToken = default)
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
query {
|
||||
data:serverInfo {
|
||||
data:workspaces {
|
||||
data:workspacesEnabled
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
var request = new GraphQLRequest { Query = QUERY };
|
||||
|
||||
var response = await _client
|
||||
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<RequiredResponse<bool>>>>(request, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return response.data.data.data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
using GraphQL;
|
||||
using Speckle.Sdk.Api.GraphQL.Inputs;
|
||||
using Speckle.Sdk.Api.GraphQL.Models;
|
||||
using Speckle.Sdk.Api.GraphQL.Models.Responses;
|
||||
|
||||
namespace Speckle.Sdk.Api.GraphQL.Resources;
|
||||
|
||||
public sealed class WorkspaceResource
|
||||
{
|
||||
private readonly ISpeckleGraphQLClient _client;
|
||||
|
||||
internal WorkspaceResource(ISpeckleGraphQLClient client)
|
||||
{
|
||||
_client = client;
|
||||
}
|
||||
|
||||
/// <param name="workspaceId"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||
public async Task<Workspace> Get(string workspaceId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
query WorkspaceGet($workspaceId: String!) {
|
||||
data:workspace(id: $workspaceId) {
|
||||
id
|
||||
name
|
||||
role
|
||||
slug
|
||||
logo
|
||||
createdAt
|
||||
updatedAt
|
||||
readOnly
|
||||
description
|
||||
creationState
|
||||
{
|
||||
completed
|
||||
}
|
||||
permissions {
|
||||
canCreateProject {
|
||||
authorized
|
||||
code
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
var request = new GraphQLRequest { Query = QUERY, Variables = new { workspaceId } };
|
||||
|
||||
var response = await _client
|
||||
.ExecuteGraphQLRequest<RequiredResponse<Workspace>>(request, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/// <param name="workspaceId"></param>
|
||||
/// <param name="limit">Max number of projects to fetch</param>
|
||||
/// <param name="cursor">Optional cursor for pagination</param>
|
||||
/// <param name="filter"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||
/// <see cref="Get"/>
|
||||
public async Task<ResourceCollection<Project>> GetProjects(
|
||||
string workspaceId,
|
||||
int limit = ServerLimits.DEFAULT_PAGINATION_REQUEST,
|
||||
string? cursor = null,
|
||||
WorkspaceProjectsFilter? filter = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
query Workspace($workspaceId: String!, $limit: Int!, $cursor: String, $filter: WorkspaceProjectsFilter) {
|
||||
data:workspace(id: $workspaceId) {
|
||||
data:projects(limit: $limit, cursor: $cursor, filter: $filter) {
|
||||
cursor
|
||||
items {
|
||||
allowPublicComments
|
||||
createdAt
|
||||
description
|
||||
id
|
||||
name
|
||||
role
|
||||
sourceApps
|
||||
updatedAt
|
||||
visibility
|
||||
workspaceId
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var request = new GraphQLRequest
|
||||
{
|
||||
Query = QUERY,
|
||||
Variables = new
|
||||
{
|
||||
workspaceId,
|
||||
limit,
|
||||
cursor,
|
||||
filter,
|
||||
},
|
||||
};
|
||||
|
||||
var response = await _client
|
||||
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<ResourceCollection<Project>>>>(
|
||||
request,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return response.data.data;
|
||||
}
|
||||
}
|
||||
@@ -26,9 +26,11 @@ public partial class Operations
|
||||
)
|
||||
{
|
||||
using var receiveActivity = activityFactory.Start("Operations.Receive");
|
||||
receiveActivity?.SetTag("speckle.url", url);
|
||||
receiveActivity?.SetTag("speckle.projectId", streamId);
|
||||
receiveActivity?.SetTag("speckle.objectId", objectId);
|
||||
metricsFactory.CreateCounter<long>("Receive").Add(1);
|
||||
|
||||
receiveActivity?.SetTag("objectId", objectId);
|
||||
var process = deserializeProcessFactory.CreateDeserializeProcess(
|
||||
url,
|
||||
streamId,
|
||||
|
||||
@@ -29,6 +29,8 @@ public partial class Operations
|
||||
)
|
||||
{
|
||||
using var receiveActivity = activityFactory.Start("Operations.Send");
|
||||
receiveActivity?.SetTag("speckle.url", url);
|
||||
receiveActivity?.SetTag("speckle.projectId", streamId);
|
||||
metricsFactory.CreateCounter<long>("Send").Add(1);
|
||||
|
||||
var process = serializeProcessFactory.CreateSerializeProcess(
|
||||
@@ -45,6 +47,11 @@ public partial class Operations
|
||||
receiveActivity?.SetStatus(SdkActivityStatusCode.Ok);
|
||||
return results;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
//this is handled by the caller
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
receiveActivity?.SetStatus(SdkActivityStatusCode.Error);
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
using Speckle.InterfaceGenerator;
|
||||
|
||||
namespace Speckle.Sdk.Caching;
|
||||
|
||||
/// <summary>
|
||||
/// This mocks away the file system operations for testing purposes.
|
||||
/// </summary>
|
||||
[GenerateAutoInterface]
|
||||
public class FileSystem : IFileSystem
|
||||
{
|
||||
public bool DirectoryExists(string path) => Directory.Exists(path);
|
||||
|
||||
public void CreateDirectory(string path) => Directory.CreateDirectory(path);
|
||||
|
||||
public IEnumerable<string> EnumerateFiles(string path) => Directory.EnumerateFiles(path);
|
||||
|
||||
public void DeleteFile(string path) => File.Delete(path);
|
||||
|
||||
public long GetFileSize(string path) => new FileInfo(path).Length;
|
||||
|
||||
public string Combine(params string[] paths) => Path.Combine(paths);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Speckle.InterfaceGenerator;
|
||||
using Speckle.Sdk.Logging;
|
||||
using Speckle.Sdk.Transports;
|
||||
|
||||
namespace Speckle.Sdk.Caching;
|
||||
|
||||
/// <summary>
|
||||
/// This class manages the cache for model data, providing methods to get stream paths, clear the cache, and calculate cache size.
|
||||
/// </summary>
|
||||
[GenerateAutoInterface]
|
||||
public class ModelCacheManager(ILogger<ModelCacheManager> logger, IFileSystem fileSystem) : IModelCacheManager
|
||||
{
|
||||
private const string DATA_FOLDER = "Projects";
|
||||
private static readonly string s_basePath = SpecklePathProvider.UserSpeckleFolderPath;
|
||||
|
||||
private static string CacheFolder => Path.Combine(s_basePath, DATA_FOLDER);
|
||||
|
||||
public string GetStreamPath(string streamId) => GetDbPath(streamId);
|
||||
|
||||
public static string GetDbPath(string streamId)
|
||||
{
|
||||
var db = Path.Combine(CacheFolder, $"{streamId}.db");
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(CacheFolder); //ensure dir is there
|
||||
return db;
|
||||
}
|
||||
catch (Exception ex)
|
||||
when (ex is ArgumentException or IOException or UnauthorizedAccessException or NotSupportedException)
|
||||
{
|
||||
throw new TransportException($"Path was invalid or could not be created {db}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearCache()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!fileSystem.DirectoryExists(CacheFolder))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var db in fileSystem.EnumerateFiles(CacheFolder))
|
||||
{
|
||||
try
|
||||
{
|
||||
fileSystem.DeleteFile(db);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or NotSupportedException)
|
||||
{
|
||||
logger.LogWarning(ex, "Failed to delete cache file {filePath}", db);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
when (ex is ArgumentException or IOException or UnauthorizedAccessException or NotSupportedException)
|
||||
{
|
||||
throw new TransportException($"Cache folder could not be cleared: {CacheFolder}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public long GetCacheSize()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!fileSystem.DirectoryExists(CacheFolder))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
long size = 0;
|
||||
foreach (var file in fileSystem.EnumerateFiles(CacheFolder))
|
||||
{
|
||||
try
|
||||
{
|
||||
size += fileSystem.GetFileSize(file);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or NotSupportedException)
|
||||
{
|
||||
logger.LogWarning(ex, "Failed to get size for cache file {a}", file);
|
||||
}
|
||||
}
|
||||
return size;
|
||||
}
|
||||
catch (Exception ex)
|
||||
when (ex is ArgumentException or IOException or UnauthorizedAccessException or NotSupportedException)
|
||||
{
|
||||
throw new TransportException($"Cache folder size could not be determined: {CacheFolder}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
namespace Speckle.Sdk.Common;
|
||||
|
||||
// MD5 implementation in pure C# (public domain / no dependencies)
|
||||
// Not for cryptographic purposes
|
||||
// Using this instead of changing ID generation but avoiding built in MD5 for FIPS compliance
|
||||
|
||||
public static class Md5
|
||||
{
|
||||
// Standard initial values
|
||||
private static readonly uint[] T =
|
||||
[
|
||||
0xd76aa478,
|
||||
0xe8c7b756,
|
||||
0x242070db,
|
||||
0xc1bdceee,
|
||||
0xf57c0faf,
|
||||
0x4787c62a,
|
||||
0xa8304613,
|
||||
0xfd469501,
|
||||
0x698098d8,
|
||||
0x8b44f7af,
|
||||
0xffff5bb1,
|
||||
0x895cd7be,
|
||||
0x6b901122,
|
||||
0xfd987193,
|
||||
0xa679438e,
|
||||
0x49b40821,
|
||||
0xf61e2562,
|
||||
0xc040b340,
|
||||
0x265e5a51,
|
||||
0xe9b6c7aa,
|
||||
0xd62f105d,
|
||||
0x02441453,
|
||||
0xd8a1e681,
|
||||
0xe7d3fbc8,
|
||||
0x21e1cde6,
|
||||
0xc33707d6,
|
||||
0xf4d50d87,
|
||||
0x455a14ed,
|
||||
0xa9e3e905,
|
||||
0xfcefa3f8,
|
||||
0x676f02d9,
|
||||
0x8d2a4c8a,
|
||||
0xfffa3942,
|
||||
0x8771f681,
|
||||
0x6d9d6122,
|
||||
0xfde5380c,
|
||||
0xa4beea44,
|
||||
0x4bdecfa9,
|
||||
0xf6bb4b60,
|
||||
0xbebfbc70,
|
||||
0x289b7ec6,
|
||||
0xeaa127fa,
|
||||
0xd4ef3085,
|
||||
0x04881d05,
|
||||
0xd9d4d039,
|
||||
0xe6db99e5,
|
||||
0x1fa27cf8,
|
||||
0xc4ac5665,
|
||||
0xf4292244,
|
||||
0x432aff97,
|
||||
0xab9423a7,
|
||||
0xfc93a039,
|
||||
0x655b59c3,
|
||||
0x8f0ccc92,
|
||||
0xffeff47d,
|
||||
0x85845dd1,
|
||||
0x6fa87e4f,
|
||||
0xfe2ce6e0,
|
||||
0xa3014314,
|
||||
0x4e0811a1,
|
||||
0xf7537e82,
|
||||
0xbd3af235,
|
||||
0x2ad7d2bb,
|
||||
0xeb86d391,
|
||||
];
|
||||
|
||||
public static byte[] ComputeHash(byte[] input)
|
||||
{
|
||||
// Pad input
|
||||
int origLenBits = input.Length * 8;
|
||||
int padLen = (56 - (input.Length + 1) % 64 + 64) % 64;
|
||||
byte[] padded = new byte[input.Length + 1 + padLen + 8];
|
||||
Array.Copy(input, padded, input.Length);
|
||||
padded[input.Length] = 0x80;
|
||||
BitConverter.GetBytes((long)origLenBits).CopyTo(padded, padded.Length - 8);
|
||||
|
||||
// Initialize MD5 buffer
|
||||
uint a = 0x67452301;
|
||||
uint b = 0xefcdab89;
|
||||
uint c = 0x98badcfe;
|
||||
uint d = 0x10325476;
|
||||
|
||||
for (int i = 0; i < padded.Length / 64; i++)
|
||||
{
|
||||
uint[] M = new uint[16];
|
||||
for (int j = 0; j < 16; j++)
|
||||
{
|
||||
M[j] = BitConverter.ToUInt32(padded, (i * 64) + j * 4);
|
||||
}
|
||||
|
||||
uint AA = a,
|
||||
BB = b,
|
||||
CC = c,
|
||||
DD = d;
|
||||
for (int j = 0; j < 64; j++)
|
||||
{
|
||||
uint f,
|
||||
g;
|
||||
if (j < 16)
|
||||
{
|
||||
f = (b & c) | (~b & d);
|
||||
g = (uint)j;
|
||||
}
|
||||
else if (j < 32)
|
||||
{
|
||||
f = (d & b) | (~d & c);
|
||||
g = (uint)((5 * j + 1) % 16);
|
||||
}
|
||||
else if (j < 48)
|
||||
{
|
||||
f = b ^ c ^ d;
|
||||
g = (uint)((3 * j + 5) % 16);
|
||||
}
|
||||
else
|
||||
{
|
||||
f = c ^ (b | ~d);
|
||||
g = (uint)((7 * j) % 16);
|
||||
}
|
||||
|
||||
uint temp = d;
|
||||
d = c;
|
||||
c = b;
|
||||
b += LeftRotate(a + f + T[j] + M[g], S(j));
|
||||
a = temp;
|
||||
}
|
||||
a += AA;
|
||||
b += BB;
|
||||
c += CC;
|
||||
d += DD;
|
||||
}
|
||||
|
||||
byte[] output = new byte[16];
|
||||
Array.Copy(BitConverter.GetBytes(a), 0, output, 0, 4);
|
||||
Array.Copy(BitConverter.GetBytes(b), 0, output, 4, 4);
|
||||
Array.Copy(BitConverter.GetBytes(c), 0, output, 8, 4);
|
||||
Array.Copy(BitConverter.GetBytes(d), 0, output, 12, 4);
|
||||
return output;
|
||||
}
|
||||
|
||||
private static int S(int i)
|
||||
{
|
||||
int[] s = { 7, 12, 17, 22, 5, 9, 14, 20, 4, 11, 16, 23, 6, 10, 15, 21 };
|
||||
return s[(i / 16) * 4 + (i % 4)];
|
||||
}
|
||||
|
||||
private static uint LeftRotate(uint x, int c) => (x << c) | (x >> (32 - c));
|
||||
|
||||
// Convenience method to get hex string
|
||||
public static string GetString(string input)
|
||||
{
|
||||
var hash = ComputeHash(System.Text.Encoding.UTF8.GetBytes(input));
|
||||
return BitConverter.ToString(hash).Replace("-", "");
|
||||
}
|
||||
}
|
||||
@@ -95,4 +95,22 @@ public static class NotNullExtensions
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
public static string NotNullOrWhiteSpace(
|
||||
[NotNull] this string? value,
|
||||
[CallerArgumentExpression(nameof(value))] string? paramName = null
|
||||
)
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
throw new ArgumentNullException(paramName ?? "Value is null");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
throw new ArgumentException("Value cannot be empty or whitespace.", paramName);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,17 +6,17 @@ using System.Text;
|
||||
using System.Runtime.InteropServices;
|
||||
#endif
|
||||
|
||||
namespace Speckle.Sdk.Helpers;
|
||||
namespace Speckle.Sdk.Common;
|
||||
|
||||
public static class Crypt
|
||||
public static class Sha256
|
||||
{
|
||||
#if NET6_0_OR_GREATER
|
||||
/// <param name="input">the value to hash</param>
|
||||
/// <param name="format"><c>"x2"</c> for lower case, <c>"X2"</c> for uppercase.</param>
|
||||
/// <param name="length">Desired length of the returned string. Must be 2 ≤ Length ≤ 64, and must be a multiple of 2</param>
|
||||
/// <returns><inheritdoc cref="Sha256(string, string?, int)"/></returns>
|
||||
/// <returns><inheritdoc cref="GetString(string, string?, int)"/></returns>
|
||||
[Pure]
|
||||
public static string Sha256(
|
||||
public static string GetString(
|
||||
ReadOnlySpan<char> input,
|
||||
[StringSyntax(StringSyntaxAttribute.NumericFormat)] string? format = "x2",
|
||||
int length = SHA256.HashSizeInBytes * sizeof(char)
|
||||
@@ -45,7 +45,7 @@ public static class Crypt
|
||||
/// <exception cref="FormatException"><paramref name="format"/> is not a recognised numeric format</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException"><inheritdoc cref="StringBuilder.ToString(int, int)"/></exception>
|
||||
[Pure]
|
||||
public static string Sha256(
|
||||
public static string GetString(
|
||||
string input,
|
||||
[StringSyntax(StringSyntaxAttribute.NumericFormat)] string? format = "x2",
|
||||
int length = 64
|
||||
@@ -67,30 +67,4 @@ public static class Crypt
|
||||
|
||||
return sb.ToString(0, length);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Sha256(string, string?, int)"/>
|
||||
/// <remarks>MD5 is a broken cryptographic algorithm and should be used subject to review see CA5351</remarks>
|
||||
[Pure]
|
||||
[SuppressMessage("Security", "CA5351:Do Not Use Broken Cryptographic Algorithms")]
|
||||
public static string Md5(
|
||||
string input,
|
||||
[StringSyntax(StringSyntaxAttribute.NumericFormat)] string? format = "x2",
|
||||
int length = 32
|
||||
)
|
||||
{
|
||||
byte[] inputBytes = Encoding.ASCII.GetBytes(input.ToLowerInvariant());
|
||||
#if NETSTANDARD2_0
|
||||
using MD5 md5 = MD5.Create();
|
||||
byte[] hashBytes = md5.ComputeHash(inputBytes);
|
||||
#else
|
||||
byte[] hashBytes = MD5.HashData(inputBytes);
|
||||
#endif
|
||||
StringBuilder sb = new(32);
|
||||
for (int i = 0; i < hashBytes.Length; i++)
|
||||
{
|
||||
sb.Append(hashBytes[i].ToString(format));
|
||||
}
|
||||
|
||||
return sb.ToString(0, length);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using Speckle.Sdk.Api.GraphQL.Models;
|
||||
using Speckle.Sdk.Helpers;
|
||||
using Speckle.Sdk.Common;
|
||||
|
||||
namespace Speckle.Sdk.Credentials;
|
||||
|
||||
@@ -25,7 +25,7 @@ public class Account : IEquatable<Account>
|
||||
throw new InvalidOperationException("Incomplete account info: cannot generate id.");
|
||||
}
|
||||
|
||||
_id = Crypt.Md5(userInfo.email + serverInfo.url, "X2");
|
||||
_id = Md5.GetString(userInfo.email + serverInfo.url).ToUpperInvariant();
|
||||
}
|
||||
return _id;
|
||||
}
|
||||
@@ -34,7 +34,7 @@ public class Account : IEquatable<Account>
|
||||
|
||||
public string token { get; set; }
|
||||
|
||||
public string refreshToken { get; set; }
|
||||
public string? refreshToken { get; set; }
|
||||
|
||||
public bool isDefault { get; set; }
|
||||
public bool isOnline { get; set; } = true;
|
||||
@@ -62,13 +62,13 @@ public class Account : IEquatable<Account>
|
||||
public string GetHashedEmail()
|
||||
{
|
||||
string email = userInfo?.email ?? "unknown";
|
||||
return "@" + Crypt.Md5(email, "X2");
|
||||
return "@" + Md5.GetString(email).ToUpperInvariant();
|
||||
}
|
||||
|
||||
public string GetHashedServer()
|
||||
{
|
||||
string url = serverInfo?.url ?? AccountManager.DEFAULT_SERVER_URL;
|
||||
return Crypt.Md5(CleanURL(url), "X2");
|
||||
return Md5.GetString(CleanURL(url)).ToUpperInvariant();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
using System.Diagnostics;
|
||||
using GraphQL;
|
||||
using Speckle.InterfaceGenerator;
|
||||
using Speckle.Sdk.Api.GraphQL;
|
||||
using Speckle.Sdk.Api.GraphQL.Models;
|
||||
|
||||
namespace Speckle.Sdk.Credentials;
|
||||
|
||||
public partial interface IAccountFactory
|
||||
{
|
||||
internal Task<ActiveUserServerInfoResponse> GetUserServerInfo(Uri serverUrl, string? authToken, CancellationToken ct);
|
||||
}
|
||||
|
||||
[GenerateAutoInterface]
|
||||
public sealed class AccountFactory(IGraphQLClientFactory graphQLClientFactory) : IAccountFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the User and Server info required for <see cref="Account"/> object creation
|
||||
/// </summary>
|
||||
/// <param name="serverUrl"></param>
|
||||
/// <param name="authToken">If <see lang="null"/>, the server will respond with a <see lang="null"/> <see cref="UserInfo"/></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <inheritdoc cref="GetUserServerInfoInternal"/>
|
||||
[DebuggerStepThrough]
|
||||
async Task<ActiveUserServerInfoResponse> IAccountFactory.GetUserServerInfo(
|
||||
Uri serverUrl,
|
||||
string? authToken,
|
||||
CancellationToken cancellationToken
|
||||
) => await GetUserServerInfoInternal(serverUrl, authToken, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
/// <exception cref="SpeckleException">Server could not find user info given the speckleToken, suggests expired or non-existent user</exception>
|
||||
/// <inheritdoc cref="Speckle.Sdk.Api.GraphQL.GraphQLErrorHandler.EnsureGraphQLSuccess(IReadOnlyCollection{GraphQLError}?)"/>
|
||||
private async Task<ActiveUserServerInfoResponse> GetUserServerInfoInternal(
|
||||
Uri serverUrl,
|
||||
string? authToken,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
using var client = graphQLClientFactory.CreateGraphQLClient(serverUrl, authToken);
|
||||
|
||||
//language=graphql
|
||||
const string QUERY_STRING = """
|
||||
query {
|
||||
activeUser {
|
||||
id
|
||||
name
|
||||
email
|
||||
company
|
||||
avatar
|
||||
}
|
||||
serverInfo {
|
||||
name
|
||||
company
|
||||
description
|
||||
version
|
||||
migration {
|
||||
movedFrom
|
||||
movedTo
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var request = new GraphQLRequest { Query = QUERY_STRING };
|
||||
|
||||
var response = await client
|
||||
.SendQueryAsync<ActiveUserServerInfoResponse>(request, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
response.EnsureGraphQLSuccess();
|
||||
|
||||
ServerInfo serverInfo = response.Data.serverInfo;
|
||||
serverInfo.url = serverUrl.ToString().TrimEnd('/');
|
||||
|
||||
return response.Data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="Account"/> object by fetching the required server/user information from the specified server
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This does not create a new account on the server, nor does it read/write from the SQLite DB. For that see <see cref="AccountManager"/>.
|
||||
/// This is just a Factory pattern around an <see cref="Account"/> object
|
||||
/// </remarks>
|
||||
/// <exception cref="SpeckleException">Server could not find user info given the speckleToken, suggests expired or non-existent user</exception>
|
||||
/// <inheritdoc cref="Speckle.Sdk.Api.GraphQL.GraphQLErrorHandler.EnsureGraphQLSuccess(IReadOnlyCollection{GraphQLError}?)"/>
|
||||
public async Task<Account> CreateAccount(
|
||||
Uri serverUrl,
|
||||
string speckleToken,
|
||||
string? refreshToken = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var res = await GetUserServerInfoInternal(serverUrl, speckleToken, cancellationToken).ConfigureAwait(false);
|
||||
if (res.activeUser == null)
|
||||
{
|
||||
throw new SpeckleException("GraphQL response indicated that the ActiveUser could not be found");
|
||||
}
|
||||
return new Account()
|
||||
{
|
||||
token = speckleToken,
|
||||
refreshToken = refreshToken,
|
||||
serverInfo = res.serverInfo,
|
||||
userInfo = res.activeUser,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,6 @@ using Speckle.Newtonsoft.Json;
|
||||
using Speckle.Sdk.Api.GraphQL;
|
||||
using Speckle.Sdk.Api.GraphQL.Models;
|
||||
using Speckle.Sdk.Api.GraphQL.Models.Responses;
|
||||
using Speckle.Sdk.Api.GraphQL.Serializer;
|
||||
using Speckle.Sdk.Common;
|
||||
using Speckle.Sdk.Helpers;
|
||||
using Speckle.Sdk.Logging;
|
||||
@@ -30,7 +29,9 @@ public partial interface IAccountManager : IDisposable;
|
||||
public sealed class AccountManager(
|
||||
ISpeckleApplication application,
|
||||
ILogger<AccountManager> logger,
|
||||
IGraphQLClientFactory graphQLClientFactory,
|
||||
ISpeckleHttp speckleHttp,
|
||||
IAccountFactory accountFactory,
|
||||
ISqLiteJsonCacheManagerFactory sqLiteJsonCacheManagerFactory
|
||||
) : IAccountManager
|
||||
{
|
||||
@@ -58,39 +59,12 @@ public sealed class AccountManager(
|
||||
/// <exception cref="AggregateException"><inheritdoc cref="GraphQLErrorHandler.EnsureGraphQLSuccess(IGraphQLResponse)"/></exception>
|
||||
public async Task<ServerInfo> GetServerInfo(Uri server, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var httpClient = speckleHttp.CreateHttpClient();
|
||||
using var gqlClient = graphQLClientFactory.CreateGraphQLClient(server, null);
|
||||
|
||||
using var gqlClient = new GraphQLHttpClient(
|
||||
new GraphQLHttpClientOptions
|
||||
{
|
||||
EndPoint = new Uri(server, "/graphql"),
|
||||
UseWebSocketForQueriesAndMutations = false,
|
||||
},
|
||||
new NewtonsoftJsonSerializer(),
|
||||
httpClient
|
||||
);
|
||||
//lang=graphql
|
||||
const string QUERY_STRING = "query { serverInfo { name company migration { movedFrom movedTo } } }";
|
||||
|
||||
System.Version version = await gqlClient
|
||||
.GetServerVersion(cancellationToken: cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
// serverMigration property was added in 2.18.5, so only query for it
|
||||
// if the server has been updated past that version
|
||||
System.Version serverMigrationVersion = new(2, 18, 5);
|
||||
|
||||
string queryString;
|
||||
if (version >= serverMigrationVersion)
|
||||
{
|
||||
//language=graphql
|
||||
queryString = "query { serverInfo { name company migration { movedFrom movedTo } } }";
|
||||
}
|
||||
else
|
||||
{
|
||||
//language=graphql
|
||||
queryString = "query { serverInfo { name company } }";
|
||||
}
|
||||
|
||||
var request = new GraphQLRequest { Query = queryString };
|
||||
var request = new GraphQLRequest { Query = QUERY_STRING };
|
||||
|
||||
var response = await gqlClient.SendQueryAsync<ServerInfoResponse>(request, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -98,7 +72,6 @@ public sealed class AccountManager(
|
||||
|
||||
ServerInfo serverInfo = response.Data.serverInfo;
|
||||
serverInfo.url = server.ToString().TrimEnd('/');
|
||||
serverInfo.frontend2 = await IsFrontend2Server(server).ConfigureAwait(false);
|
||||
|
||||
return response.Data.serverInfo;
|
||||
}
|
||||
@@ -113,13 +86,8 @@ public sealed class AccountManager(
|
||||
/// <exception cref="AggregateException"><inheritdoc cref="GraphQLErrorHandler.EnsureGraphQLSuccess(IGraphQLResponse)"/></exception>
|
||||
public async Task<UserInfo> GetUserInfo(string token, Uri server, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var httpClient = speckleHttp.CreateHttpClient(authorizationToken: token);
|
||||
using var gqlClient = graphQLClientFactory.CreateGraphQLClient(server, token);
|
||||
|
||||
using var gqlClient = new GraphQLHttpClient(
|
||||
new GraphQLHttpClientOptions { EndPoint = new Uri(server, "/graphql") },
|
||||
new NewtonsoftJsonSerializer(),
|
||||
httpClient
|
||||
);
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
query {
|
||||
@@ -142,59 +110,6 @@ public sealed class AccountManager(
|
||||
return response.Data.data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets basic user and server information given a token and a server.
|
||||
/// </summary>
|
||||
/// <param name="token"></param>
|
||||
/// <param name="server">Server URL</param>
|
||||
/// <returns></returns>
|
||||
internal async Task<ActiveUserServerInfoResponse> GetUserServerInfo(
|
||||
string token,
|
||||
Uri server,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
using var httpClient = speckleHttp.CreateHttpClient(authorizationToken: token);
|
||||
|
||||
using var client = new GraphQLHttpClient(
|
||||
new GraphQLHttpClientOptions { EndPoint = new Uri(server, "/graphql") },
|
||||
new NewtonsoftJsonSerializer(),
|
||||
httpClient
|
||||
);
|
||||
|
||||
System.Version version = await client.GetServerVersion(ct).ConfigureAwait(false);
|
||||
|
||||
// serverMigration property was added in 2.18.5, so only query for it
|
||||
// if the server has been updated past that version
|
||||
System.Version serverMigrationVersion = new(2, 18, 5);
|
||||
|
||||
string queryString;
|
||||
if (version >= serverMigrationVersion)
|
||||
{
|
||||
//language=graphql
|
||||
queryString =
|
||||
"query { activeUser { id name email company avatar streams { totalCount } commits { totalCount } } serverInfo { name company adminContact description version migration { movedFrom movedTo } } }";
|
||||
}
|
||||
else
|
||||
{
|
||||
//language=graphql
|
||||
queryString =
|
||||
"query { activeUser { id name email company avatar streams { totalCount } commits { totalCount } } serverInfo { name company adminContact description version } }";
|
||||
}
|
||||
|
||||
var request = new GraphQLRequest { Query = queryString };
|
||||
|
||||
var response = await client.SendQueryAsync<ActiveUserServerInfoResponse>(request, ct).ConfigureAwait(false);
|
||||
|
||||
response.EnsureGraphQLSuccess();
|
||||
|
||||
ServerInfo serverInfo = response.Data.serverInfo;
|
||||
serverInfo.url = server.ToString().TrimEnd('/');
|
||||
serverInfo.frontend2 = await IsFrontend2Server(server).ConfigureAwait(false);
|
||||
|
||||
return response.Data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Default Server URL for authentication, can be overridden by placing a file with the alternatrive url in the Speckle folder or with an ENV_VAR
|
||||
/// </summary>
|
||||
@@ -254,13 +169,12 @@ public sealed class AccountManager(
|
||||
account.serverInfo.migration.movedTo = null;
|
||||
account.serverInfo.migration.movedFrom = new Uri(account.serverInfo.url);
|
||||
account.serverInfo.url = upgradeUri.ToString().TrimEnd('/');
|
||||
account.serverInfo.frontend2 = true;
|
||||
|
||||
// setting the id to null will force it to be recreated
|
||||
account.id = null!; //TODO this is gross so remove when id is nullable
|
||||
|
||||
RemoveAccount(id);
|
||||
_accountStorage.SaveObject(account.id.NotNull(), JsonConvert.SerializeObject(account));
|
||||
_accountStorage.UpdateObject(account.id.NotNull(), JsonConvert.SerializeObject(account));
|
||||
}
|
||||
|
||||
public IEnumerable<Account> GetAccounts(string serverUrl)
|
||||
@@ -410,11 +324,11 @@ public sealed class AccountManager(
|
||||
try
|
||||
{
|
||||
Uri url = new(account.serverInfo.url);
|
||||
var userServerInfo = await GetUserServerInfo(account.token, url, ct).ConfigureAwait(false);
|
||||
var userServerInfo = await accountFactory.GetUserServerInfo(url, account.token, ct).ConfigureAwait(false);
|
||||
|
||||
//the token has expired
|
||||
//TODO: once we get a token expired exception from the server use that instead
|
||||
if (userServerInfo?.activeUser == null || userServerInfo.serverInfo == null)
|
||||
if (userServerInfo.activeUser == null || userServerInfo.serverInfo == null)
|
||||
{
|
||||
// We were initially was handling refresh token here bc quite a while ago server was returning null
|
||||
// for activeUser and serverInfo instead of throwing exception. In short, our logic moved into catch block to cover both.
|
||||
@@ -493,7 +407,7 @@ public sealed class AccountManager(
|
||||
{
|
||||
account.isDefault = true;
|
||||
}
|
||||
_accountStorage.SaveObject(account.id, JsonConvert.SerializeObject(account));
|
||||
_accountStorage.UpdateObject(account.id, JsonConvert.SerializeObject(account));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -635,16 +549,13 @@ public sealed class AccountManager(
|
||||
try
|
||||
{
|
||||
var tokenResponse = await GetToken(accessCode, challenge, server).ConfigureAwait(false);
|
||||
var userResponse = await GetUserServerInfo(tokenResponse.token, server).ConfigureAwait(false);
|
||||
|
||||
var account = new Account
|
||||
{
|
||||
token = tokenResponse.token,
|
||||
refreshToken = tokenResponse.refreshToken,
|
||||
isDefault = !GetAccounts().Any(),
|
||||
serverInfo = userResponse.serverInfo,
|
||||
userInfo = userResponse.activeUser,
|
||||
};
|
||||
var account = await accountFactory
|
||||
.CreateAccount(server, tokenResponse.token, tokenResponse.refreshToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
account.isDefault = !GetAccounts().Any();
|
||||
|
||||
logger.LogInformation("Successfully created account for {serverUrl}", server);
|
||||
|
||||
return account;
|
||||
@@ -781,7 +692,7 @@ public sealed class AccountManager(
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<TokenExchangeResponse> GetRefreshedToken(string refreshToken, Uri server, string app = "sca")
|
||||
private async Task<TokenExchangeResponse> GetRefreshedToken(string? refreshToken, Uri server, string app = "sca")
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -808,38 +719,6 @@ public sealed class AccountManager(
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a simple get request to the <paramref name="server"/>, and checks the response headers for a <c>"x-speckle-frontend-2"</c> <see cref="Boolean"/> value
|
||||
/// </summary>
|
||||
/// <param name="server">Server endpoint to get header</param>
|
||||
/// <returns><see langword="true"/> if response contains FE2 header and the value was <see langword="true"/></returns>
|
||||
/// <exception cref="SpeckleException">response contained FE2 header, but the value was <see langword="null"/>, empty, or not parseable to a <see cref="Boolean"/></exception>
|
||||
/// <exception cref="System.Net.Http.HttpRequestException">Request to <paramref name="server"/> failed to send or response was not successful</exception>
|
||||
private async Task<bool> IsFrontend2Server(Uri server)
|
||||
{
|
||||
using var httpClient = speckleHttp.CreateHttpClient();
|
||||
|
||||
var response = await speckleHttp.HttpPing(server).ConfigureAwait(false);
|
||||
|
||||
var headers = response.Headers;
|
||||
const string HEADER = "x-speckle-frontend-2";
|
||||
if (!headers.TryGetValues(HEADER, out IEnumerable<string>? values))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string? headerValue = values.FirstOrDefault();
|
||||
|
||||
if (!bool.TryParse(headerValue, out bool value))
|
||||
{
|
||||
throw new SpeckleException(
|
||||
$"Headers contained {HEADER} header, but value {headerValue} could not be parsed to a bool"
|
||||
);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private static string GenerateChallenge()
|
||||
{
|
||||
#if NET8_0
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
using System.Net.WebSockets;
|
||||
using System.Reflection;
|
||||
using GraphQL.Client.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Speckle.InterfaceGenerator;
|
||||
using Speckle.Newtonsoft.Json;
|
||||
using Speckle.Newtonsoft.Json.Serialization;
|
||||
using Speckle.Sdk.Api.GraphQL.Serializer;
|
||||
using Speckle.Sdk.Helpers;
|
||||
|
||||
namespace Speckle.Sdk.Credentials;
|
||||
|
||||
[GenerateAutoInterface]
|
||||
public class GraphQLClientFactory(
|
||||
ISpeckleApplication application,
|
||||
ISpeckleHttp speckleHttp,
|
||||
ILogger<GraphQLClientFactory> logger
|
||||
) : IGraphQLClientFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// <inheritdoc cref="CreateGraphQLClient(Uri, string)"/>
|
||||
/// </summary>
|
||||
/// <param name="account">The account to use for authentication</param>
|
||||
/// <returns></returns>
|
||||
public GraphQLHttpClient CreateGraphQLClient(Account account)
|
||||
{
|
||||
return CreateGraphQLClient(new(account.serverInfo.url), account.token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="GraphQLHttpClient"/> configured for communication with a Speckle server
|
||||
/// </summary>
|
||||
/// <param name="serverUrl">The base url of the speckle server to communicate with</param>
|
||||
/// <param name="authToken">If provided, all requests will be authenticated</param>
|
||||
/// <returns></returns>
|
||||
public GraphQLHttpClient CreateGraphQLClient(Uri serverUrl, string? authToken)
|
||||
{
|
||||
var gQLClient = new GraphQLHttpClient(
|
||||
new GraphQLHttpClientOptions
|
||||
{
|
||||
EndPoint = new Uri(serverUrl, "/graphql"),
|
||||
UseWebSocketForQueriesAndMutations = false,
|
||||
WebSocketProtocol = "graphql-ws",
|
||||
ConfigureWebSocketConnectionInitPayload = _ =>
|
||||
{
|
||||
return SpeckleHttp.CanAddAuth(authToken, out string? authValue) ? new { Authorization = authValue } : null;
|
||||
},
|
||||
},
|
||||
new NewtonsoftJsonSerializer(
|
||||
new JsonSerializerSettings()
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver { IgnoreIsSpecifiedMembers = true }, //(Default)
|
||||
MissingMemberHandling = MissingMemberHandling.Error, //(not default) If you query for a member that doesn't exist, this will throw (except websocket responses see https://github.com/graphql-dotnet/graphql-client/issues/660)
|
||||
NullValueHandling = NullValueHandling.Ignore, //(not default) We won't serialize nulls, as can open more opportunity for conflicting with servers that are old and don't have the latest schema
|
||||
Converters = { new ConstantCaseEnumConverter() }, //(Default) enums will be serialized using the GraphQL const case standard
|
||||
}
|
||||
),
|
||||
CreateHttpClient(authToken)
|
||||
);
|
||||
|
||||
gQLClient.WebSocketReceiveErrors.Subscribe(ex =>
|
||||
{
|
||||
if (ex is WebSocketException we)
|
||||
{
|
||||
logger.LogError(
|
||||
we,
|
||||
"GraphQL Websocket received an {WebSocketErrorCode} ({NativeErrorCode}) error that has been swallowed",
|
||||
we.WebSocketErrorCode,
|
||||
we.ErrorCode
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogError(ex, "GraphQL Websocket received an error that has been swallowed");
|
||||
}
|
||||
});
|
||||
return gQLClient;
|
||||
}
|
||||
|
||||
private HttpClient CreateHttpClient(string? token)
|
||||
{
|
||||
var httpClient = speckleHttp.CreateHttpClient(timeoutSeconds: 30, authorizationToken: token);
|
||||
|
||||
httpClient.DefaultRequestHeaders.Add("apollographql-client-name", application.ApplicationAndVersion);
|
||||
httpClient.DefaultRequestHeaders.Add(
|
||||
"apollographql-client-version",
|
||||
Assembly.GetExecutingAssembly().GetName().Version?.ToString()
|
||||
);
|
||||
return httpClient;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
using Speckle.Newtonsoft.Json;
|
||||
using Speckle.Sdk.Api.GraphQL.Models;
|
||||
|
||||
namespace Speckle.Sdk.Credentials;
|
||||
|
||||
internal sealed class ActiveUserServerInfoResponse
|
||||
{
|
||||
public UserInfo activeUser { get; init; }
|
||||
[property: JsonProperty(Required = Required.AllowNull)]
|
||||
public UserInfo? activeUser { get; init; }
|
||||
|
||||
[property: JsonProperty(Required = Required.Always)]
|
||||
public ServerInfo serverInfo { get; init; }
|
||||
}
|
||||
|
||||
|
||||
@@ -51,9 +51,7 @@ public class SpeckleHttp(ILogger<SpeckleHttp> logger, ISpeckleHttpClientHandlerF
|
||||
|
||||
var client = new HttpClient(speckleHandler)
|
||||
{
|
||||
Timeout =
|
||||
Timeout.InfiniteTimeSpan //timeout is configured on the SpeckleHttpClientHandler through policy
|
||||
,
|
||||
Timeout = Timeout.InfiniteTimeSpan, //timeout is configured on the SpeckleHttpClientHandler through policy
|
||||
};
|
||||
AddAuthHeader(client, authorizationToken);
|
||||
return client;
|
||||
|
||||
@@ -15,10 +15,14 @@ internal static class TypeLoader
|
||||
private static ConcurrentDictionary<string, Type> s_cachedTypes = new();
|
||||
private static ConcurrentDictionary<Type, string> s_fullTypeStrings = new();
|
||||
private static ConcurrentDictionary<PropertyInfo, JsonPropertyAttribute?> s_jsonPropertyAttribute = new();
|
||||
private static readonly ConcurrentDictionary<PropertyInfo, bool> s_obsolete = new();
|
||||
private static ConcurrentDictionary<Type, IReadOnlyList<PropertyInfo>> s_propInfoCache = new();
|
||||
|
||||
public static IEnumerable<LoadedType> Types => s_availableTypes;
|
||||
|
||||
public static bool IsObsolete(PropertyInfo property) =>
|
||||
s_obsolete.GetOrAdd(property, p => p.IsDefined(typeof(ObsoleteAttribute), true));
|
||||
|
||||
public static JsonPropertyAttribute? GetJsonPropertyAttribute(PropertyInfo property) =>
|
||||
s_jsonPropertyAttribute.GetOrAdd(property, p => p.GetCustomAttribute<JsonPropertyAttribute>(true));
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
using Speckle.Newtonsoft.Json;
|
||||
using Speckle.Newtonsoft.Json.Linq;
|
||||
using Speckle.Sdk.Common;
|
||||
using Speckle.Sdk.Helpers;
|
||||
using Speckle.Sdk.Host;
|
||||
using Speckle.Sdk.Serialisation;
|
||||
@@ -92,8 +91,7 @@ public class Base : DynamicBase, ISpeckleObject
|
||||
var typedProps = @base.GetInstanceMembers();
|
||||
foreach (var prop in typedProps.Where(p => p.CanRead))
|
||||
{
|
||||
bool isIgnored =
|
||||
prop.IsDefined(typeof(ObsoleteAttribute), true) || prop.IsDefined(typeof(JsonIgnoreAttribute), true);
|
||||
bool isIgnored = TypeLoader.IsObsolete(prop) || prop.IsDefined(typeof(JsonIgnoreAttribute), true);
|
||||
if (isIgnored)
|
||||
{
|
||||
continue;
|
||||
@@ -193,30 +191,4 @@ public class Base : DynamicBase, ISpeckleObject
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a shallow copy of the current base object.
|
||||
/// This operation does NOT copy/duplicate the data inside each prop.
|
||||
/// The new object's property values will be pointers to the original object's property value.
|
||||
/// </summary>
|
||||
/// <returns>A shallow copy of the original object.</returns>
|
||||
public Base ShallowCopy()
|
||||
{
|
||||
Type type = GetType();
|
||||
Base myDuplicate = (Base)Activator.CreateInstance(type).NotNull();
|
||||
myDuplicate.id = id;
|
||||
myDuplicate.applicationId = applicationId;
|
||||
|
||||
foreach (var kvp in GetMembers())
|
||||
{
|
||||
var propertyInfo = type.GetProperty(kvp.Key);
|
||||
if (propertyInfo is not null && !propertyInfo.CanWrite)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
myDuplicate[kvp.Key] = kvp.Value;
|
||||
}
|
||||
|
||||
return myDuplicate;
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user