Compare commits

...

96 Commits

Author SHA1 Message Date
Adam Hathcock 4b82db8ea2 Merge pull request #361 from specklesystems/dev
.NET Build and Publish / build (push) Has been cancelled
Main to dev
2025-07-23 10:23:04 +01:00
Adam Hathcock 9e7f26f7a6 Add ModelCacheManager class and use it (#356)
* Introduce ModelCacheManager to manage cache and sizes and deletions

* move and abstract

* add tests and format

* Update src/Speckle.Sdk/Caching/ModelCacheManager.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Clean up

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-23 10:07:57 +01:00
Adam Hathcock b19f8c4219 Merge pull request #358 from specklesystems/dev
.NET Build and Publish / build (push) Has been cancelled
(NO SQUASH) Main to dev for release
2025-07-21 15:49:49 +01:00
Adam Hathcock c517e61517 Merge pull request #360 from specklesystems/main-dev
Main to dev
2025-07-21 15:47:38 +01:00
Adam Hathcock b3e0623856 Merge remote-tracking branch 'origin/main' into main-dev 2025-07-21 15:36:30 +01:00
Adam Hathcock e5d1ef2448 Merge pull request #354 from specklesystems/main-dev
Merge pull request #348 from specklesystems/dev
2025-07-21 11:12:54 +01:00
Adam Hathcock 83c3de05fa Merge remote-tracking branch 'origin/dev' into main-dev 2025-07-21 11:02:02 +01:00
Adam Hathcock 507ded7d4a Fix shallow copy allocations and perf (#357)
* add more DynamicBase Tests

* Move ShallowCopy to dynamic and try to be faster with copy

* Correct tests for macOS

* use cache obsolete attribute

* Update src/Speckle.Sdk/Models/DynamicBase.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update tests/Speckle.Sdk.Tests.Unit/Models/DynamicBaseTests.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update tests/Speckle.Sdk.Tests.Unit/Models/DynamicBaseTests.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix AI

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-21 11:01:00 +01:00
Adam Hathcock e15029bab3 Merge pull request #350 from specklesystems/dev
.NET Build and Publish / build (push) Has been cancelled
(dev to main) If we're already cancelling, ignore extra exceptions (#349)
2025-07-14 10:34:06 +01:00
Adam Hathcock a43fd44206 Stop recording an exception that's rethrown (#355)
* Stop recording an exception that's rethrown

* Update src/Speckle.Sdk/Api/GraphQL/Client.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-14 10:15:40 +01:00
Adam Hathcock 1bcd8ac3a4 Merge branch 'dev' into main-dev 2025-07-03 10:42:05 +01:00
Adam Hathcock a8dc93e22b Adds detail to message so that user isn't going WTF SDK (#351)
* Adds detail to message so that user isn't going WTF SDK

* update exception tests
2025-07-03 09:39:53 +00:00
Jedd Morgan 5a0f883b98 Add compatibility with :local docker images (#353)
Co-authored-by: Adam Hathcock <adamhathcock@users.noreply.github.com>
2025-07-03 10:28:35 +01:00
Adam Hathcock a5d035671a If we're already cancelling, ignore extra exceptions (#349)
* If we're already cancelling, ignore extra exceptions

* Do more robust cancellation

* Try to have more robust disposal and cancellation check
2025-07-01 10:20:32 +01:00
Adam Hathcock cd6ebad619 Merge pull request #348 from specklesystems/dev
.NET Build and Publish / build (push) Has been cancelled
New release for connectors
2025-06-30 15:15:51 +01:00
Adam Hathcock 33c2e6e1a4 Better handle graphql commit errors (#343)
* Better handle graphql commit errors

* add graphql error test
2025-06-30 10:54:44 +00:00
Adam Hathcock b97702adb1 Small fixes to SDK (#347)
* Increase channel capacity to make things more performant

* Avoid logging send cancellation exceptions, caller did it

* Try to avoid collection modified errors when cancelling by more aggressive checks

* oops, rethrow, don't catch
2025-06-30 10:37:30 +00:00
Adam Hathcock 80c4f694ec Merge pull request #346 from specklesystems/main-dev
Main to dev
2025-06-30 11:26:36 +01:00
Adam Hathcock fb5042004f Merge remote-tracking branch 'origin/dev' into main-dev 2025-06-30 08:58:57 +01:00
Adam Hathcock c0a9291632 Merge pull request #344 from specklesystems/oguzhan/level-proxies
.NET Build and Publish / build (push) Has been cancelled
Feat(objects): level proxies
2025-06-23 15:05:15 +01:00
oguzhankoral b783d2acb6 Format 2025-06-23 15:57:24 +03:00
oguzhankoral 93539adc1e Add level proxies 2025-06-23 15:42:48 +03:00
Adam Hathcock 98005933de Remove DistinctBy as we don't use it (#342) 2025-06-19 09:34:34 +01:00
Adam Hathcock 50906b172a Merge pull request #340 from specklesystems/dev
.NET Build and Publish / build (push) Has been cancelled
2025-06-11 17:35:27 +01:00
Adam Hathcock 05f7353925 Revert "Merge pull request #335 from specklesystems/adam/cnx-1786-allow-multiple-sends-to-access-sqlite-in-a-non-locking-2" (#339)
This reverts commit 59019bf846, reversing
changes made to 3afaf61a1a.

Co-authored-by: Adam Hathcock <adam@Adams-Mac-mini.localdomain>
2025-06-11 15:32:06 +00:00
Adam Hathcock 8328498553 Merge pull request #338 from specklesystems/dev
.NET Build and Publish / build (push) Has been cancelled
Dev to Main (no squash)
2025-06-11 11:07:20 +01:00
Adam Hathcock 59019bf846 Merge pull request #335 from specklesystems/adam/cnx-1786-allow-multiple-sends-to-access-sqlite-in-a-non-locking-2
Use an object saver per stream instead of sqlite manager per stream
2025-06-11 10:50:34 +01:00
Adam Hathcock 3afaf61a1a Merge pull request #337 from specklesystems/main
Main to dev (no squash)
2025-06-11 10:50:18 +01:00
Adam Hathcock 424609fad0 fix tests 2025-06-10 13:18:34 +01:00
Adam Hathcock 46c067308e Fix DI dependency and tests 2025-06-10 11:39:42 +01:00
Adam Hathcock 0e33e8df8f add tests 2025-06-10 11:33:17 +01:00
Adam Hathcock bc81c21e9d format 2025-06-10 11:15:30 +01:00
Adam Hathcock 7f8b59d348 Pool object savers instead of sqlite 2025-06-10 11:15:01 +01:00
Adam Hathcock 44ba61e4a5 Adjustments to avoid sqlite "database is locked" errors (#333)
* add new exception test

* Make memory tests and file path tests be explicit

* set the default write parallelism to 1

* set to single reader for caching channel

* format

* Try to have consistent DB locked error test

* always a single reader of the channel

* Remove extra snapshot

* Revert "Try to have consistent DB locked error test"

This reverts commit 93669c57a3.

* remove extra test that doesn't do anything
2025-06-09 16:24:39 +00:00
Adam Hathcock 4f7b470901 Merge pull request #329 from specklesystems/dev
.NET Build and Publish / build (push) Has been cancelled
SDK 3.4 release (do not squash)
2025-06-05 13:59:43 +01:00
Jedd Morgan 8c6426d617 Updated one more usage of newtonsoft (#332)
Co-authored-by: Adam Hathcock <adamhathcock@users.noreply.github.com>
2025-06-05 10:39:16 +00:00
Adam Hathcock 5562ce1a2d Add detail to the message in logs (#331) 2025-06-05 09:54:36 +01:00
Jedd Morgan 7019b8d7c6 Fix(automate): STJ camel casing (#330)
* stj required

* More requireds

* Use JSON serializer settings for camel case rule

* readonly

* static naming
2025-06-04 15:56:50 +00:00
Adam Hathcock 58a0326060 Receive should sort the root closures to see a speed improvement (#311)
* Maybe really fixes closures

* fornat

* add ai generated tests

* fix tests

* fix tests

* added test with correct number of closures?

* closures are self contained.  don't increment on attached properties

* format

* MergeClosure should reuse if exists, not just set

* Add generated tests and sort the parser correctly when using get closures

* add extra options to not sort and make sorting default for receive

* hide private method
2025-06-04 13:54:26 +00:00
Adam Hathcock 55f83919d1 Don't log aggregate exceptions that only contain cancelled exceptions (#326)
* Don't log aggregate exceptions that only contain cancelled exceptions

* check if all are not cancelled
2025-06-04 12:59:51 +00:00
Adam Hathcock 46c57b18be Cancel before kicking off tasks to ensure they throw (#328) 2025-06-04 13:49:03 +01:00
Adam Hathcock 7b5ada57cd Changing uploaded to uploading to better show progress than just rate (#321) 2025-06-03 16:27:02 +00:00
Adam Hathcock e29b27bcd3 Add some debugging stats about the sent or received payloads to add debugging when things are massive (#319) 2025-06-03 14:15:37 +00:00
Jedd Morgan ff1b688321 refactor(accountmanager): Refactor account manager for automate (#320)
* First pass refactor account manager

* Use GraphQLClient factory in account manager also

* update account references

* cleanup

* Added test + comments

* more docstrings

* More tests
2025-06-03 12:54:44 +00:00
Adam Hathcock 0be143d391 Make nulls appear as soon as possible (#324) 2025-06-03 12:09:55 +00:00
Adam Hathcock c0a66a297a Add info to send/receives for debugging (#325) 2025-06-03 12:48:05 +01:00
Adam Hathcock aad604e819 Merge pull request #323 from specklesystems/main-dev
Main to dev
2025-06-03 09:41:16 +01:00
Adam Hathcock 48313cb082 Merge remote-tracking branch 'origin/dev' into main-dev 2025-06-02 15:50:19 +01:00
kekesidavid 0361a6e5b7 Merge pull request #318 from specklesystems/david/move-text-class-update-pr-to-main
.NET Build and Publish / build (push) Has been cancelled
fix (sdk) Text class updates: removed origin from Text class and added screenAligned prop
2025-06-02 09:22:57 +02:00
David Kekesi 0e97782c29 fixed comment 2025-06-02 09:12:12 +02:00
David Kekesi 298dedc3af text class update pr moved to main 2025-05-30 19:01:02 +02:00
Adam Hathcock 422403d499 fix concurrent tests 2 - This should wait for cancellation to happen (#316)
* Fix test to be deterministic

* This should wait for cancellation to happen

* update csharpier
2025-05-30 14:17:04 +00:00
Adam Hathcock b652ffa773 Fix test to be deterministic (#315) 2025-05-30 12:30:57 +00:00
Adam Hathcock 68ace02e2d Use custom md5 just for account/user IDs, not anything real (#314)
* Use custom md5 just for account/user IDs, not anything real

* test fixes

* To lower and upper as needed
2025-05-30 12:15:44 +00:00
Jedd Morgan b6be7a351f feat(automate): Add automate SDK (#313)
* First pass

* First pass adding service registraiton

* Finished up service registration

* Json exception

* Moved to the right place

* Fixed tests

* Added some missing docs strings

* Reflecting Gergo's specklepy changes

* Correct the DI registration

* Readme

* No warn beta packages

* Format

* renamed misleading variable

* Fixed lock files

* Disable SQLite for automate
2025-05-30 13:05:14 +01:00
Adam Hathcock 1039e75d0c Calculate closures correctly (#309)
* Maybe really fixes closures

* fornat

* add ai generated tests

* fix tests

* fix tests

* added test with correct number of closures?

* closures are self contained.  don't increment on attached properties

* format

* MergeClosure should reuse if exists, not just set

* add not null on a method
2025-05-27 14:05:10 +01:00
Jedd Morgan 0f8752d5ab feat(api): Improvements to GrahpQL error handling (#304)
* Graphql extras

* extra server resource test

* usings

* Fixed test
2025-05-20 12:44:23 +00:00
Adam Hathcock 64a93345d6 Merge pull request #310 from specklesystems/main
Main to dev (no squash)
2025-05-19 12:00:56 +01:00
Jedd Morgan efc38d8f5c Added Workspace project visibility (#307)
.NET Build and Publish / build (push) Has been cancelled
2025-05-14 21:37:39 +03:00
Jedd Morgan e3ca75abe1 removed csharp 4.7 dependency from .net8 target (#306) 2025-05-14 13:12:37 +01:00
Adam Hathcock b479d368ad SLNX added, only supported in IDEs and .NET 9+ (#294) 2025-05-14 09:35:43 +00:00
Adam Hathcock 8d3985f93b Merge pull request #305 from specklesystems/dev
.NET Build and Publish / build (push) Has been cancelled
Dev to Main
2025-05-14 10:19:51 +01:00
Jedd Morgan 915a18dc98 Merge pull request #303 from specklesystems/jrm/main-dev
Main -> Dev
2025-05-13 11:38:56 +01:00
Jedd Morgan 5fcb3223d6 Merge branch 'dev' into jrm/main-dev 2025-05-13 11:28:13 +01:00
Adam Hathcock 21851c06d2 Use WhenAll instead of WhenAny and avoid manual task exception processing (#300)
* No reason to process exceptions manually

* formatting

* use a pool for gathering child task results

* Use a smaller whenany with a cancellation

* formatting
2025-05-13 10:00:53 +01:00
Adam Hathcock 3a9a633d30 Add empty DataObject tests (#301)
* Add empty DataObject tests

* format
2025-05-09 14:51:03 +00:00
Jedd Morgan 7f092d529c feat(api): Add ActiveUserResource.GetProjectsWithPermissions (#299)
.NET Build and Publish / build (push) Has been cancelled
* Fixed Mistakes (#296)

* Added extra permission checks (#297)

* Add extra query for project with permissions

---------

Co-authored-by: Adam Hathcock <adamhathcock@users.noreply.github.com>
Co-authored-by: Adam Hathcock <adam@hathcock.uk>
2025-05-08 15:11:53 +00:00
Jedd Morgan a4f0e0e4aa Added extra permission checks (#297)
.NET Build and Publish / build (push) Has been cancelled
2025-05-08 09:25:08 +00:00
Jedd Morgan 227729a0df Fixed Mistakes (#296)
.NET Build and Publish / build (push) Has been cancelled
2025-05-07 17:01:56 +00:00
Jedd Morgan 178085f3f8 dev -> main (#295)
.NET Build and Publish / build (push) Has been cancelled
* Sanitize test references

* add registration tests to SDK and ignore classes that can't be made

* add additional test

* chore(dev) Update to csharpier 1.0 (#284)

* Update to csharpier 1.0

* Fix check and nowarn

* format

* Update dependencies (#285)

* Added extra test for GetMembers (#290)

* Added extra test for GetMembers

* fixed tests

* verify

* Format

* Run the module init on unit tests and make it json

* update deps

* gitignore support was disabled in csharpier 1.0.1

---------

Co-authored-by: Adam Hathcock <adam@hathcock.uk>

* Add workspaces queries (#291)

* Add workspaces queries

* Format

* extra tweaks

* init speckle verify

* Add workspace creation state

* Add workspace creation test

* test exceptional cases

* GetActiveWorkspace tests

* fixed test

---------

Co-authored-by: Adam Hathcock <adamhathcock@users.noreply.github.com>
Co-authored-by: Adam Hathcock <adam@hathcock.uk>
2025-05-06 18:02:59 +03:00
Adam Hathcock 9794195e9c Merge pull request #293 from specklesystems/main-dev
.NET Build and Publish / build (push) Has been cancelled
Main to dev (NO SQUASH)
2025-05-06 14:00:54 +01:00
Adam Hathcock a61c442930 Merge branch 'dev' into main-dev 2025-05-06 13:50:16 +01:00
Jedd Morgan 68a407905d Add workspaces queries (#291)
* Add workspaces queries

* Format

* extra tweaks

* init speckle verify

* Add workspace creation state

* Add workspace creation test

* test exceptional cases

* GetActiveWorkspace tests

* fixed test
2025-05-01 21:23:30 +03:00
Adam Hathcock 0f2abaf532 Merge remote-tracking branch 'origin/dev' into main-dev 2025-04-30 16:24:06 +01:00
Adam Hathcock 07634b6f6a Merge remote-tracking branch 'origin/dev' into main-dev 2025-04-30 16:22:39 +01:00
Jedd Morgan e938725d35 Added extra test for GetMembers (#290)
* Added extra test for GetMembers

* fixed tests

* verify

* Format

* Run the module init on unit tests and make it json

* update deps

* gitignore support was disabled in csharpier 1.0.1

---------

Co-authored-by: Adam Hathcock <adam@hathcock.uk>
2025-04-30 16:13:41 +01:00
Adam Hathcock d3369e3ce5 Merge pull request #289 from specklesystems/main-dev
Main to dev (don't squash)
2025-04-30 16:00:46 +01:00
KatKatKateryna d75a61d775 Add text class (#271)
.NET Build and Publish / build (push) Has been cancelled
* draft class

* corrections

* edits

* max width

* remove import

* typo

* naming

* move directories

* delete from old location

* comment

* formatting

---------

Co-authored-by: Claire Kuang <kuang.claire@gmail.com>
2025-04-30 10:56:29 +02:00
Adam Hathcock 2ae4003afb Merge branch 'main' into main-dev 2025-04-28 10:52:58 +01:00
Adam Hathcock 24db4c4ae4 Merge pull request #288 from specklesystems/adam/no-drop-writes
.NET Build and Publish / build (push) Has been cancelled
fix (main) Don't drop items to write when sending fast
2025-04-28 10:18:48 +01:00
Adam Hathcock edf63d4a1b fix build issue 2025-04-28 09:39:46 +01:00
Adam Hathcock b5b0922e7f Revert to write async 2025-04-28 09:35:02 +01:00
Adam Hathcock ff390f772d just wait for space instead of another task and reduce size to 1000 2025-04-25 18:24:34 +01:00
Adam Hathcock d69f0bba2a fmt 2025-04-25 18:13:05 +01:00
Adam Hathcock 33c14fc14c Remove extras 2025-04-25 18:09:04 +01:00
Adam Hathcock 536e58aacc Don't drop items to write when sending fast 2025-04-25 17:45:01 +01:00
Adam Hathcock 88188aace6 Merge pull request #287 from specklesystems/main-dev
Main to dev (NO SQUASH)
2025-04-24 12:12:06 +01:00
Adam Hathcock ad44a7cdbc Merge branch 'dev' into main-dev 2025-04-24 12:01:32 +01:00
Adam Hathcock 38449dca9a Update dependencies (#285) 2025-04-24 11:59:26 +01:00
Adam Hathcock 764eb43838 Merge branch 'dev' into main-dev 2025-04-24 11:52:02 +01:00
Adam Hathcock a84e6d89ca chore(dev) Update to csharpier 1.0 (#284)
* Update to csharpier 1.0

* Fix check and nowarn

* format
2025-04-24 10:45:09 +00:00
Adam Hathcock a479440b66 Merge pull request #286 from specklesystems/adam/check-registration
feature (dev) check registration
2025-04-24 11:29:27 +01:00
Adam Hathcock d44b4fa52b add additional test 2025-04-24 08:33:42 +01:00
Adam Hathcock ea6ca8c555 add registration tests to SDK and ignore classes that can't be made 2025-04-23 15:57:03 +01:00
Adam Hathcock 113f0fd551 Sanitize test references 2025-04-23 15:42:57 +01:00
Adam Hathcock bcc4e25970 Merge pull request #280 from specklesystems/main-dev
Main->Dev
2025-04-23 13:04:37 +01:00
204 changed files with 6780 additions and 1459 deletions
+2 -2
View File
@@ -3,9 +3,9 @@
"isRoot": true,
"tools": {
"csharpier": {
"version": "0.30.6",
"version": "1.0.2",
"commands": [
"dotnet-csharpier"
"csharpier"
],
"rollForward": false
},
+26
View File
@@ -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
View File
@@ -1,6 +1,6 @@
printWidth: 120
useTabs: false
tabWidth: 2
indentSize: 2
preprocessorSymbolSets:
- ""
- "DEBUG"
+14
View File
@@ -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.
+22
View File
@@ -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 FluentAssertions 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!
+8 -21
View File
@@ -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
View File
@@ -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>
+7 -4
View File
@@ -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" />
+2 -1
View File
@@ -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
View File
@@ -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
@@ -51,6 +54,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 +112,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 +134,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
+46
View File
@@ -0,0 +1,46 @@
<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="GitVersion.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>
+1 -1
View File
@@ -68,7 +68,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"));
+1 -2
View File
@@ -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" />
+2
View File
@@ -100,6 +100,8 @@ services:
POSTGRES_PASSWORD: "speckle"
POSTGRES_DB: "speckle"
ENABLE_MP: "false"
LOG_PRETTY: "true"
networks:
default:
@@ -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 { }
+195
View File
@@ -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;
}
}
+618
View File
@@ -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=="
}
}
}
}
+68
View File
@@ -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,
}
-1
View File
@@ -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>();
-1
View File
@@ -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>>();
+20
View File
@@ -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>
-7
View File
@@ -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)
@@ -45,9 +45,9 @@ public abstract class ChannelSaver<T>
cancellationToken
)
.Join()
.Batch(cacheBatchSize ?? MAX_CACHE_BATCH)
.Batch(cacheBatchSize ?? MAX_CACHE_BATCH, singleReader: true)
.WithTimeout(HTTP_BATCH_TIMEOUT)
.ReadAllConcurrently(maxParallelism ?? MAX_CACHE_WRITE_PARALLELISM, SaveToCache, cancellationToken)
.ReadAllConcurrently(MAX_CACHE_WRITE_PARALLELISM, SaveToCache, cancellationToken)
.ContinueWith(
t =>
{
@@ -68,14 +68,15 @@ public abstract class ChannelSaver<T>
TaskScheduler.Current
);
public void Save(T item)
public async Task SaveAsync(T item, CancellationToken cancellationToken)
{
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);
+26 -26
View File
@@ -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",
+41
View File
@@ -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) { }
}
+9 -73
View File
@@ -1,18 +1,13 @@
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.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 +31,14 @@ 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 Uri ServerUrl => new(Account.serverInfo.url);
[JsonIgnore]
public Account Account { get; }
private HttpClient HttpClient { get; }
[AutoInterfaceIgnore]
public GraphQLHttpClient GQLClient { get; }
@@ -52,8 +47,7 @@ public sealed class Client : ISpeckleGraphQLClient, IClient
public Client(
ILogger<Client> logger,
ISdkActivityFactory activityFactory,
ISpeckleApplication application,
ISpeckleHttp speckleHttp,
IGraphQLClientFactory graphqlClientFactory,
Account account
)
{
@@ -69,10 +63,10 @@ public sealed class Client : ISpeckleGraphQLClient, IClient
ProjectInvite = new(this);
Comment = new(this);
Subscription = new(this);
Workspace = new(this);
Server = new(this);
HttpClient = CreateHttpClient(application, speckleHttp, account);
GQLClient = CreateGraphQLClient(account, HttpClient);
GQLClient = graphqlClientFactory.CreateGraphQLClient(account);
}
[AutoInterfaceIgnore]
@@ -105,7 +99,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 +127,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 +180,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;
}
}
+2 -4
View File
@@ -1,7 +1,6 @@
using Microsoft.Extensions.Logging;
using Speckle.InterfaceGenerator;
using Speckle.Sdk.Credentials;
using Speckle.Sdk.Helpers;
using Speckle.Sdk.Logging;
namespace Speckle.Sdk.Api;
@@ -10,10 +9,9 @@ namespace Speckle.Sdk.Api;
public class ClientFactory(
ILoggerFactory loggerFactory,
ISdkActivityFactory activityFactory,
ISpeckleApplication application,
ISpeckleHttp speckleHttp
IGraphQLClientFactory graphQLClientFactory
) : IClientFactory
{
public IClient Create(Account account) =>
new Client(loggerFactory.CreateLogger<Client>(), activityFactory, application, speckleHttp, account);
new Client(loggerFactory.CreateLogger<Client>(), activityFactory, graphQLClientFactory, 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);
@@ -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,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,26 @@
namespace Speckle.Sdk.Api.GraphQL.Models;
public sealed class Workspace
{
public string id { get; init; }
public string name { get; init; }
public string role { get; init; }
public string slug { get; init; }
public string? description { get; init; }
public string? logo { get; init; }
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,250 @@ 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></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<Workspace?> GetActiveWorkspace(CancellationToken cancellationToken = default)
{
//language=graphql
const string QUERY = """
query ActiveUser {
data:activeUser {
data:activeWorkspace {
id
name
role
slug
logo
createdAt
updatedAt
readOnly
description
creationState
{
completed
}
permissions {
canCreateProject {
authorized
code
message
}
}
}
}
}
""";
var request = new GraphQLRequest { Query = QUERY };
var response = await _client
.ExecuteGraphQLRequest<NullableResponse<NullableResponse<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="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;
}
}
@@ -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);
+22
View File
@@ -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);
}
}
}
+165
View File
@@ -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 &#x2264; Length &#x2264; 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);
}
}
+5 -5
View File
@@ -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,
};
}
}
+16 -137
View File
@@ -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,7 +169,6 @@ 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
@@ -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.
@@ -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;
}
}
+5 -1
View File
@@ -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; }
}
+1 -3
View File
@@ -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;
+4
View File
@@ -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));
+1 -29
View File
@@ -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;
}
}
+40 -2
View File
@@ -7,7 +7,7 @@ namespace Speckle.Sdk.Models;
/// <summary>
/// Base class implementing a bunch of nice dynamic object methods, like adding and removing props dynamically. Makes c# feel like json.
/// <para>Orginally adapted from Rick Strahl 🤘</para>
/// <para>Originally adapted from Rick Strahl 🤘</para>
/// <para>https://weblog.west-wind.com/posts/2012/feb/08/creating-a-dynamic-extensible-c-expando-object</para>
/// </summary>
public class DynamicBase : DynamicObject, IDynamicMetaObjectProvider
@@ -84,6 +84,44 @@ public class DynamicBase : DynamicObject, IDynamicMetaObjectProvider
}
}
/// <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 DynamicBase ShallowCopy()
{
Type type = GetType();
DynamicBase myDuplicate = (DynamicBase)(
Activator.CreateInstance(type) ?? throw new SpeckleException($"Failed to create instance of {type.Name}")
);
// Add dynamic members
foreach (var kvp in _properties)
{
myDuplicate._properties[kvp.Key] = kvp.Value;
}
var pinfos = TypeLoader.GetBaseProperties(type).Where(x => !TypeLoader.IsObsolete(x));
foreach (var pi in pinfos)
{
if (pi.CanWrite)
{
try
{
pi.SetValue(myDuplicate, pi.GetValue(this));
}
catch (Exception ex) when (!ex.IsFatal())
{
throw new SpeckleException($"Failed to set value for {type.Name}.{pi.Name}", ex);
}
}
}
return myDuplicate;
}
/// <inheritdoc />
/// <summary>
/// Gets properties via the dot syntax.
@@ -232,7 +270,7 @@ public class DynamicBase : DynamicObject, IDynamicMetaObjectProvider
.GetBaseProperties(GetType())
.Where(x =>
{
var hasObsolete = x.IsDefined(typeof(ObsoleteAttribute), true);
var hasObsolete = TypeLoader.IsObsolete(x);
// If obsolete is false and prop has obsolete attr
// OR
@@ -1,6 +1,7 @@
using System.Text;
using Microsoft.Data.Sqlite;
using Speckle.InterfaceGenerator;
using Speckle.Sdk.Common;
using Speckle.Sdk.Dependencies;
namespace Speckle.Sdk.SQLite;
@@ -12,7 +13,26 @@ public sealed class SqLiteJsonCacheManager : ISqLiteJsonCacheManager
{
private readonly CacheDbCommandPool _pool;
public SqLiteJsonCacheManager(string path, int concurrency)
public static ISqLiteJsonCacheManager FromMemory(int concurrency) => new SqLiteJsonCacheManager(concurrency);
private SqLiteJsonCacheManager(int concurrency)
{
//disable pooling as we pool ourselves
var builder = new SqliteConnectionStringBuilder
{
Pooling = false,
DataSource = ":memory:",
Cache = SqliteCacheMode.Shared,
Mode = SqliteOpenMode.Memory,
};
_pool = new CacheDbCommandPool(builder.ToString(), concurrency);
Initialize();
}
public static ISqLiteJsonCacheManager FromFilePath(string path, int concurrency) =>
new SqLiteJsonCacheManager(path, concurrency);
private SqLiteJsonCacheManager(string path, int concurrency)
{
//disable pooling as we pool ourselves
var builder = new SqliteConnectionStringBuilder { Pooling = false, DataSource = path };
@@ -46,12 +66,6 @@ public sealed class SqLiteJsonCacheManager : ISqLiteJsonCacheManager
command.ExecuteNonQuery();
}
// Insert Optimisations
//Note / Hack: This setting has the potential to corrupt the db.
//cmd = new SqliteCommand("PRAGMA synchronous=OFF;", Connection);
//cmd.ExecuteNonQuery();
using (SqliteCommand cmd1 = new("PRAGMA count_changes=OFF;", c))
{
cmd1.ExecuteNonQuery();
@@ -120,7 +134,10 @@ public sealed class SqLiteJsonCacheManager : ISqLiteJsonCacheManager
);
//This does an insert or ignores if already exists
public void SaveObject(string id, string json) =>
public void SaveObject(string id, string json)
{
id.NotNullOrWhiteSpace();
json.NotNullOrWhiteSpace();
_pool.Use(
CacheOperation.InsertOrIgnore,
command =>
@@ -130,6 +147,7 @@ public sealed class SqLiteJsonCacheManager : ISqLiteJsonCacheManager
command.ExecuteNonQuery();
}
);
}
//This does an insert or replaces if already exists
public void UpdateObject(string id, string json) =>
@@ -148,29 +166,45 @@ public sealed class SqLiteJsonCacheManager : ISqLiteJsonCacheManager
CacheOperation.BulkInsertOrIgnore,
cmd =>
{
CreateBulkInsert(cmd, items);
return cmd.ExecuteNonQuery();
if (CreateBulkInsert(cmd, items))
{
cmd.ExecuteNonQuery();
}
}
);
private void CreateBulkInsert(SqliteCommand cmd, IEnumerable<(string id, string json)> items)
private bool CreateBulkInsert(SqliteCommand cmd, IEnumerable<(string id, string json)> items)
{
StringBuilder sb = Pools.StringBuilders.Get();
sb.AppendLine(CacheDbCommands.Commands[(int)CacheOperation.BulkInsertOrIgnore]);
int i = 0;
foreach (var (id, json) in items)
try
{
sb.Append($"(@key{i}, @value{i}),");
cmd.Parameters.AddWithValue($"@key{i}", id);
cmd.Parameters.AddWithValue($"@value{i}", json);
i++;
}
sb.Remove(sb.Length - 1, 1);
sb.Append(';');
sb.AppendLine(CacheDbCommands.Commands[(int)CacheOperation.BulkInsertOrIgnore]);
int i = 0;
foreach (var (id, json) in items)
{
sb.Append($"(@key{i}, @value{i}),");
cmd.Parameters.AddWithValue($"@key{i}", id);
cmd.Parameters.AddWithValue($"@value{i}", json);
i++;
}
if (i == 0)
{
return false;
}
sb.Remove(sb.Length - 1, 1);
sb.Append(';');
#pragma warning disable CA2100
cmd.CommandText = sb.ToString();
cmd.CommandText = sb.ToString();
#pragma warning restore CA2100
Pools.StringBuilders.Return(sb);
}
finally
{
Pools.StringBuilders.Return(sb);
}
return true;
}
public bool HasObject(string objectId) =>
@@ -1,19 +1,20 @@
using Speckle.InterfaceGenerator;
using Speckle.Sdk.Caching;
using Speckle.Sdk.Logging;
using Speckle.Sdk.Serialisation.Utilities;
namespace Speckle.Sdk.SQLite;
[GenerateAutoInterface]
public class SqLiteJsonCacheManagerFactory : ISqLiteJsonCacheManagerFactory
public class SqLiteJsonCacheManagerFactory(IModelCacheManager modelCacheManager) : ISqLiteJsonCacheManagerFactory
{
public const int INITIAL_CONCURRENCY = 4;
private ISqLiteJsonCacheManager Create(string path, int concurrency) => new SqLiteJsonCacheManager(path, concurrency);
private ISqLiteJsonCacheManager Create(string path, int concurrency) =>
SqLiteJsonCacheManager.FromFilePath(path, concurrency);
public ISqLiteJsonCacheManager CreateForUser(string scope) =>
Create(Path.Combine(SpecklePathProvider.UserApplicationDataPath(), "Speckle", $"{scope}.db"), 1);
public ISqLiteJsonCacheManager CreateFromStream(string streamId) =>
Create(SqlitePaths.GetDBPath(streamId), INITIAL_CONCURRENCY);
Create(modelCacheManager.GetStreamPath(streamId), INITIAL_CONCURRENCY);
}
+3 -3
View File
@@ -1,5 +1,5 @@
using System.Diagnostics.Contracts;
using Speckle.Sdk.Helpers;
using Speckle.Sdk.Common;
using Speckle.Sdk.Models;
namespace Speckle.Sdk.Serialisation;
@@ -10,9 +10,9 @@ public static class IdGenerator
public static Id ComputeId(Json serialized)
{
#if NET6_0_OR_GREATER
string hash = Crypt.Sha256(serialized.Value.AsSpan(), length: HashUtility.HASH_LENGTH);
string hash = Sha256.GetString(serialized.Value.AsSpan(), length: HashUtility.HASH_LENGTH);
#else
string hash = Crypt.Sha256(serialized.Value, length: HashUtility.HASH_LENGTH);
string hash = Sha256.GetString(serialized.Value, length: HashUtility.HASH_LENGTH);
#endif
return new Id(hash);
}
@@ -2,13 +2,25 @@
public readonly record struct SerializationResult(Json Json, Id? Id);
public readonly record struct Json(string Value)
public readonly record struct Json
{
public Json(string json)
{
Value = json ?? throw new ArgumentNullException(nameof(json));
}
public override string ToString() => Value;
public string Value { get; }
}
public readonly record struct Id(string Value)
public readonly record struct Id
{
public Id(string id)
{
Value = id ?? throw new ArgumentNullException(nameof(id));
}
public override string ToString() => Value;
public bool Equals(Id? other)
@@ -22,4 +34,6 @@ public readonly record struct Id(string Value)
}
public override int GetHashCode() => Value.GetHashCode();
public string Value { get; }
}
@@ -5,7 +5,10 @@ namespace Speckle.Sdk.Serialisation.Utilities;
public static class ClosureParser
{
public static IReadOnlyList<(string, int)> GetClosures(string json, CancellationToken cancellationToken)
public static IReadOnlyList<(string, int)> GetClosures(string json, CancellationToken cancellationToken) =>
GetClosuresPrivate(json, cancellationToken);
private static List<(string, int)> GetClosuresPrivate(string json, CancellationToken cancellationToken)
{
try
{
@@ -31,10 +34,17 @@ public static class ClosureParser
return [];
}
public static IReadOnlyList<(string, int)> GetClosuresSorted(string json, CancellationToken cancellationToken)
{
var closures = GetClosuresPrivate(json, cancellationToken);
closures.Sort((a, b) => b.Item2.CompareTo(a.Item2));
return closures;
}
public static IEnumerable<string> GetChildrenIds(string json, CancellationToken cancellationToken) =>
GetClosures(json, cancellationToken).Select(x => x.Item1);
private static IReadOnlyList<(string, int)> ReadObject(JsonTextReader reader, CancellationToken cancellationToken)
private static List<(string, int)> ReadObject(JsonTextReader reader, CancellationToken cancellationToken)
{
reader.Read();
while (reader.TokenType != JsonToken.EndObject)
@@ -1,30 +0,0 @@
using Speckle.Sdk.Logging;
using Speckle.Sdk.Transports;
namespace Speckle.Sdk.Serialisation.Utilities;
public static class SqlitePaths
{
private const string APPLICATION_NAME = "Speckle";
private const string DATA_FOLDER = "Projects";
private static readonly string basePath = SpecklePathProvider.UserApplicationDataPath();
public static string BlobStorageFolder =>
SpecklePathProvider.BlobStoragePath(Path.Combine(basePath, APPLICATION_NAME));
public static string GetDBPath(string streamId)
{
var dir = Path.Combine(basePath, APPLICATION_NAME, DATA_FOLDER);
var db = Path.Combine(dir, $"{streamId}.db");
try
{
Directory.CreateDirectory(dir); //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);
}
}
}
@@ -7,6 +7,8 @@ using Speckle.Sdk.Transports;
namespace Speckle.Sdk.Serialisation.V2;
/// <seealso cref="DeserializeProcessFactoryNoCache"/>
/// <seealso cref="DeserializeProcess"/>
[GenerateAutoInterface]
public class DeserializeProcessFactory(
IBaseDeserializer baseDeserializer,
@@ -0,0 +1,60 @@
using System.Collections.Concurrent;
using Microsoft.Extensions.Logging;
using Speckle.InterfaceGenerator;
using Speckle.Sdk.Serialisation.V2.Receive;
using Speckle.Sdk.Transports;
namespace Speckle.Sdk.Serialisation.V2;
/// <summary>
/// A version of <seealso cref="DeserializeProcessFactory"/> but without any SQLite usage.
/// This class doesn't have a matching <seealso cref="GenerateAutoInterfaceAttribute"/>, so will not be registered by <seealso cref="ServiceRegistration"/> automatically
/// Instead consumers can register this to override the default <seealso cref="DeserializeProcessFactory"/>
/// </summary>
/// <seealso cref="DeserializeProcessFactory"/>
/// <seealso cref="DeserializeProcess"/>
public sealed class DeserializeProcessFactoryNoCache(
IBaseDeserializer baseDeserializer,
IServerObjectManagerFactory serverObjectManagerFactory,
ILoggerFactory loggerFactory
) : IDeserializeProcessFactory
{
public IDeserializeProcess CreateDeserializeProcess(
Uri url,
string streamId,
string? authorizationToken,
IProgress<ProgressArgs>? progress,
CancellationToken cancellationToken,
DeserializeProcessOptions? options = null
)
{
var sqLiteJsonCacheManager = new MemoryJsonCacheManager(new());
var serverObjectManager = serverObjectManagerFactory.Create(url, streamId, authorizationToken);
return new DeserializeProcess(
sqLiteJsonCacheManager,
serverObjectManager,
progress,
baseDeserializer,
loggerFactory,
cancellationToken,
options
);
}
public IDeserializeProcess CreateDeserializeProcess(
ConcurrentDictionary<Id, Json> jsonCache,
ConcurrentDictionary<string, string> objects,
IProgress<ProgressArgs>? progress,
CancellationToken cancellationToken,
DeserializeProcessOptions? options = null
) =>
new DeserializeProcess(
new MemoryJsonCacheManager(jsonCache),
new MemoryServerObjectManager(objects),
progress,
baseDeserializer,
loggerFactory,
cancellationToken,
options
);
}
@@ -41,7 +41,13 @@ public sealed class DeserializeProcess(
:
#pragma warning disable CA2000
this(
new ObjectLoader(sqLiteJsonCacheManager, serverObjectManager, progress, cancellationToken),
new ObjectLoader(
sqLiteJsonCacheManager,
serverObjectManager,
progress,
loggerFactory.CreateLogger<ObjectLoader>(),
cancellationToken
),
progress,
baseDeserializer,
loggerFactory,

Some files were not shown because too many files have changed in this diff Show More