Compare commits

...

10 Commits

Author SHA1 Message Date
Jedd Morgan c517dead03 Internal 2026-03-24 10:24:43 +00:00
Jedd Morgan 2b61ab7d2e fallback mechanism 2026-03-24 10:04:44 +00:00
Jedd Morgan 4b319499c3 Fix mistake 2026-03-23 16:10:44 +00:00
Jedd Morgan d4055c6ff1 Avoid deprecated fields 2026-03-23 16:03:20 +00:00
Jedd Morgan af0fc9f669 Merge pull request #439 from specklesystems/duckdev
.NET Build and Publish / build (push) Has been cancelled
feat(packfile): duckdb packfile api (duckdev -> main)
2026-03-23 11:08:57 +00:00
Jedd Morgan edbc884d74 Clean diff 2026-03-23 10:45:57 +00:00
Jedd Morgan 025d7f70ba Merge branch 'main' into duckdev 2026-03-23 10:41:22 +00:00
Jedd Morgan 70acc06f37 feat(otel): Change sig for remote (#460)
.NET Build and Publish / build (push) Has been cancelled
* Remote changes

* Fix tests

* Fix tests

* ditto
2026-03-16 15:32:33 +00:00
Jedd Morgan a2c99a537a feat(otel): Add proper support for remote spans (#458)
.NET Build and Publish / build (push) Has been cancelled
* Add proper support for remote spans

* Fix tests
2026-03-12 12:52:10 +00:00
dependabot[bot] abf86eda03 chore(deps): bump docker/login-action from 3 to 4 (#456)
Bumps [docker/login-action](https://github.com/docker/login-action) from 3 to 4.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-10 09:50:34 +00:00
15 changed files with 141 additions and 68 deletions
+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
- name: 🔐 Login to Github Container Registry - name: 🔐 Login to Github Container Registry
if: ${{ inputs.use-internal-image }} if: ${{ inputs.use-internal-image }}
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
registry: "ghcr.io" registry: "ghcr.io"
username: ${{ github.actor }} username: ${{ github.actor }}
@@ -5,6 +5,7 @@ public interface ISdkActivity : IDisposable
void SetTag(string key, object? value); void SetTag(string key, object? value);
void RecordException(Exception e); void RecordException(Exception e);
string TraceId { get; } string TraceId { get; }
string SpanId { get; }
void SetStatus(SdkActivityStatusCode code); void SetStatus(SdkActivityStatusCode code);
void InjectHeaders(Action<string, string> header); void InjectHeaders(Action<string, string> header);
@@ -1,12 +1,20 @@
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using Speckle.Connectors.Logging;
namespace Speckle.Sdk.Logging; namespace Speckle.Sdk.Logging;
public interface ISdkActivityFactory : IDisposable public interface ISdkActivityFactory : IDisposable
{ {
/// <param name="name"></param> ISdkActivity? Start(
/// <param name="source"></param> string? name = null,
/// <param name="parentId">Only need to set if the parent is coming from an external source (e.g.to trace between client and server)</param> SdkActivityKind kind = SdkActivityKind.Internal,
/// <returns></returns> [CallerMemberName] string source = ""
ISdkActivity? Start(string? name = default, [CallerMemberName] string source = "", string? parentId = null); );
ISdkActivity? StartRemote(
string traceContext,
SdkActivityKind kind,
string? name = null,
[CallerMemberName] string source = ""
);
} }
@@ -0,0 +1,30 @@
namespace Speckle.Connectors.Logging;
public enum SdkActivityKind
{
/// <summary>
/// Default value.
/// Indicates that the Activity represents an internal operation within an application, as opposed to an operations with remote parents or children.
/// </summary>
Internal = 0,
/// <summary>
/// Server activity represents request incoming from external component.
/// </summary>
Server = 1,
/// <summary>
/// Client activity represents outgoing request to the external component.
/// </summary>
Client = 2,
/// <summary>
/// Producer activity represents output provided to external components.
/// </summary>
Producer = 3,
/// <summary>
/// Consumer activity represents output received from an external component.
/// </summary>
Consumer = 4,
}
@@ -1,3 +1,5 @@
using Speckle.Newtonsoft.Json;
namespace Speckle.Sdk.Api.GraphQL.Models; namespace Speckle.Sdk.Api.GraphQL.Models;
public class LimitedWorkspace public class LimitedWorkspace
@@ -6,8 +8,12 @@ public class LimitedWorkspace
public string name { get; init; } public string name { get; init; }
public string? role { get; init; } public string? role { get; init; }
public string slug { get; init; } public string slug { get; init; }
public string? logo { get; init; } public string? logoUri { get; init; }
public string? description { get; init; } public string? description { get; init; }
[JsonIgnore]
[Obsolete($"Deprecated, use {nameof(logoUri)} instead", true)]
public string? logo { get; init; }
} }
public class Workspace : LimitedWorkspace public class Workspace : LimitedWorkspace
@@ -16,9 +22,13 @@ public class Workspace : LimitedWorkspace
public DateTime updatedAt { get; init; } public DateTime updatedAt { get; init; }
public bool readOnly { get; init; } public bool readOnly { get; init; }
public WorkspacePermissionChecks permissions { get; init; } public WorkspacePermissionChecks permissions { get; init; }
[JsonIgnore]
[Obsolete("Workspaces no longer have creation state, is always created true", true)]
public WorkspaceCreationState? creationState { get; init; } public WorkspaceCreationState? creationState { get; init; }
} }
[Obsolete("Workspaces no longer have creation state, is always created true")]
public sealed class WorkspaceCreationState public sealed class WorkspaceCreationState
{ {
public bool completed { get; init; } public bool completed { get; init; }
@@ -264,15 +264,11 @@ public sealed class ActiveUserResource
name name
role role
slug slug
logo logoUrl
createdAt createdAt
updatedAt updatedAt
readOnly readOnly
description description
creationState
{
completed
}
permissions { permissions {
canCreateProject { canCreateProject {
authorized authorized
@@ -317,7 +313,7 @@ public sealed class ActiveUserResource
/// <remarks>note this returns a <see cref="LimitedWorkspace"/>, because it may be a workspace the user is not a member of</remarks> /// <remarks>note this returns a <see cref="LimitedWorkspace"/>, because it may be a workspace the user is not a member of</remarks>
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/> /// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
/// <exception cref="SpeckleException">The ActiveUser could not be found (e.g. the client is not authenticated)</exception> /// <exception cref="SpeckleException">The ActiveUser could not be found (e.g. the client is not authenticated)</exception>
public async Task<LimitedWorkspace?> GetActiveWorkspace(CancellationToken cancellationToken = default) private async Task<LimitedWorkspace?> GetActiveWorkspace_Legacy(CancellationToken cancellationToken = default)
{ {
//language=graphql //language=graphql
const string QUERY = """ const string QUERY = """
@@ -328,7 +324,6 @@ public sealed class ActiveUserResource
name name
role role
slug slug
logo
description description
} }
} }
@@ -349,6 +344,47 @@ public sealed class ActiveUserResource
return response.data.data; return response.data.data;
} }
public async Task<LimitedWorkspace?> GetActiveWorkspace(CancellationToken cancellationToken = default)
{
//language=graphql
const string QUERY = """
query ActiveUser {
data:activeUser {
data:activeWorkspace {
id
name
role
slug
logoUrl
description
}
}
}
""";
var request = new GraphQLRequest { Query = QUERY };
NullableResponse<NullableResponse<LimitedWorkspace?>?> response;
try
{
response = await _client
.ExecuteGraphQLRequest<NullableResponse<NullableResponse<LimitedWorkspace?>?>>(request, cancellationToken)
.ConfigureAwait(false);
}
catch (SpeckleGraphQLInvalidQueryException)
{
//v2.x.x servers do not have a logoUrl property
return await GetActiveWorkspace_Legacy(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="limit">Max number of projects to fetch</param>
/// <param name="cursor">Optional cursor for pagination</param> /// <param name="cursor">Optional cursor for pagination</param>
/// <param name="filter">Optional filter</param> /// <param name="filter">Optional filter</param>
@@ -52,7 +52,6 @@ public sealed class OtherUserResource
/// <param name="query">String to search for. Must be at least 3 characters</param> /// <param name="query">String to search for. Must be at least 3 characters</param>
/// <param name="limit">Max number of users to fetch</param> /// <param name="limit">Max number of users to fetch</param>
/// <param name="cursor">Optional cursor for pagination</param> /// <param name="cursor">Optional cursor for pagination</param>
/// <param name="archived"></param>
/// <param name="emailOnly"></param> /// <param name="emailOnly"></param>
/// <param name="cancellationToken"></param> /// <param name="cancellationToken"></param>
/// <returns></returns> /// <returns></returns>
@@ -61,26 +60,25 @@ public sealed class OtherUserResource
string query, string query,
int limit = ServerLimits.DEFAULT_PAGINATION_REQUEST, int limit = ServerLimits.DEFAULT_PAGINATION_REQUEST,
string? cursor = null, string? cursor = null,
bool archived = false,
bool emailOnly = false, bool emailOnly = false,
CancellationToken cancellationToken = default CancellationToken cancellationToken = default
) )
{ {
//language=graphql //language=graphql
const string QUERY = """ const string QUERY = """
query UserSearch($query: String!, $limit: Int!, $cursor: String, $archived: Boolean, $emailOnly: Boolean) { query Users($input: UsersRetrievalInput!) {
data:userSearch(query: $query, limit: $limit, cursor: $cursor, archived: $archived, emailOnly: $emailOnly) { data:users(input: $input) {
cursor cursor
items { items {
id id
name name
bio bio
company company
avatar avatar
verified verified
role role
} }
} }
} }
"""; """;
@@ -89,11 +87,13 @@ public sealed class OtherUserResource
Query = QUERY, Query = QUERY,
Variables = new Variables = new
{ {
query, input = new
limit, {
cursor, query,
archived, limit,
emailOnly, emailOnly,
cursor,
},
}, },
}; };
@@ -76,6 +76,7 @@ public sealed class SubscriptionResource : IDisposable
/// <summary>Subscribe to updates to resource comments/threads. Optionally specify resource ID string to only receive updates regarding comments for those resources</summary> /// <summary>Subscribe to updates to resource comments/threads. Optionally specify resource ID string to only receive updates regarding comments for those resources</summary>
/// <remarks><inheritdoc cref="CreateUserProjectsUpdatedSubscription"/></remarks> /// <remarks><inheritdoc cref="CreateUserProjectsUpdatedSubscription"/></remarks>
/// <inheritdoc cref="ISpeckleGraphQLClient.SubscribeTo{T}"/> /// <inheritdoc cref="ISpeckleGraphQLClient.SubscribeTo{T}"/>
[Obsolete("Comments are now issues, and we've not update SDKs with the new subs")]
public Subscription<ProjectCommentsUpdatedMessage> CreateProjectCommentsUpdatedSubscription( public Subscription<ProjectCommentsUpdatedMessage> CreateProjectCommentsUpdatedSubscription(
ViewerUpdateTrackingTarget target ViewerUpdateTrackingTarget target
) )
@@ -28,15 +28,11 @@ public sealed class WorkspaceResource
name name
role role
slug slug
logo logoUrl
createdAt createdAt
updatedAt updatedAt
readOnly readOnly
description description
creationState
{
completed
}
permissions { permissions {
canCreateProject { canCreateProject {
authorized authorized
@@ -1,8 +1,12 @@
namespace Speckle.Sdk.Logging; using Speckle.Connectors.Logging;
namespace Speckle.Sdk.Logging;
public sealed class NullActivityFactory : ISdkActivityFactory public sealed class NullActivityFactory : ISdkActivityFactory
{ {
public void Dispose() { } public void Dispose() { }
public ISdkActivity? Start(string? name = default, string source = "", string? parentId = null) => null; public ISdkActivity? Start(string? name, SdkActivityKind kind, string source) => null;
public ISdkActivity? StartRemote(string traceContext, SdkActivityKind kind, string? name, string source) => null;
} }
@@ -7,32 +7,33 @@ namespace Speckle.Sdk.Models;
public enum DynamicBaseMemberType public enum DynamicBaseMemberType
{ {
/// <summary> /// <summary>
/// The typed members of the DynamicBase object /// The typed members of the <see cref="DynamicBase"/> object
/// </summary> /// </summary>
Instance = 1, Instance = 1,
/// <summary> /// <summary>
/// The dynamically added members of the DynamicBase object /// The dynamically added members of the <see cref="DynamicBase"/> object
/// </summary> /// </summary>
Dynamic = 2, Dynamic = 2,
/// <summary> /// <summary>
/// The typed members flagged with ObsoleteAttribute attribute. /// The typed members flagged with <see cref="ObsoleteAttribute"/> attribute.
/// </summary> /// </summary>
Obsolete = 4, Obsolete = 4,
/// <summary> /// <summary>
/// The typed methods flagged with TODO: /// Old feature supported in v2 for grasshopper
/// </summary> /// </summary>
[Obsolete("Feature no longer supported")]
SchemaComputed = 16, SchemaComputed = 16,
/// <summary> /// <summary>
/// All the typed members, including ones with ObsoleteAttribute attributes. /// All the typed members, including ones with <see cref="ObsoleteAttribute"/> attributes.
/// </summary> /// </summary>
InstanceAll = Instance + Obsolete, InstanceAll = Instance + Obsolete,
/// <summary> /// <summary>
/// All the members, including dynamic and instance members flagged with ObsoleteAttribute attributes /// All the members, including dynamic and instance members flagged with <see cref="ObsoleteAttribute"/> attributes
/// </summary> /// </summary>
All = InstanceAll + Dynamic, All = InstanceAll + Dynamic,
} }
@@ -52,7 +52,7 @@ public class ServerObjectManagerTests : MoqTest
http.Setup(x => x.CreateHttpClient(It.IsAny<HttpClientHandler>(), timeout, token)).Returns(httpClient); http.Setup(x => x.CreateHttpClient(It.IsAny<HttpClientHandler>(), timeout, token)).Returns(httpClient);
var activityFactory = Create<ISdkActivityFactory>(); var activityFactory = Create<ISdkActivityFactory>();
activityFactory.Setup(x => x.Start(null, "DownloadObjects", null)).Returns((ISdkActivity?)null); activityFactory.Setup(x => x.Start(null, default, "DownloadObjects")).Returns((ISdkActivity?)null);
var serverObjectManager = new ServerObjectManager( var serverObjectManager = new ServerObjectManager(
http.Object, http.Object,
@@ -91,7 +91,7 @@ public class ServerObjectManagerTests : MoqTest
http.Setup(x => x.CreateHttpClient(It.IsAny<HttpClientHandler>(), timeout, token)).Returns(httpClient); http.Setup(x => x.CreateHttpClient(It.IsAny<HttpClientHandler>(), timeout, token)).Returns(httpClient);
var activityFactory = Create<ISdkActivityFactory>(); var activityFactory = Create<ISdkActivityFactory>();
activityFactory.Setup(x => x.Start(null, "DownloadSingleObject", null)).Returns((ISdkActivity?)null); activityFactory.Setup(x => x.Start(null, default, "DownloadSingleObject")).Returns((ISdkActivity?)null);
var serverObjectManager = new ServerObjectManager( var serverObjectManager = new ServerObjectManager(
http.Object, http.Object,
@@ -132,7 +132,7 @@ public class ServerObjectManagerTests : MoqTest
http.Setup(x => x.CreateHttpClient(It.IsAny<HttpClientHandler>(), timeout, token)).Returns(httpClient); http.Setup(x => x.CreateHttpClient(It.IsAny<HttpClientHandler>(), timeout, token)).Returns(httpClient);
var activityFactory = Create<ISdkActivityFactory>(); var activityFactory = Create<ISdkActivityFactory>();
activityFactory.Setup(x => x.Start(null, "HasObjects", null)).Returns((ISdkActivity?)null); activityFactory.Setup(x => x.Start(null, default, "HasObjects")).Returns((ISdkActivity?)null);
var serverObjectManager = new ServerObjectManager( var serverObjectManager = new ServerObjectManager(
http.Object, http.Object,
@@ -171,7 +171,7 @@ public class ServerObjectManagerTests : MoqTest
http.Setup(x => x.CreateHttpClient(It.IsAny<HttpClientHandler>(), timeout, token)).Returns(httpClient); http.Setup(x => x.CreateHttpClient(It.IsAny<HttpClientHandler>(), timeout, token)).Returns(httpClient);
var activityFactory = Create<ISdkActivityFactory>(); var activityFactory = Create<ISdkActivityFactory>();
activityFactory.Setup(x => x.Start(null, "UploadObjects", null)).Returns((ISdkActivity?)null); activityFactory.Setup(x => x.Start(null, default, "UploadObjects")).Returns((ISdkActivity?)null);
var serverObjectManager = new ServerObjectManager( var serverObjectManager = new ServerObjectManager(
http.Object, http.Object,
@@ -1,8 +0,0 @@
{
"Type": "AggregateException",
"InnerException": {
"Data": {},
"Message": "FORBIDDEN: Your auth token does not have the required scope: workspace:read.",
"Type": "SpeckleGraphQLForbiddenException"
}
}
@@ -1,8 +0,0 @@
{
"Type": "AggregateException",
"InnerException": {
"Data": {},
"Message": "FORBIDDEN: Your auth token does not have the required scope: workspace:read.",
"Type": "SpeckleGraphQLForbiddenException"
}
}
@@ -21,17 +21,19 @@ public class WorkspaceResourceTests
return testUser; return testUser;
} }
[Fact] [Fact, Trait("Server", "Internal")]
public async Task TestGetWorkspace() public async Task TestGetWorkspace()
{ {
var ex = await Assert.ThrowsAsync<AggregateException>(async () => _ = await Sut.Get("non-existent-id")); var ex = await Assert.ThrowsAsync<AggregateException>(async () => _ = await Sut.Get("non-existent-id"));
await Verify(ex); Assert.Single(ex.InnerExceptions);
Assert.All(ex.InnerExceptions, item => Assert.IsType<SpeckleGraphQLForbiddenException>(item));
} }
[Fact] [Fact]
public async Task TestGetProjects() public async Task TestGetProjects()
{ {
var ex = await Assert.ThrowsAsync<AggregateException>(async () => _ = await Sut.GetProjects("non-existent-id")); var ex = await Assert.ThrowsAsync<AggregateException>(async () => _ = await Sut.GetProjects("non-existent-id"));
await Verify(ex); Assert.Single(ex.InnerExceptions);
Assert.All(ex.InnerExceptions, item => Assert.IsType<SpeckleGraphQLForbiddenException>(item));
} }
} }