diff --git a/Makefile b/Makefile index a920a74c9..57cbebee5 100644 --- a/Makefile +++ b/Makefile @@ -808,7 +808,7 @@ tools: # Spec-shape lints #------------------------------------------------------------------------------ -.PHONY: check-bucket-flat-parity validate-api-gaps check-deprecation-parity kt-check-optional-arrays-and-scalars check-fixture-coverage check-idempotency-parity +.PHONY: check-bucket-flat-parity validate-api-gaps check-deprecation-parity kt-check-optional-arrays-and-scalars check-fixture-coverage check-idempotency-parity check-retry-metadata-parity # Verify every bucket-scoped GET list operation has a flat-path counterpart # (or is justified in spec/bucket-scoped-allowlist.txt). Cross-project SDK @@ -854,6 +854,9 @@ kt-check-optional-arrays-and-scalars: check-idempotency-parity: @./scripts/check-idempotency-parity +check-retry-metadata-parity: + @python3 ./scripts/check-retry-metadata-parity.py + #------------------------------------------------------------------------------ # Combined targets #------------------------------------------------------------------------------ @@ -876,7 +879,7 @@ generate: @echo "==> Generation complete" # Run all checks (Smithy + Go + TypeScript + Ruby + Kotlin + Swift + Python + Behavior Model + Conformance + Provenance + Actions lint) -check: lint-actions sync-spec-version-check smithy-check behavior-model-check provenance-check sync-api-version-check url-routes-check go-check-drift go-check-wrapper-drift go-check-generated-drift auth-routable-check kt-check-drift swift-check-drift go-check ts-check rb-check kt-check swift-check py-check conformance check-bucket-flat-parity validate-api-gaps check-deprecation-parity check-fixture-coverage kt-check-optional-arrays-and-scalars check-idempotency-parity +check: lint-actions sync-spec-version-check smithy-check behavior-model-check provenance-check sync-api-version-check url-routes-check go-check-drift go-check-wrapper-drift go-check-generated-drift auth-routable-check kt-check-drift swift-check-drift go-check ts-check rb-check kt-check swift-check py-check conformance check-bucket-flat-parity validate-api-gaps check-deprecation-parity check-fixture-coverage kt-check-optional-arrays-and-scalars check-idempotency-parity check-retry-metadata-parity @echo "==> All checks passed" # Clean all build artifacts diff --git a/SPEC.md b/SPEC.md index 1e499604f..dbb53270c 100644 --- a/SPEC.md +++ b/SPEC.md @@ -108,6 +108,12 @@ END **Naming note:** `max_retries` means total attempts (including the initial request), not the number of retries after the first attempt. With `max_retries = 3`, the transport makes at most 3 attempts total (1 initial + 2 retries). This name is inherited from the shipping Ruby SDK; the behavior-model.json uses `retry.max` with identical semantics. +**Per-operation retry ceiling.** Each operation carries a per-op `retry.max` in behavior-model.json (183 ops at `3`, 43 at `2`). **TypeScript and Swift** drive their retry loops directly from this per-op value, which is unambiguous there because neither exposes a numeric client-wide cap — only an on/off (`enableRetry`). **Kotlin does expose one** (`BasecampConfig.maxRetries`), but resolves the two as `opRetry?.maxRetries ?: config.maxRetries`, so the per-op value *overrides* the client's rather than bounding it: a Kotlin caller who lowers `maxRetries` to `1` still gets the operation's count. That is a divergence from this section, not a sanctioned variation — tracked in #485. Generated Go and Python expose a numeric client cap (`RetryConfig.MaxRetries` / `config.max_retries`) *and* honor the per-op value as a **ceiling**: `effective_attempts = min(client_cap, op_max)`. The ceiling can only reduce attempts below the client cap, never raise them, so a client that lowered its cap (e.g. to `0`/`1` to disable/restrict retries) is still honored. Because every op's `max` is ≤ the default cap of `3`, a default or raised Go/Python client makes exactly the per-op number of attempts — matching TS/Swift/Kotlin. Observable changes from the former client-wide behavior, by client configuration: + +- **Default client (`max_retries = 3`):** only the **9 idempotent `max:2` operations** (account/gauge/preference writes: `UpdateAccountName`, `UpdateAccountLogo`, `RemoveAccountLogo`, `UpdateMyPreferences`, `DisableOutOfOffice`, `MarkAsRead`, `ToggleGauge`, `UpdateGaugeNeedle`, `DestroyGaugeNeedle`) change — they now retry at most twice instead of three times. The other 178 retry-eligible ops are unaffected (`min(3, 3) = 3`). +- **Client that raised its cap above 3:** **all 187 retry-eligible operations** are now clamped to their per-op `max` (178 to `3`, 9 to `2`) instead of retrying up to the raised cap. This is the intended meaning of a per-op ceiling and brings Go/Python into line with TS/Swift/Kotlin, which never retry beyond the per-op `max`. (Go and Python are, however, the only two that also honor a caller who wants *fewer* attempts than the operation declares — see the Kotlin note above.) +- **Client that lowered its cap to `0`/`1`:** unchanged — the cap still wins (`min(cap, op_max) = cap`). Go/Python consume only `max`; the emitted `base_delay_ms`/`backoff`/`retry_on` remain inert per-op metadata for them (retained for parity — see `scripts/check-retry-metadata-parity.py`). Ruby retries GET only and bounds its loop by `config.max_retries` alone, so the per-op `max` is not enforced there at all — the opposite gap from Kotlin's. All per-op retry fields are inert in Ruby. + **Recommended default:** A connect timeout of 10 seconds is recommended but not a required config field. Only Ruby exposes this (Faraday `open_timeout = 10`); other SDKs use their HTTP library's default. ### Environment Variable Mapping (optional convention) diff --git a/go/pkg/basecamp/generated_perop_retry_test.go b/go/pkg/basecamp/generated_perop_retry_test.go new file mode 100644 index 000000000..bbee3b3bb --- /dev/null +++ b/go/pkg/basecamp/generated_perop_retry_test.go @@ -0,0 +1,103 @@ +package basecamp + +import ( + "context" + "net/http" + "net/http/httptest" + "sync/atomic" + "testing" + + "github.com/basecamp/basecamp-sdk/go/pkg/generated" +) + +// These tests pin the generated client's per-operation retry ceiling. The +// x-basecamp-retry.maxAttempts value for each operation is emitted into the +// operationRetryMax map (read via GetOperationRetryMax), and doWithRetry applies +// it as a ceiling (upper bound on attempts): effective attempts = +// min(client cap, op retry max). This +// matches how TS/Swift/ +// Kotlin drive their retry loops directly from the per-op max, while still +// honoring a Go client that lowered its cap. DisableOutOfOffice carries +// RetryMax:2 (an idempotent account write); CompleteTodo carries RetryMax:3. +// They stay outside pkg/generated per the repo rule. + +func TestGeneratedPerOpRetryMax_CeilingBindsBelowClientCap(t *testing.T) { + cases := []struct { + name string + operation string + clientCap int + wantAttempts int32 + }{ + // DisableOutOfOffice has RetryMax:2. With the default cap (3) the op + // ceiling binds: min(3, 2) == 2, not the client-wide 3. + {"op ceiling below default cap", "DisableOutOfOffice", 3, 2}, + // Raising the client cap cannot exceed the op ceiling: min(5, 2) == 2. + {"op ceiling below raised cap", "DisableOutOfOffice", 5, 2}, + // Lowering the client cap below the op ceiling is honored: min(1, 2) == 1. + {"client cap below op ceiling", "DisableOutOfOffice", 1, 1}, + // CompleteTodo has RetryMax:3, equal to the default cap: min(3, 3) == 3 + // (control — unchanged behavior). + {"op ceiling equals cap", "CompleteTodo", 3, 3}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + var attempts int32 + server := httptest.NewServer(always503(&attempts)) + defer server.Close() + + client, err := generated.NewClient(server.URL, generated.WithRetryConfig(fastRetryConfig(tc.clientCap))) + if err != nil { + t.Fatalf("NewClient: %v", err) + } + + resp := mustCallIdempotent(t, client, tc.operation) + if resp != nil { + _ = resp.Body.Close() + } + + if got := atomic.LoadInt32(&attempts); got != tc.wantAttempts { + t.Errorf("%s with client cap %d made %d attempts, want %d (effective = min(cap, RetryMax))", + tc.operation, tc.clientCap, got, tc.wantAttempts) + } + }) + } +} + +// TestGeneratedPerOpRetryMax_MetadataMatchesModel is a lightweight guard that +// the emitted per-op retry max is present and positive for the ops under test, +// so the ceiling test above is exercising real per-op data rather than a zero +// default. +func TestGeneratedPerOpRetryMax_MetadataPresent(t *testing.T) { + for op, want := range map[string]int{"DisableOutOfOffice": 2, "CompleteTodo": 3} { + got, ok := generated.GetOperationRetryMax(op) + if !ok { + t.Fatalf("no retry max for %s", op) + } + if got != want { + t.Errorf("%s retry max = %d, want %d", op, got, want) + } + } +} + +// mustCallIdempotent invokes an idempotent no-body operation by name so the +// ceiling test can parameterize over operations. +func mustCallIdempotent(t *testing.T, client *generated.Client, operation string) *http.Response { + t.Helper() + switch operation { + case "DisableOutOfOffice": + r, err := client.DisableOutOfOffice(context.Background(), "99999", 100) + if err != nil { + t.Fatalf("DisableOutOfOffice: %v", err) + } + return r + case "CompleteTodo": + r, err := client.CompleteTodo(context.Background(), "99999", 100) + if err != nil { + t.Fatalf("CompleteTodo: %v", err) + } + return r + default: + t.Fatalf("unknown operation %q", operation) + return nil + } +} diff --git a/go/pkg/generated/client.gen.go b/go/pkg/generated/client.gen.go index fbce82be0..a9e62cdee 100644 --- a/go/pkg/generated/client.gen.go +++ b/go/pkg/generated/client.gen.go @@ -3986,6 +3986,13 @@ func (c *Client) doWithRetry(ctx context.Context, buildRequest func() (*http.Req if !isIdempotent { maxAttempts = 1 } + // Apply the per-operation retry ceiling from x-basecamp-retry as an upper + // bound on attempts: effective attempts = min(client cap, op ceiling). The + // ceiling can only reduce attempts below the client cap, never raise them, so + // a client that lowered its cap (e.g. to disable retries) is still honored. + if opMax, ok := operationRetryMax[operationId]; ok && opMax > 0 && opMax < maxAttempts { + maxAttempts = opMax + } var lastResp *http.Response var lastErr error @@ -19798,6 +19805,249 @@ var operationMetadata = map[string]OperationMetadata{ "UpdateWebhook": {Idempotent: true, HasSensitiveParams: false}, } +// operationRetryMax maps operation IDs to their per-operation maximum TOTAL +// attempt count from the x-basecamp-retry extension. doWithRetry applies it as a +// per-operation ceiling: effective attempts = min(client cap, retry max). It is +// kept as a separate map (rather than a field on OperationMetadata) so wiring it +// up does not change that exported struct's shape, which would break external +// unkeyed composite literals. +var operationRetryMax = map[string]int{ + "GetAccount": 3, + "RemoveAccountLogo": 2, + "UpdateAccountLogo": 2, + "UpdateAccountName": 2, + "CreateAttachment": 3, + "GetEverythingBoosts": 3, + "DeleteBoost": 3, + "GetBoost": 3, + "SetCardColumnColor": 3, + "DisableCardColumnOnHold": 3, + "EnableCardColumnOnHold": 2, + "DeleteWormhole": 3, + "UpdateWormhole": 3, + "CreateWormhole": 2, + "ListMessageTypes": 3, + "CreateMessageType": 2, + "DeleteMessageType": 3, + "GetMessageType": 3, + "UpdateMessageType": 3, + "CreateTool": 2, + "ListWebhooks": 3, + "CreateWebhook": 2, + "GetCard": 3, + "UpdateCard": 3, + "MoveCard": 2, + "RepositionCardStep": 2, + "CreateCardStep": 2, + "GetCardColumn": 3, + "UpdateCardColumn": 3, + "ListCards": 3, + "CreateCard": 2, + "UnsubscribeFromCardColumn": 3, + "SubscribeToCardColumn": 3, + "GetCardStep": 3, + "UpdateCardStep": 3, + "SetCardStepCompletion": 3, + "GetCardTable": 3, + "CreateCardColumn": 2, + "MoveCardColumn": 2, + "GetEverythingCompletedCards": 3, + "GetEverythingNoDueDateCards": 3, + "GetEverythingNotNowCards": 3, + "GetEverythingOpenCards": 3, + "GetEverythingOverdueCards": 3, + "GetEverythingUnassignedCards": 3, + "ListCampfires": 3, + "GetCampfire": 3, + "ListChatbots": 3, + "CreateChatbot": 2, + "DeleteChatbot": 3, + "GetChatbot": 3, + "UpdateChatbot": 3, + "ListCampfireLines": 3, + "CreateCampfireLine": 2, + "DeleteCampfireLine": 3, + "GetCampfireLine": 3, + "UpdateCampfireLine": 3, + "ListCampfireUploads": 3, + "CreateCampfireUpload": 3, + "GetEverythingCheckins": 3, + "ListPingablePeople": 3, + "ListClientApprovals": 3, + "GetClientApproval": 3, + "ListClientCorrespondences": 3, + "GetClientCorrespondence": 3, + "ListClientReplies": 3, + "GetClientReply": 3, + "GetEverythingComments": 3, + "GetComment": 3, + "UpdateComment": 3, + "DeleteTool": 3, + "GetTool": 3, + "UpdateTool": 3, + "GetDocument": 3, + "UpdateDocument": 3, + "GetEverythingFiles": 3, + "GetEverythingForwards": 3, + "DestroyGaugeNeedle": 2, + "GetGaugeNeedle": 3, + "UpdateGaugeNeedle": 2, + "GetForward": 3, + "ListForwardReplies": 3, + "CreateForwardReply": 2, + "GetForwardReply": 3, + "GetInbox": 3, + "ListForwards": 3, + "ListLineupMarkers": 3, + "CreateLineupMarker": 2, + "DeleteLineupMarker": 3, + "UpdateLineupMarker": 3, + "GetMessageBoard": 3, + "ListMessages": 3, + "CreateMessage": 2, + "GetEverythingMessages": 3, + "GetMessage": 3, + "UpdateMessage": 3, + "GetMyAssignments": 3, + "GetMyCompletedAssignments": 3, + "GetMyDueAssignments": 3, + "GetMyPreferences": 3, + "UpdateMyPreferences": 2, + "GetMyProfile": 3, + "UpdateMyProfile": 3, + "GetQuestionReminders": 3, + "GetMyNotifications": 3, + "GetBubbleUps": 3, + "MarkAsRead": 2, + "ListPeople": 3, + "GetPerson": 3, + "DisableOutOfOffice": 2, + "GetOutOfOffice": 3, + "EnableOutOfOffice": 2, + "ListProjects": 3, + "CreateProject": 3, + "ListRecordings": 3, + "TrashProject": 3, + "GetProject": 3, + "UpdateProject": 3, + "ToggleGauge": 2, + "ListGaugeNeedles": 3, + "CreateGaugeNeedle": 2, + "ListProjectPeople": 3, + "UpdateProjectAccess": 3, + "GetProjectTimeline": 3, + "GetProjectTimesheet": 3, + "GetAnswer": 3, + "UpdateAnswer": 3, + "GetQuestionnaire": 3, + "ListQuestions": 3, + "CreateQuestion": 2, + "GetQuestion": 3, + "UpdateQuestion": 3, + "ListAnswers": 3, + "CreateAnswer": 2, + "ListQuestionAnswerers": 3, + "GetAnswersByPerson": 3, + "UpdateQuestionNotificationSettings": 3, + "ResumeQuestion": 3, + "PauseQuestion": 3, + "UnpinMessage": 3, + "PinMessage": 2, + "GetRecording": 3, + "ListRecordingBoosts": 3, + "CreateRecordingBoost": 2, + "SetClientVisibility": 3, + "ListComments": 3, + "CreateComment": 2, + "ListEvents": 3, + "ListEventBoosts": 3, + "CreateEventBoost": 2, + "UnarchiveRecording": 3, + "ArchiveRecording": 3, + "TrashRecording": 3, + "Unsubscribe": 3, + "GetSubscription": 3, + "Subscribe": 2, + "UpdateSubscription": 3, + "GetRecordingTimesheet": 3, + "CreateTimesheetEntry": 3, + "DisableTool": 3, + "EnableTool": 2, + "RepositionTool": 3, + "ListGauges": 3, + "GetProgressReport": 3, + "GetUpcomingSchedule": 3, + "GetTimesheetReport": 3, + "ListAssignablePeople": 3, + "GetAssignedTodos": 3, + "GetOverdueTodos": 3, + "GetPersonProgress": 3, + "GetScheduleEntry": 3, + "UpdateScheduleEntry": 3, + "GetScheduleEntryOccurrence": 3, + "GetSchedule": 3, + "UpdateScheduleSettings": 3, + "ListScheduleEntries": 3, + "CreateScheduleEntry": 2, + "Search": 3, + "GetSearchMetadata": 3, + "ListTemplates": 3, + "CreateTemplate": 2, + "DeleteTemplate": 3, + "GetTemplate": 3, + "UpdateTemplate": 3, + "CreateProjectFromTemplate": 2, + "GetProjectConstruction": 3, + "GetTimesheetEntry": 3, + "UpdateTimesheetEntry": 3, + "RepositionTodolistGroup": 3, + "GetTodolistOrGroup": 3, + "UpdateTodolistOrGroup": 3, + "ListTodolistGroups": 3, + "CreateTodolistGroup": 2, + "ListTodos": 3, + "CreateTodo": 3, + "GetEverythingCompletedTodos": 3, + "GetEverythingNoDueDateTodos": 3, + "GetEverythingOpenTodos": 3, + "GetEverythingOverdueTodos": 3, + "GetEverythingUnassignedTodos": 3, + "TrashTodo": 3, + "GetTodo": 3, + "ReplaceTodo": 3, + "UncompleteTodo": 3, + "CompleteTodo": 3, + "RepositionTodo": 3, + "RepositionTodolist": 3, + "GetTodoset": 3, + "GetHillChart": 3, + "UpdateHillChartSettings": 3, + "ListTodolists": 3, + "CreateTodolist": 2, + "GetUpload": 3, + "UpdateUpload": 3, + "ListUploadVersions": 3, + "GetVault": 3, + "UpdateVault": 3, + "ListDocuments": 3, + "CreateDocument": 2, + "ListUploads": 3, + "CreateUpload": 2, + "ListVaults": 3, + "CreateVault": 2, + "DeleteWebhook": 3, + "GetWebhook": 3, + "UpdateWebhook": 3, +} + +// GetOperationRetryMax returns the per-operation retry ceiling (maximum total +// attempts) for the given operation ID. ok is false when the operation carries +// no per-op override, in which case the client-configured cap applies unchanged. +func GetOperationRetryMax(operationId string) (int, bool) { + m, ok := operationRetryMax[operationId] + return m, ok +} + // GetOperationMetadata returns metadata for the given operation ID. func GetOperationMetadata(operationId string) (OperationMetadata, bool) { meta, ok := operationMetadata[operationId] diff --git a/go/templates/client.tmpl b/go/templates/client.tmpl index 218bfe2c6..a72f19c77 100644 --- a/go/templates/client.tmpl +++ b/go/templates/client.tmpl @@ -170,6 +170,13 @@ func (c *{{ $clientTypeName }}) doWithRetry(ctx context.Context, buildRequest fu if !isIdempotent { maxAttempts = 1 } + // Apply the per-operation retry ceiling from x-basecamp-retry as an upper + // bound on attempts: effective attempts = min(client cap, op ceiling). The + // ceiling can only reduce attempts below the client cap, never raise them, so + // a client that lowered its cap (e.g. to disable retries) is still honored. + if opMax, ok := operationRetryMax[operationId]; ok && opMax > 0 && opMax < maxAttempts { + maxAttempts = opMax + } var lastResp *http.Response var lastErr error @@ -594,6 +601,36 @@ var operationMetadata = map[string]OperationMetadata{ {{end}} } +// operationRetryMax maps operation IDs to their per-operation maximum TOTAL +// attempt count from the x-basecamp-retry extension. doWithRetry applies it as a +// per-operation ceiling: effective attempts = min(client cap, retry max). It is +// kept as a separate map (rather than a field on OperationMetadata) so wiring it +// up does not change that exported struct's shape, which would break external +// unkeyed composite literals. +var operationRetryMax = map[string]int{ +{{range . -}} +{{$opid := .OperationId -}} +{{$retryMax := 0 -}} +{{if .Spec -}} +{{if .Spec.Extensions -}} +{{$retry := index .Spec.Extensions "x-basecamp-retry" -}} +{{if $retry -}} +{{$retryMax = index $retry "maxAttempts" -}} +{{end -}} +{{end -}} +{{end -}} + "{{$opid}}": {{$retryMax}}, +{{end}} +} + +// GetOperationRetryMax returns the per-operation retry ceiling (maximum total +// attempts) for the given operation ID. ok is false when the operation carries +// no per-op override, in which case the client-configured cap applies unchanged. +func GetOperationRetryMax(operationId string) (int, bool) { + m, ok := operationRetryMax[operationId] + return m, ok +} + // GetOperationMetadata returns metadata for the given operation ID. func GetOperationMetadata(operationId string) (OperationMetadata, bool) { meta, ok := operationMetadata[operationId] diff --git a/python/scripts/generate_services.py b/python/scripts/generate_services.py index ba71a196d..9798894df 100644 --- a/python/scripts/generate_services.py +++ b/python/scripts/generate_services.py @@ -651,10 +651,13 @@ def build_query_params_expr(op: dict) -> str: def operation_kwarg(op: dict) -> str: - """Return the operation= kwarg string for mutations, empty string for GETs.""" - if op["is_mutation"]: - return f', operation="{op["operation_id"]}"' - return "" + """Return the ``operation=`` kwarg string carrying the canonical (PascalCase) + operationId. The transport uses it to look up per-operation metadata — the + idempotency gate for mutations and the retry.max ceiling for every retryable + request (GET/list/pagination included). OperationInfo.operation is the + snake_case display name and is NOT a metadata key, so it must not be used for + lookups.""" + return f', operation="{op["operation_id"]}"' def is_paginated_list(op: dict) -> bool: @@ -696,26 +699,26 @@ def generate_method_body(op: dict, service_name: str, *, is_async: bool) -> list if op["query_params"]: lines.append(f" return {_await(is_async)}self._request_paginated_wrapped(") lines.append(f' OperationInfo({info_kwargs}), {path_expr}, "{key}",') - lines.append(f" params={build_query_params_expr(op)},") + lines.append(f" params={build_query_params_expr(op)}{operation_kwarg(op)},") lines.append(" )") else: - lines.append(f' return {_await(is_async)}self._request_paginated_wrapped(OperationInfo({info_kwargs}), {path_expr}, "{key}")') + lines.append(f' return {_await(is_async)}self._request_paginated_wrapped(OperationInfo({info_kwargs}), {path_expr}, "{key}"{operation_kwarg(op)})') elif is_paginated_list(op): if op["query_params"]: lines.append(f" return {_await(is_async)}self._request_paginated(") lines.append(f" OperationInfo({info_kwargs}), {path_expr},") - lines.append(f" params={build_query_params_expr(op)},") + lines.append(f" params={build_query_params_expr(op)}{operation_kwarg(op)},") lines.append(" )") else: - lines.append(f" return {_await(is_async)}self._request_paginated(OperationInfo({info_kwargs}), {path_expr})") + lines.append(f" return {_await(is_async)}self._request_paginated(OperationInfo({info_kwargs}), {path_expr}{operation_kwarg(op)})") elif is_unpaginated_array(op): if op["query_params"]: lines.append(f" return {_await(is_async)}self._request_list(") lines.append(f" OperationInfo({info_kwargs}), {path_expr},") - lines.append(f" params={build_query_params_expr(op)},") + lines.append(f" params={build_query_params_expr(op)}{operation_kwarg(op)},") lines.append(" )") else: - lines.append(f" return {_await(is_async)}self._request_list(OperationInfo({info_kwargs}), {path_expr})") + lines.append(f" return {_await(is_async)}self._request_list(OperationInfo({info_kwargs}), {path_expr}{operation_kwarg(op)})") elif op["has_binary_body"]: # Binary upload if op["query_params"]: diff --git a/python/src/basecamp/_async_http.py b/python/src/basecamp/_async_http.py index 4a3fc6337..3f430e551 100644 --- a/python/src/basecamp/_async_http.py +++ b/python/src/basecamp/_async_http.py @@ -48,9 +48,9 @@ def __init__( def base_url(self) -> str: return self._config.base_url - async def get(self, url: str, *, params: dict | None = None) -> httpx.Response: + async def get(self, url: str, *, params: dict | None = None, operation: str | None = None) -> httpx.Response: url = self._build_url(url) - return await self._request_with_retry("GET", url, params=params) + return await self._request_with_retry("GET", url, params=params, operation=operation) async def get_absolute(self, url: str, *, params: dict | None = None) -> httpx.Response: if not _security.is_localhost(url): @@ -90,6 +90,7 @@ async def post_raw( params=params, content=content, content_type=content_type, + operation=operation, ) return await self._single_request( "POST", @@ -115,7 +116,7 @@ async def request_multipart( safe_content_type = content_type.replace("\r", "").replace("\n", "") files = {field: (safe_filename, content, safe_content_type)} if operation and self._is_retryable_operation(operation): - return await self._request_with_retry(method, url, files=files) + return await self._request_with_retry(method, url, files=files, operation=operation) return await self._single_request(method, url, files=files) async def get_no_retry(self, url: str) -> httpx.Response: @@ -131,7 +132,7 @@ async def _mutation( self, method: str, url: str, *, json_body: dict | None = None, operation: str | None = None ) -> httpx.Response: if operation and self._is_retryable_operation(operation): - return await self._request_with_retry(method, url, json_body=json_body) + return await self._request_with_retry(method, url, json_body=json_body, operation=operation) return await self._single_request(method, url, json_body=json_body) async def _request_with_retry( @@ -145,11 +146,13 @@ async def _request_with_retry( content_type: str | None = None, files: dict | None = None, allow_cross_origin: bool = False, + operation: str | None = None, ) -> httpx.Response: # max_retries is a TOTAL attempt count (config validation guarantees it # is >= 0). 0 is accepted as a compatibility exception and means a single # attempt with no retry. max_attempts = self._config.max_retries if self._config.max_retries > 0 else 1 + max_attempts = self._apply_operation_retry_max(operation, max_attempts) attempt = 0 last_error: BasecampError | None = None @@ -307,3 +310,16 @@ def _calculate_delay(self, attempt: int, server_retry_after: int | None = None) def _is_retryable_operation(self, operation: str) -> bool: op_meta = self._metadata.get(operation, {}) return op_meta.get("idempotent", False) + + def _apply_operation_retry_max(self, operation: str | None, max_attempts: int) -> int: + # Apply the per-operation retry ceiling from metadata as an upper bound on + # attempts: effective = min(client cap, operation's retry.max). The + # ceiling can only reduce attempts below the client-configured cap, never + # raise them, so a client that lowered its cap is still honored. An + # operation with no metadata (or no retry.max) leaves the cap unchanged. + if not operation: + return max_attempts + op_max = self._metadata.get(operation, {}).get("retry", {}).get("max") + if isinstance(op_max, int) and 0 < op_max < max_attempts: + return op_max + return max_attempts diff --git a/python/src/basecamp/_http.py b/python/src/basecamp/_http.py index 292be1f61..cb5f1fa9e 100644 --- a/python/src/basecamp/_http.py +++ b/python/src/basecamp/_http.py @@ -47,9 +47,9 @@ def __init__( def base_url(self) -> str: return self._config.base_url - def get(self, url: str, *, params: dict | None = None) -> httpx.Response: + def get(self, url: str, *, params: dict | None = None, operation: str | None = None) -> httpx.Response: url = self._build_url(url) - return self._request_with_retry("GET", url, params=params) + return self._request_with_retry("GET", url, params=params, operation=operation) def get_absolute(self, url: str, *, params: dict | None = None) -> httpx.Response: if not _security.is_localhost(url): @@ -89,6 +89,7 @@ def post_raw( params=params, content=content, content_type=content_type, + operation=operation, ) return self._single_request( "POST", @@ -115,7 +116,7 @@ def request_multipart( safe_content_type = content_type.replace("\r", "").replace("\n", "") files = {field: (safe_filename, content, safe_content_type)} if operation and self._is_retryable_operation(operation): - return self._request_with_retry(method, url, files=files) + return self._request_with_retry(method, url, files=files, operation=operation) return self._single_request(method, url, files=files) def get_no_retry(self, url: str) -> httpx.Response: @@ -131,7 +132,7 @@ def _mutation( self, method: str, url: str, *, json_body: dict | None = None, operation: str | None = None ) -> httpx.Response: if operation and self._is_retryable_operation(operation): - return self._request_with_retry(method, url, json_body=json_body) + return self._request_with_retry(method, url, json_body=json_body, operation=operation) return self._single_request(method, url, json_body=json_body) def _request_with_retry( @@ -145,11 +146,13 @@ def _request_with_retry( content_type: str | None = None, files: dict | None = None, allow_cross_origin: bool = False, + operation: str | None = None, ) -> httpx.Response: # max_retries is a TOTAL attempt count (config validation guarantees it # is >= 0). 0 is accepted as a compatibility exception and means a single # attempt with no retry. max_attempts = self._config.max_retries if self._config.max_retries > 0 else 1 + max_attempts = self._apply_operation_retry_max(operation, max_attempts) attempt = 0 last_error: BasecampError | None = None @@ -307,3 +310,16 @@ def _calculate_delay(self, attempt: int, server_retry_after: int | None = None) def _is_retryable_operation(self, operation: str) -> bool: op_meta = self._metadata.get(operation, {}) return op_meta.get("idempotent", False) + + def _apply_operation_retry_max(self, operation: str | None, max_attempts: int) -> int: + # Apply the per-operation retry ceiling from metadata as an upper bound on + # attempts: effective = min(client cap, operation's retry.max). The + # ceiling can only reduce attempts below the client-configured cap, never + # raise them, so a client that lowered its cap is still honored. An + # operation with no metadata (or no retry.max) leaves the cap unchanged. + if not operation: + return max_attempts + op_max = self._metadata.get(operation, {}).get("retry", {}).get("max") + if isinstance(op_max, int) and 0 < op_max < max_attempts: + return op_max + return max_attempts diff --git a/python/src/basecamp/generated/services/_async_base.py b/python/src/basecamp/generated/services/_async_base.py index 7e5d57e51..6d87bc985 100644 --- a/python/src/basecamp/generated/services/_async_base.py +++ b/python/src/basecamp/generated/services/_async_base.py @@ -52,7 +52,9 @@ async def _request( safe_hook(self._hooks.on_operation_start, info) try: if method == "GET": - response = await self._client.http.get(self._client.account_path(path), params=params) + response = await self._client.http.get( + self._client.account_path(path), params=params, operation=operation + ) elif method == "POST": response = await self._client.http.post( self._client.account_path(path), json_body=json_body, operation=operation @@ -81,6 +83,7 @@ async def _request_list( path: str, *, params: dict | None = None, + operation: str | None = None, ) -> ListResult: """Fetch a complete, unpaginated array in a single request. @@ -92,7 +95,7 @@ async def _request_list( start = time.monotonic() safe_hook(self._hooks.on_operation_start, info) try: - response = await self._client.http.get(self._client.account_path(path), params=params) + response = await self._client.http.get(self._client.account_path(path), params=params, operation=operation) _security.check_body_size(response.content, _security.MAX_RESPONSE_BODY_BYTES) items = response.json() _normalize_person_ids(items) @@ -204,11 +207,12 @@ async def _request_paginated( *, params: dict | None = None, max_items: int | None = None, + operation: str | None = None, ) -> ListResult: start = time.monotonic() safe_hook(self._hooks.on_operation_start, info) try: - result = await self._paginate(path, params=params, max_items=max_items) + result = await self._paginate(path, params=params, max_items=max_items, operation=operation) duration_ms = int((time.monotonic() - start) * 1000) safe_hook(self._hooks.on_operation_end, info, OperationResult(duration_ms=duration_ms)) return result @@ -224,11 +228,12 @@ async def _request_paginated_key( key: str, *, params: dict | None = None, + operation: str | None = None, ) -> ListResult: start = time.monotonic() safe_hook(self._hooks.on_operation_start, info) try: - result = await self._paginate_key(path, key, params=params) + result = await self._paginate_key(path, key, params=params, operation=operation) duration_ms = int((time.monotonic() - start) * 1000) safe_hook(self._hooks.on_operation_end, info, OperationResult(duration_ms=duration_ms)) return result @@ -244,11 +249,12 @@ async def _request_paginated_wrapped( key: str, *, params: dict | None = None, + operation: str | None = None, ) -> dict: start = time.monotonic() safe_hook(self._hooks.on_operation_start, info) try: - result = await self._paginate_wrapped(path, key, params=params) + result = await self._paginate_wrapped(path, key, params=params, operation=operation) duration_ms = int((time.monotonic() - start) * 1000) safe_hook(self._hooks.on_operation_end, info, OperationResult(duration_ms=duration_ms)) return result @@ -257,7 +263,14 @@ async def _request_paginated_wrapped( safe_hook(self._hooks.on_operation_end, info, OperationResult(duration_ms=duration_ms, error=e)) raise - async def _paginate(self, path: str, *, params: dict | None = None, max_items: int | None = None) -> ListResult: + async def _paginate( + self, + path: str, + *, + params: dict | None = None, + max_items: int | None = None, + operation: str | None = None, + ) -> ListResult: base_url = self._client.http._build_url(self._client.account_path(path)) url = base_url all_items: list = [] @@ -266,7 +279,7 @@ async def _paginate(self, path: str, *, params: dict | None = None, max_items: i for page in range(1, self._client.config.max_pages + 1): safe_hook(self._hooks.on_paginate, url, page) - response = await self._client.http.get(url, params=params if page == 1 else None) + response = await self._client.http.get(url, params=params if page == 1 else None, operation=operation) _security.check_body_size(response.content, _security.MAX_RESPONSE_BODY_BYTES) if page == 1: @@ -299,7 +312,9 @@ async def _paginate(self, path: str, *, params: dict | None = None, max_items: i return ListResult(all_items, ListMeta(total_count=total_count, truncated=truncated)) - async def _paginate_key(self, path: str, key: str, *, params: dict | None = None) -> ListResult: + async def _paginate_key( + self, path: str, key: str, *, params: dict | None = None, operation: str | None = None + ) -> ListResult: base_url = self._client.http._build_url(self._client.account_path(path)) url = base_url all_items: list = [] @@ -307,7 +322,7 @@ async def _paginate_key(self, path: str, key: str, *, params: dict | None = None for page in range(1, self._client.config.max_pages + 1): safe_hook(self._hooks.on_paginate, url, page) - response = await self._client.http.get(url, params=params if page == 1 else None) + response = await self._client.http.get(url, params=params if page == 1 else None, operation=operation) _security.check_body_size(response.content, _security.MAX_RESPONSE_BODY_BYTES) if page == 1: @@ -334,11 +349,13 @@ async def _paginate_key(self, path: str, key: str, *, params: dict | None = None return ListResult(all_items, ListMeta(total_count=total_count)) - async def _paginate_wrapped(self, path: str, key: str, *, params: dict | None = None) -> dict: + async def _paginate_wrapped( + self, path: str, key: str, *, params: dict | None = None, operation: str | None = None + ) -> dict: base_url = self._client.http._build_url(self._client.account_path(path)) safe_hook(self._hooks.on_paginate, base_url, 1) - first_response = await self._client.http.get(base_url, params=params) + first_response = await self._client.http.get(base_url, params=params, operation=operation) _security.check_body_size(first_response.content, _security.MAX_RESPONSE_BODY_BYTES) total_count = parse_total_count(dict(first_response.headers)) @@ -363,7 +380,7 @@ async def _paginate_wrapped(self, path: str, key: str, *, params: dict | None = raise ApiError(f"Pagination Link header points to different origin: {_security.truncate(next_url)}") safe_hook(self._hooks.on_paginate, next_url, page) - response = await self._client.http.get(next_url) + response = await self._client.http.get(next_url, operation=operation) _security.check_body_size(response.content, _security.MAX_RESPONSE_BODY_BYTES) try: diff --git a/python/src/basecamp/generated/services/_base.py b/python/src/basecamp/generated/services/_base.py index 485eeec5a..b27dfe656 100644 --- a/python/src/basecamp/generated/services/_base.py +++ b/python/src/basecamp/generated/services/_base.py @@ -59,7 +59,7 @@ def _request( safe_hook(self._hooks.on_operation_start, info) try: if method == "GET": - response = self._client.http.get(self._client.account_path(path), params=params) + response = self._client.http.get(self._client.account_path(path), params=params, operation=operation) elif method == "POST": response = self._client.http.post( self._client.account_path(path), json_body=json_body, operation=operation @@ -88,6 +88,7 @@ def _request_list( path: str, *, params: dict | None = None, + operation: str | None = None, ) -> ListResult: """Fetch a complete, unpaginated array in a single request. @@ -99,7 +100,7 @@ def _request_list( start = time.monotonic() safe_hook(self._hooks.on_operation_start, info) try: - response = self._client.http.get(self._client.account_path(path), params=params) + response = self._client.http.get(self._client.account_path(path), params=params, operation=operation) _security.check_body_size(response.content, _security.MAX_RESPONSE_BODY_BYTES) items = response.json() _normalize_person_ids(items) @@ -211,11 +212,12 @@ def _request_paginated( *, params: dict | None = None, max_items: int | None = None, + operation: str | None = None, ) -> ListResult: start = time.monotonic() safe_hook(self._hooks.on_operation_start, info) try: - result = self._paginate(path, params=params, max_items=max_items) + result = self._paginate(path, params=params, max_items=max_items, operation=operation) duration_ms = int((time.monotonic() - start) * 1000) safe_hook(self._hooks.on_operation_end, info, OperationResult(duration_ms=duration_ms)) return result @@ -231,11 +233,12 @@ def _request_paginated_key( key: str, *, params: dict | None = None, + operation: str | None = None, ) -> ListResult: start = time.monotonic() safe_hook(self._hooks.on_operation_start, info) try: - result = self._paginate_key(path, key, params=params) + result = self._paginate_key(path, key, params=params, operation=operation) duration_ms = int((time.monotonic() - start) * 1000) safe_hook(self._hooks.on_operation_end, info, OperationResult(duration_ms=duration_ms)) return result @@ -251,11 +254,12 @@ def _request_paginated_wrapped( key: str, *, params: dict | None = None, + operation: str | None = None, ) -> dict: start = time.monotonic() safe_hook(self._hooks.on_operation_start, info) try: - result = self._paginate_wrapped(path, key, params=params) + result = self._paginate_wrapped(path, key, params=params, operation=operation) duration_ms = int((time.monotonic() - start) * 1000) safe_hook(self._hooks.on_operation_end, info, OperationResult(duration_ms=duration_ms)) return result @@ -264,7 +268,14 @@ def _request_paginated_wrapped( safe_hook(self._hooks.on_operation_end, info, OperationResult(duration_ms=duration_ms, error=e)) raise - def _paginate(self, path: str, *, params: dict | None = None, max_items: int | None = None) -> ListResult: + def _paginate( + self, + path: str, + *, + params: dict | None = None, + max_items: int | None = None, + operation: str | None = None, + ) -> ListResult: base_url = self._client.http._build_url(self._client.account_path(path)) url = base_url all_items: list = [] @@ -273,7 +284,7 @@ def _paginate(self, path: str, *, params: dict | None = None, max_items: int | N for page in range(1, self._client.config.max_pages + 1): safe_hook(self._hooks.on_paginate, url, page) - response = self._client.http.get(url, params=params if page == 1 else None) + response = self._client.http.get(url, params=params if page == 1 else None, operation=operation) _security.check_body_size(response.content, _security.MAX_RESPONSE_BODY_BYTES) if page == 1: @@ -306,7 +317,9 @@ def _paginate(self, path: str, *, params: dict | None = None, max_items: int | N return ListResult(all_items, ListMeta(total_count=total_count, truncated=truncated)) - def _paginate_key(self, path: str, key: str, *, params: dict | None = None) -> ListResult: + def _paginate_key( + self, path: str, key: str, *, params: dict | None = None, operation: str | None = None + ) -> ListResult: base_url = self._client.http._build_url(self._client.account_path(path)) url = base_url all_items: list = [] @@ -314,7 +327,7 @@ def _paginate_key(self, path: str, key: str, *, params: dict | None = None) -> L for page in range(1, self._client.config.max_pages + 1): safe_hook(self._hooks.on_paginate, url, page) - response = self._client.http.get(url, params=params if page == 1 else None) + response = self._client.http.get(url, params=params if page == 1 else None, operation=operation) _security.check_body_size(response.content, _security.MAX_RESPONSE_BODY_BYTES) if page == 1: @@ -341,11 +354,13 @@ def _paginate_key(self, path: str, key: str, *, params: dict | None = None) -> L return ListResult(all_items, ListMeta(total_count=total_count)) - def _paginate_wrapped(self, path: str, key: str, *, params: dict | None = None) -> dict: + def _paginate_wrapped( + self, path: str, key: str, *, params: dict | None = None, operation: str | None = None + ) -> dict: base_url = self._client.http._build_url(self._client.account_path(path)) safe_hook(self._hooks.on_paginate, base_url, 1) - first_response = self._client.http.get(base_url, params=params) + first_response = self._client.http.get(base_url, params=params, operation=operation) _security.check_body_size(first_response.content, _security.MAX_RESPONSE_BODY_BYTES) total_count = parse_total_count(dict(first_response.headers)) @@ -370,7 +385,7 @@ def _paginate_wrapped(self, path: str, key: str, *, params: dict | None = None) raise ApiError(f"Pagination Link header points to different origin: {_security.truncate(next_url)}") safe_hook(self._hooks.on_paginate, next_url, page) - response = self._client.http.get(next_url) + response = self._client.http.get(next_url, operation=operation) _security.check_body_size(response.content, _security.MAX_RESPONSE_BODY_BYTES) try: diff --git a/python/src/basecamp/generated/services/account.py b/python/src/basecamp/generated/services/account.py index 23c4d1397..da0aa58cd 100644 --- a/python/src/basecamp/generated/services/account.py +++ b/python/src/basecamp/generated/services/account.py @@ -13,7 +13,10 @@ class AccountService(BaseService): def get_account(self) -> dict[str, Any]: return self._request( - OperationInfo(service="account", operation="get_account", is_mutation=False), "GET", "/account.json" + OperationInfo(service="account", operation="get_account", is_mutation=False), + "GET", + "/account.json", + operation="GetAccount", ) def update_account_logo(self, *, content: bytes, filename: str, content_type: str) -> None: @@ -49,7 +52,10 @@ def update_account_name(self, *, name: str) -> dict[str, Any]: class AsyncAccountService(AsyncBaseService): async def get_account(self) -> dict[str, Any]: return await self._request( - OperationInfo(service="account", operation="get_account", is_mutation=False), "GET", "/account.json" + OperationInfo(service="account", operation="get_account", is_mutation=False), + "GET", + "/account.json", + operation="GetAccount", ) async def update_account_logo(self, *, content: bytes, filename: str, content_type: str) -> None: diff --git a/python/src/basecamp/generated/services/automation.py b/python/src/basecamp/generated/services/automation.py index e8a8991ce..0daed49d4 100644 --- a/python/src/basecamp/generated/services/automation.py +++ b/python/src/basecamp/generated/services/automation.py @@ -15,6 +15,7 @@ def list_lineup_markers(self) -> ListResult: return self._request_list( OperationInfo(service="automation", operation="list_lineup_markers", is_mutation=False), "/lineup/markers.json", + operation="ListLineupMarkers", ) @@ -23,4 +24,5 @@ async def list_lineup_markers(self) -> ListResult: return await self._request_list( OperationInfo(service="automation", operation="list_lineup_markers", is_mutation=False), "/lineup/markers.json", + operation="ListLineupMarkers", ) diff --git a/python/src/basecamp/generated/services/boosts.py b/python/src/basecamp/generated/services/boosts.py index 3eaa7e162..98e1ba531 100644 --- a/python/src/basecamp/generated/services/boosts.py +++ b/python/src/basecamp/generated/services/boosts.py @@ -16,6 +16,7 @@ def get_boost(self, *, boost_id: int) -> dict[str, Any]: OperationInfo(service="boosts", operation="get_boost", is_mutation=False, resource_id=boost_id), "GET", f"/boosts/{boost_id}", + operation="GetBoost", ) def delete_boost(self, *, boost_id: int) -> None: @@ -32,6 +33,7 @@ def list_recording_boosts(self, *, recording_id: int) -> ListResult: service="boosts", operation="list_recording_boosts", is_mutation=False, resource_id=recording_id ), f"/recordings/{recording_id}/boosts.json", + operation="ListRecordingBoosts", ) def create_recording_boost(self, *, recording_id: int, content: str) -> dict[str, Any]: @@ -49,6 +51,7 @@ def list_event_boosts(self, *, recording_id: int, event_id: int) -> ListResult: return self._request_paginated( OperationInfo(service="boosts", operation="list_event_boosts", is_mutation=False, resource_id=event_id), f"/recordings/{recording_id}/events/{event_id}/boosts.json", + operation="ListEventBoosts", ) def create_event_boost(self, *, recording_id: int, event_id: int, content: str) -> dict[str, Any]: @@ -67,6 +70,7 @@ async def get_boost(self, *, boost_id: int) -> dict[str, Any]: OperationInfo(service="boosts", operation="get_boost", is_mutation=False, resource_id=boost_id), "GET", f"/boosts/{boost_id}", + operation="GetBoost", ) async def delete_boost(self, *, boost_id: int) -> None: @@ -83,6 +87,7 @@ async def list_recording_boosts(self, *, recording_id: int) -> ListResult: service="boosts", operation="list_recording_boosts", is_mutation=False, resource_id=recording_id ), f"/recordings/{recording_id}/boosts.json", + operation="ListRecordingBoosts", ) async def create_recording_boost(self, *, recording_id: int, content: str) -> dict[str, Any]: @@ -100,6 +105,7 @@ async def list_event_boosts(self, *, recording_id: int, event_id: int) -> ListRe return await self._request_paginated( OperationInfo(service="boosts", operation="list_event_boosts", is_mutation=False, resource_id=event_id), f"/recordings/{recording_id}/events/{event_id}/boosts.json", + operation="ListEventBoosts", ) async def create_event_boost(self, *, recording_id: int, event_id: int, content: str) -> dict[str, Any]: diff --git a/python/src/basecamp/generated/services/campfires.py b/python/src/basecamp/generated/services/campfires.py index 5e8ac1dec..fe361cc47 100644 --- a/python/src/basecamp/generated/services/campfires.py +++ b/python/src/basecamp/generated/services/campfires.py @@ -13,7 +13,9 @@ class CampfiresService(BaseService): def list(self) -> ListResult: return self._request_paginated( - OperationInfo(service="campfires", operation="list", is_mutation=False), "/chats.json" + OperationInfo(service="campfires", operation="list", is_mutation=False), + "/chats.json", + operation="ListCampfires", ) def get(self, *, campfire_id: int) -> dict[str, Any]: @@ -21,12 +23,14 @@ def get(self, *, campfire_id: int) -> dict[str, Any]: OperationInfo(service="campfires", operation="get", is_mutation=False, resource_id=campfire_id), "GET", f"/chats/{campfire_id}", + operation="GetCampfire", ) def list_chatbots(self, *, campfire_id: int) -> ListResult: return self._request_paginated( OperationInfo(service="campfires", operation="list_chatbots", is_mutation=False, resource_id=campfire_id), f"/chats/{campfire_id}/integrations.json", + operation="ListChatbots", ) def create_chatbot(self, *, campfire_id: int, service_name: str, command_url: str | None = None) -> dict[str, Any]: @@ -43,6 +47,7 @@ def get_chatbot(self, *, campfire_id: int, chatbot_id: int) -> dict[str, Any]: OperationInfo(service="campfires", operation="get_chatbot", is_mutation=False, resource_id=chatbot_id), "GET", f"/chats/{campfire_id}/integrations/{chatbot_id}", + operation="GetChatbot", ) def update_chatbot( @@ -69,6 +74,7 @@ def list_lines(self, *, campfire_id: int, sort: str | None = None, direction: st OperationInfo(service="campfires", operation="list_lines", is_mutation=False, resource_id=campfire_id), f"/chats/{campfire_id}/lines.json", params=self._compact(sort=sort, direction=direction), + operation="ListCampfireLines", ) def create_line(self, *, campfire_id: int, content: str, content_type: str | None = None) -> dict[str, Any]: @@ -85,6 +91,7 @@ def get_line(self, *, campfire_id: int, line_id: int) -> dict[str, Any]: OperationInfo(service="campfires", operation="get_line", is_mutation=False, resource_id=line_id), "GET", f"/chats/{campfire_id}/lines/{line_id}", + operation="GetCampfireLine", ) def update_line(self, *, campfire_id: int, line_id: int, content: str) -> None: @@ -109,6 +116,7 @@ def list_uploads(self, *, campfire_id: int, sort: str | None = None, direction: OperationInfo(service="campfires", operation="list_uploads", is_mutation=False, resource_id=campfire_id), f"/chats/{campfire_id}/uploads.json", params=self._compact(sort=sort, direction=direction), + operation="ListCampfireUploads", ) def create_upload(self, *, campfire_id: int, content: bytes, content_type: str, name: str) -> dict[str, Any]: @@ -125,7 +133,9 @@ def create_upload(self, *, campfire_id: int, content: bytes, content_type: str, class AsyncCampfiresService(AsyncBaseService): async def list(self) -> ListResult: return await self._request_paginated( - OperationInfo(service="campfires", operation="list", is_mutation=False), "/chats.json" + OperationInfo(service="campfires", operation="list", is_mutation=False), + "/chats.json", + operation="ListCampfires", ) async def get(self, *, campfire_id: int) -> dict[str, Any]: @@ -133,12 +143,14 @@ async def get(self, *, campfire_id: int) -> dict[str, Any]: OperationInfo(service="campfires", operation="get", is_mutation=False, resource_id=campfire_id), "GET", f"/chats/{campfire_id}", + operation="GetCampfire", ) async def list_chatbots(self, *, campfire_id: int) -> ListResult: return await self._request_paginated( OperationInfo(service="campfires", operation="list_chatbots", is_mutation=False, resource_id=campfire_id), f"/chats/{campfire_id}/integrations.json", + operation="ListChatbots", ) async def create_chatbot( @@ -157,6 +169,7 @@ async def get_chatbot(self, *, campfire_id: int, chatbot_id: int) -> dict[str, A OperationInfo(service="campfires", operation="get_chatbot", is_mutation=False, resource_id=chatbot_id), "GET", f"/chats/{campfire_id}/integrations/{chatbot_id}", + operation="GetChatbot", ) async def update_chatbot( @@ -185,6 +198,7 @@ async def list_lines( OperationInfo(service="campfires", operation="list_lines", is_mutation=False, resource_id=campfire_id), f"/chats/{campfire_id}/lines.json", params=self._compact(sort=sort, direction=direction), + operation="ListCampfireLines", ) async def create_line(self, *, campfire_id: int, content: str, content_type: str | None = None) -> dict[str, Any]: @@ -201,6 +215,7 @@ async def get_line(self, *, campfire_id: int, line_id: int) -> dict[str, Any]: OperationInfo(service="campfires", operation="get_line", is_mutation=False, resource_id=line_id), "GET", f"/chats/{campfire_id}/lines/{line_id}", + operation="GetCampfireLine", ) async def update_line(self, *, campfire_id: int, line_id: int, content: str) -> None: @@ -227,6 +242,7 @@ async def list_uploads( OperationInfo(service="campfires", operation="list_uploads", is_mutation=False, resource_id=campfire_id), f"/chats/{campfire_id}/uploads.json", params=self._compact(sort=sort, direction=direction), + operation="ListCampfireUploads", ) async def create_upload(self, *, campfire_id: int, content: bytes, content_type: str, name: str) -> dict[str, Any]: diff --git a/python/src/basecamp/generated/services/card_columns.py b/python/src/basecamp/generated/services/card_columns.py index c0f476f49..833f196bc 100644 --- a/python/src/basecamp/generated/services/card_columns.py +++ b/python/src/basecamp/generated/services/card_columns.py @@ -59,6 +59,7 @@ def get(self, *, column_id: int) -> dict[str, Any]: OperationInfo(service="cardcolumns", operation="get", is_mutation=False, resource_id=column_id), "GET", f"/card_tables/columns/{column_id}", + operation="GetCardColumn", ) def update(self, *, column_id: int, title: str | None = None, description: str | None = None) -> dict[str, Any]: @@ -158,6 +159,7 @@ async def get(self, *, column_id: int) -> dict[str, Any]: OperationInfo(service="cardcolumns", operation="get", is_mutation=False, resource_id=column_id), "GET", f"/card_tables/columns/{column_id}", + operation="GetCardColumn", ) async def update( diff --git a/python/src/basecamp/generated/services/card_steps.py b/python/src/basecamp/generated/services/card_steps.py index b518f3ce8..f34ce9b57 100644 --- a/python/src/basecamp/generated/services/card_steps.py +++ b/python/src/basecamp/generated/services/card_steps.py @@ -36,6 +36,7 @@ def get(self, *, step_id: int) -> dict[str, Any]: OperationInfo(service="cardsteps", operation="get", is_mutation=False, resource_id=step_id), "GET", f"/card_tables/steps/{step_id}", + operation="GetCardStep", ) def update( @@ -90,6 +91,7 @@ async def get(self, *, step_id: int) -> dict[str, Any]: OperationInfo(service="cardsteps", operation="get", is_mutation=False, resource_id=step_id), "GET", f"/card_tables/steps/{step_id}", + operation="GetCardStep", ) async def update( diff --git a/python/src/basecamp/generated/services/card_tables.py b/python/src/basecamp/generated/services/card_tables.py index 1a9ded1b4..75c2e4f02 100644 --- a/python/src/basecamp/generated/services/card_tables.py +++ b/python/src/basecamp/generated/services/card_tables.py @@ -16,6 +16,7 @@ def get(self, *, card_table_id: int) -> dict[str, Any]: OperationInfo(service="cardtables", operation="get", is_mutation=False, resource_id=card_table_id), "GET", f"/card_tables/{card_table_id}", + operation="GetCardTable", ) @@ -25,4 +26,5 @@ async def get(self, *, card_table_id: int) -> dict[str, Any]: OperationInfo(service="cardtables", operation="get", is_mutation=False, resource_id=card_table_id), "GET", f"/card_tables/{card_table_id}", + operation="GetCardTable", ) diff --git a/python/src/basecamp/generated/services/cards.py b/python/src/basecamp/generated/services/cards.py index 5bfe7657f..f53b882c0 100644 --- a/python/src/basecamp/generated/services/cards.py +++ b/python/src/basecamp/generated/services/cards.py @@ -16,6 +16,7 @@ def get(self, *, card_id: int) -> dict[str, Any]: OperationInfo(service="cards", operation="get", is_mutation=False, resource_id=card_id), "GET", f"/card_tables/cards/{card_id}", + operation="GetCard", ) def update( @@ -48,6 +49,7 @@ def list(self, *, column_id: int) -> ListResult: return self._request_paginated( OperationInfo(service="cards", operation="list", is_mutation=False, resource_id=column_id), f"/card_tables/lists/{column_id}/cards.json", + operation="ListCards", ) def create( @@ -74,6 +76,7 @@ async def get(self, *, card_id: int) -> dict[str, Any]: OperationInfo(service="cards", operation="get", is_mutation=False, resource_id=card_id), "GET", f"/card_tables/cards/{card_id}", + operation="GetCard", ) async def update( @@ -106,6 +109,7 @@ async def list(self, *, column_id: int) -> ListResult: return await self._request_paginated( OperationInfo(service="cards", operation="list", is_mutation=False, resource_id=column_id), f"/card_tables/lists/{column_id}/cards.json", + operation="ListCards", ) async def create( diff --git a/python/src/basecamp/generated/services/checkins.py b/python/src/basecamp/generated/services/checkins.py index c473d0bed..2f7ec03fd 100644 --- a/python/src/basecamp/generated/services/checkins.py +++ b/python/src/basecamp/generated/services/checkins.py @@ -13,7 +13,9 @@ class CheckinsService(BaseService): def reminders(self) -> ListResult: return self._request_paginated( - OperationInfo(service="checkins", operation="reminders", is_mutation=False), "/my/question_reminders.json" + OperationInfo(service="checkins", operation="reminders", is_mutation=False), + "/my/question_reminders.json", + operation="GetQuestionReminders", ) def get_answer(self, *, answer_id: int) -> dict[str, Any]: @@ -21,6 +23,7 @@ def get_answer(self, *, answer_id: int) -> dict[str, Any]: OperationInfo(service="checkins", operation="get_answer", is_mutation=False, resource_id=answer_id), "GET", f"/question_answers/{answer_id}", + operation="GetAnswer", ) def update_answer(self, *, answer_id: int, content: str, group_on: str | None = None) -> None: @@ -39,6 +42,7 @@ def get_questionnaire(self, *, questionnaire_id: int) -> dict[str, Any]: ), "GET", f"/questionnaires/{questionnaire_id}", + operation="GetQuestionnaire", ) def list_questions(self, *, questionnaire_id: int) -> ListResult: @@ -47,6 +51,7 @@ def list_questions(self, *, questionnaire_id: int) -> ListResult: service="checkins", operation="list_questions", is_mutation=False, resource_id=questionnaire_id ), f"/questionnaires/{questionnaire_id}/questions.json", + operation="ListQuestions", ) def create_question( @@ -67,6 +72,7 @@ def get_question(self, *, question_id: int) -> dict[str, Any]: OperationInfo(service="checkins", operation="get_question", is_mutation=False, resource_id=question_id), "GET", f"/questions/{question_id}", + operation="GetQuestion", ) def update_question( @@ -84,6 +90,7 @@ def list_answers(self, *, question_id: int) -> ListResult: return self._request_paginated( OperationInfo(service="checkins", operation="list_answers", is_mutation=False, resource_id=question_id), f"/questions/{question_id}/answers.json", + operation="ListAnswers", ) def create_answer(self, *, question_id: int, content: str, group_on: str | None = None) -> dict[str, Any]: @@ -99,12 +106,14 @@ def answerers(self, *, question_id: int) -> ListResult: return self._request_paginated( OperationInfo(service="checkins", operation="answerers", is_mutation=False, resource_id=question_id), f"/questions/{question_id}/answers/by.json", + operation="ListQuestionAnswerers", ) def by_person(self, *, question_id: int, person_id: int) -> ListResult: return self._request_paginated( OperationInfo(service="checkins", operation="by_person", is_mutation=False, resource_id=person_id), f"/questions/{question_id}/answers/by/{person_id}", + operation="GetAnswersByPerson", ) def update_notification_settings( @@ -142,7 +151,9 @@ def resume(self, *, question_id: int) -> dict[str, Any]: class AsyncCheckinsService(AsyncBaseService): async def reminders(self) -> ListResult: return await self._request_paginated( - OperationInfo(service="checkins", operation="reminders", is_mutation=False), "/my/question_reminders.json" + OperationInfo(service="checkins", operation="reminders", is_mutation=False), + "/my/question_reminders.json", + operation="GetQuestionReminders", ) async def get_answer(self, *, answer_id: int) -> dict[str, Any]: @@ -150,6 +161,7 @@ async def get_answer(self, *, answer_id: int) -> dict[str, Any]: OperationInfo(service="checkins", operation="get_answer", is_mutation=False, resource_id=answer_id), "GET", f"/question_answers/{answer_id}", + operation="GetAnswer", ) async def update_answer(self, *, answer_id: int, content: str, group_on: str | None = None) -> None: @@ -168,6 +180,7 @@ async def get_questionnaire(self, *, questionnaire_id: int) -> dict[str, Any]: ), "GET", f"/questionnaires/{questionnaire_id}", + operation="GetQuestionnaire", ) async def list_questions(self, *, questionnaire_id: int) -> ListResult: @@ -176,6 +189,7 @@ async def list_questions(self, *, questionnaire_id: int) -> ListResult: service="checkins", operation="list_questions", is_mutation=False, resource_id=questionnaire_id ), f"/questionnaires/{questionnaire_id}/questions.json", + operation="ListQuestions", ) async def create_question( @@ -196,6 +210,7 @@ async def get_question(self, *, question_id: int) -> dict[str, Any]: OperationInfo(service="checkins", operation="get_question", is_mutation=False, resource_id=question_id), "GET", f"/questions/{question_id}", + operation="GetQuestion", ) async def update_question( @@ -213,6 +228,7 @@ async def list_answers(self, *, question_id: int) -> ListResult: return await self._request_paginated( OperationInfo(service="checkins", operation="list_answers", is_mutation=False, resource_id=question_id), f"/questions/{question_id}/answers.json", + operation="ListAnswers", ) async def create_answer(self, *, question_id: int, content: str, group_on: str | None = None) -> dict[str, Any]: @@ -228,12 +244,14 @@ async def answerers(self, *, question_id: int) -> ListResult: return await self._request_paginated( OperationInfo(service="checkins", operation="answerers", is_mutation=False, resource_id=question_id), f"/questions/{question_id}/answers/by.json", + operation="ListQuestionAnswerers", ) async def by_person(self, *, question_id: int, person_id: int) -> ListResult: return await self._request_paginated( OperationInfo(service="checkins", operation="by_person", is_mutation=False, resource_id=person_id), f"/questions/{question_id}/answers/by/{person_id}", + operation="GetAnswersByPerson", ) async def update_notification_settings( diff --git a/python/src/basecamp/generated/services/client_approvals.py b/python/src/basecamp/generated/services/client_approvals.py index dfaad61fc..72fa0ce40 100644 --- a/python/src/basecamp/generated/services/client_approvals.py +++ b/python/src/basecamp/generated/services/client_approvals.py @@ -16,6 +16,7 @@ def list(self, *, sort: str | None = None, direction: str | None = None) -> List OperationInfo(service="clientapprovals", operation="list", is_mutation=False), "/client/approvals.json", params=self._compact(sort=sort, direction=direction), + operation="ListClientApprovals", ) def get(self, *, approval_id: int) -> dict[str, Any]: @@ -23,6 +24,7 @@ def get(self, *, approval_id: int) -> dict[str, Any]: OperationInfo(service="clientapprovals", operation="get", is_mutation=False, resource_id=approval_id), "GET", f"/client/approvals/{approval_id}", + operation="GetClientApproval", ) @@ -32,6 +34,7 @@ async def list(self, *, sort: str | None = None, direction: str | None = None) - OperationInfo(service="clientapprovals", operation="list", is_mutation=False), "/client/approvals.json", params=self._compact(sort=sort, direction=direction), + operation="ListClientApprovals", ) async def get(self, *, approval_id: int) -> dict[str, Any]: @@ -39,4 +42,5 @@ async def get(self, *, approval_id: int) -> dict[str, Any]: OperationInfo(service="clientapprovals", operation="get", is_mutation=False, resource_id=approval_id), "GET", f"/client/approvals/{approval_id}", + operation="GetClientApproval", ) diff --git a/python/src/basecamp/generated/services/client_correspondences.py b/python/src/basecamp/generated/services/client_correspondences.py index 866a24554..45bc030a6 100644 --- a/python/src/basecamp/generated/services/client_correspondences.py +++ b/python/src/basecamp/generated/services/client_correspondences.py @@ -16,6 +16,7 @@ def list(self, *, sort: str | None = None, direction: str | None = None) -> List OperationInfo(service="clientcorrespondences", operation="list", is_mutation=False), "/client/correspondences.json", params=self._compact(sort=sort, direction=direction), + operation="ListClientCorrespondences", ) def get(self, *, correspondence_id: int) -> dict[str, Any]: @@ -25,6 +26,7 @@ def get(self, *, correspondence_id: int) -> dict[str, Any]: ), "GET", f"/client/correspondences/{correspondence_id}", + operation="GetClientCorrespondence", ) @@ -34,6 +36,7 @@ async def list(self, *, sort: str | None = None, direction: str | None = None) - OperationInfo(service="clientcorrespondences", operation="list", is_mutation=False), "/client/correspondences.json", params=self._compact(sort=sort, direction=direction), + operation="ListClientCorrespondences", ) async def get(self, *, correspondence_id: int) -> dict[str, Any]: @@ -43,4 +46,5 @@ async def get(self, *, correspondence_id: int) -> dict[str, Any]: ), "GET", f"/client/correspondences/{correspondence_id}", + operation="GetClientCorrespondence", ) diff --git a/python/src/basecamp/generated/services/client_replies.py b/python/src/basecamp/generated/services/client_replies.py index 754c9d8b4..28f0d58dc 100644 --- a/python/src/basecamp/generated/services/client_replies.py +++ b/python/src/basecamp/generated/services/client_replies.py @@ -15,6 +15,7 @@ def list(self, *, recording_id: int) -> ListResult: return self._request_paginated( OperationInfo(service="clientreplies", operation="list", is_mutation=False, resource_id=recording_id), f"/client/recordings/{recording_id}/replies.json", + operation="ListClientReplies", ) def get(self, *, recording_id: int, reply_id: int) -> dict[str, Any]: @@ -22,6 +23,7 @@ def get(self, *, recording_id: int, reply_id: int) -> dict[str, Any]: OperationInfo(service="clientreplies", operation="get", is_mutation=False, resource_id=reply_id), "GET", f"/client/recordings/{recording_id}/replies/{reply_id}", + operation="GetClientReply", ) @@ -30,6 +32,7 @@ async def list(self, *, recording_id: int) -> ListResult: return await self._request_paginated( OperationInfo(service="clientreplies", operation="list", is_mutation=False, resource_id=recording_id), f"/client/recordings/{recording_id}/replies.json", + operation="ListClientReplies", ) async def get(self, *, recording_id: int, reply_id: int) -> dict[str, Any]: @@ -37,4 +40,5 @@ async def get(self, *, recording_id: int, reply_id: int) -> dict[str, Any]: OperationInfo(service="clientreplies", operation="get", is_mutation=False, resource_id=reply_id), "GET", f"/client/recordings/{recording_id}/replies/{reply_id}", + operation="GetClientReply", ) diff --git a/python/src/basecamp/generated/services/comments.py b/python/src/basecamp/generated/services/comments.py index adee6891e..68bb762d4 100644 --- a/python/src/basecamp/generated/services/comments.py +++ b/python/src/basecamp/generated/services/comments.py @@ -16,6 +16,7 @@ def get(self, *, comment_id: int) -> dict[str, Any]: OperationInfo(service="comments", operation="get", is_mutation=False, resource_id=comment_id), "GET", f"/comments/{comment_id}", + operation="GetComment", ) def update(self, *, comment_id: int, content: str) -> dict[str, Any]: @@ -31,6 +32,7 @@ def list(self, *, recording_id: int) -> ListResult: return self._request_paginated( OperationInfo(service="comments", operation="list", is_mutation=False, resource_id=recording_id), f"/recordings/{recording_id}/comments.json", + operation="ListComments", ) def create(self, *, recording_id: int, content: str) -> dict[str, Any]: @@ -49,6 +51,7 @@ async def get(self, *, comment_id: int) -> dict[str, Any]: OperationInfo(service="comments", operation="get", is_mutation=False, resource_id=comment_id), "GET", f"/comments/{comment_id}", + operation="GetComment", ) async def update(self, *, comment_id: int, content: str) -> dict[str, Any]: @@ -64,6 +67,7 @@ async def list(self, *, recording_id: int) -> ListResult: return await self._request_paginated( OperationInfo(service="comments", operation="list", is_mutation=False, resource_id=recording_id), f"/recordings/{recording_id}/comments.json", + operation="ListComments", ) async def create(self, *, recording_id: int, content: str) -> dict[str, Any]: diff --git a/python/src/basecamp/generated/services/documents.py b/python/src/basecamp/generated/services/documents.py index 4a3eaf4fd..f08295ea1 100644 --- a/python/src/basecamp/generated/services/documents.py +++ b/python/src/basecamp/generated/services/documents.py @@ -16,6 +16,7 @@ def get(self, *, document_id: int) -> dict[str, Any]: OperationInfo(service="documents", operation="get", is_mutation=False, resource_id=document_id), "GET", f"/documents/{document_id}", + operation="GetDocument", ) def update(self, *, document_id: int, title: str | None = None, content: str | None = None) -> dict[str, Any]: @@ -31,6 +32,7 @@ def list(self, *, vault_id: int) -> ListResult: return self._request_paginated( OperationInfo(service="documents", operation="list", is_mutation=False, resource_id=vault_id), f"/vaults/{vault_id}/documents.json", + operation="ListDocuments", ) def create( @@ -64,6 +66,7 @@ async def get(self, *, document_id: int) -> dict[str, Any]: OperationInfo(service="documents", operation="get", is_mutation=False, resource_id=document_id), "GET", f"/documents/{document_id}", + operation="GetDocument", ) async def update(self, *, document_id: int, title: str | None = None, content: str | None = None) -> dict[str, Any]: @@ -79,6 +82,7 @@ async def list(self, *, vault_id: int) -> ListResult: return await self._request_paginated( OperationInfo(service="documents", operation="list", is_mutation=False, resource_id=vault_id), f"/vaults/{vault_id}/documents.json", + operation="ListDocuments", ) async def create( diff --git a/python/src/basecamp/generated/services/events.py b/python/src/basecamp/generated/services/events.py index 84e6e3e21..9f1a079d4 100644 --- a/python/src/basecamp/generated/services/events.py +++ b/python/src/basecamp/generated/services/events.py @@ -15,6 +15,7 @@ def list(self, *, recording_id: int) -> ListResult: return self._request_paginated( OperationInfo(service="events", operation="list", is_mutation=False, resource_id=recording_id), f"/recordings/{recording_id}/events.json", + operation="ListEvents", ) @@ -23,4 +24,5 @@ async def list(self, *, recording_id: int) -> ListResult: return await self._request_paginated( OperationInfo(service="events", operation="list", is_mutation=False, resource_id=recording_id), f"/recordings/{recording_id}/events.json", + operation="ListEvents", ) diff --git a/python/src/basecamp/generated/services/everything.py b/python/src/basecamp/generated/services/everything.py index 50b5bc00e..427e3cfe7 100644 --- a/python/src/basecamp/generated/services/everything.py +++ b/python/src/basecamp/generated/services/everything.py @@ -16,6 +16,7 @@ def get_everything_boosts(self, *, page: int | None = None) -> ListResult: OperationInfo(service="everything", operation="get_everything_boosts", is_mutation=False), "/boosts.json", params=self._compact(page=page), + operation="GetEverythingBoosts", ) def get_everything_completed_cards(self, *, page: int | None = None) -> ListResult: @@ -23,6 +24,7 @@ def get_everything_completed_cards(self, *, page: int | None = None) -> ListResu OperationInfo(service="everything", operation="get_everything_completed_cards", is_mutation=False), "/cards/completed.json", params=self._compact(page=page), + operation="GetEverythingCompletedCards", ) def get_everything_no_due_date_cards(self, *, page: int | None = None) -> ListResult: @@ -30,6 +32,7 @@ def get_everything_no_due_date_cards(self, *, page: int | None = None) -> ListRe OperationInfo(service="everything", operation="get_everything_no_due_date_cards", is_mutation=False), "/cards/no_due_date.json", params=self._compact(page=page), + operation="GetEverythingNoDueDateCards", ) def get_everything_not_now_cards(self, *, page: int | None = None) -> ListResult: @@ -37,6 +40,7 @@ def get_everything_not_now_cards(self, *, page: int | None = None) -> ListResult OperationInfo(service="everything", operation="get_everything_not_now_cards", is_mutation=False), "/cards/not_now.json", params=self._compact(page=page), + operation="GetEverythingNotNowCards", ) def get_everything_open_cards(self, *, page: int | None = None) -> ListResult: @@ -44,12 +48,14 @@ def get_everything_open_cards(self, *, page: int | None = None) -> ListResult: OperationInfo(service="everything", operation="get_everything_open_cards", is_mutation=False), "/cards/open.json", params=self._compact(page=page), + operation="GetEverythingOpenCards", ) def get_everything_overdue_cards(self) -> ListResult: return self._request_list( OperationInfo(service="everything", operation="get_everything_overdue_cards", is_mutation=False), "/cards/overdue.json", + operation="GetEverythingOverdueCards", ) def get_everything_unassigned_cards(self, *, page: int | None = None) -> ListResult: @@ -57,6 +63,7 @@ def get_everything_unassigned_cards(self, *, page: int | None = None) -> ListRes OperationInfo(service="everything", operation="get_everything_unassigned_cards", is_mutation=False), "/cards/unassigned.json", params=self._compact(page=page), + operation="GetEverythingUnassignedCards", ) def get_everything_checkins(self, *, page: int | None = None) -> ListResult: @@ -64,6 +71,7 @@ def get_everything_checkins(self, *, page: int | None = None) -> ListResult: OperationInfo(service="everything", operation="get_everything_checkins", is_mutation=False), "/checkins.json", params=self._compact(page=page), + operation="GetEverythingCheckins", ) def get_everything_comments(self, *, page: int | None = None) -> ListResult: @@ -71,6 +79,7 @@ def get_everything_comments(self, *, page: int | None = None) -> ListResult: OperationInfo(service="everything", operation="get_everything_comments", is_mutation=False), "/comments.json", params=self._compact(page=page), + operation="GetEverythingComments", ) def get_everything_files( @@ -80,6 +89,7 @@ def get_everything_files( OperationInfo(service="everything", operation="get_everything_files", is_mutation=False), "/files.json", params={k: v for k, v in {"kind": kind, "people_ids[]": people_ids, "page": page}.items() if v is not None}, + operation="GetEverythingFiles", ) def get_everything_forwards(self, *, page: int | None = None) -> ListResult: @@ -87,6 +97,7 @@ def get_everything_forwards(self, *, page: int | None = None) -> ListResult: OperationInfo(service="everything", operation="get_everything_forwards", is_mutation=False), "/forwards.json", params=self._compact(page=page), + operation="GetEverythingForwards", ) def get_everything_messages(self, *, page: int | None = None) -> ListResult: @@ -94,6 +105,7 @@ def get_everything_messages(self, *, page: int | None = None) -> ListResult: OperationInfo(service="everything", operation="get_everything_messages", is_mutation=False), "/messages.json", params=self._compact(page=page), + operation="GetEverythingMessages", ) def get_everything_completed_todos(self, *, page: int | None = None) -> ListResult: @@ -101,6 +113,7 @@ def get_everything_completed_todos(self, *, page: int | None = None) -> ListResu OperationInfo(service="everything", operation="get_everything_completed_todos", is_mutation=False), "/todos/completed.json", params=self._compact(page=page), + operation="GetEverythingCompletedTodos", ) def get_everything_no_due_date_todos(self, *, page: int | None = None) -> ListResult: @@ -108,6 +121,7 @@ def get_everything_no_due_date_todos(self, *, page: int | None = None) -> ListRe OperationInfo(service="everything", operation="get_everything_no_due_date_todos", is_mutation=False), "/todos/no_due_date.json", params=self._compact(page=page), + operation="GetEverythingNoDueDateTodos", ) def get_everything_open_todos(self, *, page: int | None = None) -> ListResult: @@ -115,12 +129,14 @@ def get_everything_open_todos(self, *, page: int | None = None) -> ListResult: OperationInfo(service="everything", operation="get_everything_open_todos", is_mutation=False), "/todos/open.json", params=self._compact(page=page), + operation="GetEverythingOpenTodos", ) def get_everything_overdue_todos(self) -> ListResult: return self._request_list( OperationInfo(service="everything", operation="get_everything_overdue_todos", is_mutation=False), "/todos/overdue.json", + operation="GetEverythingOverdueTodos", ) def get_everything_unassigned_todos(self, *, page: int | None = None) -> ListResult: @@ -128,6 +144,7 @@ def get_everything_unassigned_todos(self, *, page: int | None = None) -> ListRes OperationInfo(service="everything", operation="get_everything_unassigned_todos", is_mutation=False), "/todos/unassigned.json", params=self._compact(page=page), + operation="GetEverythingUnassignedTodos", ) @@ -137,6 +154,7 @@ async def get_everything_boosts(self, *, page: int | None = None) -> ListResult: OperationInfo(service="everything", operation="get_everything_boosts", is_mutation=False), "/boosts.json", params=self._compact(page=page), + operation="GetEverythingBoosts", ) async def get_everything_completed_cards(self, *, page: int | None = None) -> ListResult: @@ -144,6 +162,7 @@ async def get_everything_completed_cards(self, *, page: int | None = None) -> Li OperationInfo(service="everything", operation="get_everything_completed_cards", is_mutation=False), "/cards/completed.json", params=self._compact(page=page), + operation="GetEverythingCompletedCards", ) async def get_everything_no_due_date_cards(self, *, page: int | None = None) -> ListResult: @@ -151,6 +170,7 @@ async def get_everything_no_due_date_cards(self, *, page: int | None = None) -> OperationInfo(service="everything", operation="get_everything_no_due_date_cards", is_mutation=False), "/cards/no_due_date.json", params=self._compact(page=page), + operation="GetEverythingNoDueDateCards", ) async def get_everything_not_now_cards(self, *, page: int | None = None) -> ListResult: @@ -158,6 +178,7 @@ async def get_everything_not_now_cards(self, *, page: int | None = None) -> List OperationInfo(service="everything", operation="get_everything_not_now_cards", is_mutation=False), "/cards/not_now.json", params=self._compact(page=page), + operation="GetEverythingNotNowCards", ) async def get_everything_open_cards(self, *, page: int | None = None) -> ListResult: @@ -165,12 +186,14 @@ async def get_everything_open_cards(self, *, page: int | None = None) -> ListRes OperationInfo(service="everything", operation="get_everything_open_cards", is_mutation=False), "/cards/open.json", params=self._compact(page=page), + operation="GetEverythingOpenCards", ) async def get_everything_overdue_cards(self) -> ListResult: return await self._request_list( OperationInfo(service="everything", operation="get_everything_overdue_cards", is_mutation=False), "/cards/overdue.json", + operation="GetEverythingOverdueCards", ) async def get_everything_unassigned_cards(self, *, page: int | None = None) -> ListResult: @@ -178,6 +201,7 @@ async def get_everything_unassigned_cards(self, *, page: int | None = None) -> L OperationInfo(service="everything", operation="get_everything_unassigned_cards", is_mutation=False), "/cards/unassigned.json", params=self._compact(page=page), + operation="GetEverythingUnassignedCards", ) async def get_everything_checkins(self, *, page: int | None = None) -> ListResult: @@ -185,6 +209,7 @@ async def get_everything_checkins(self, *, page: int | None = None) -> ListResul OperationInfo(service="everything", operation="get_everything_checkins", is_mutation=False), "/checkins.json", params=self._compact(page=page), + operation="GetEverythingCheckins", ) async def get_everything_comments(self, *, page: int | None = None) -> ListResult: @@ -192,6 +217,7 @@ async def get_everything_comments(self, *, page: int | None = None) -> ListResul OperationInfo(service="everything", operation="get_everything_comments", is_mutation=False), "/comments.json", params=self._compact(page=page), + operation="GetEverythingComments", ) async def get_everything_files( @@ -201,6 +227,7 @@ async def get_everything_files( OperationInfo(service="everything", operation="get_everything_files", is_mutation=False), "/files.json", params={k: v for k, v in {"kind": kind, "people_ids[]": people_ids, "page": page}.items() if v is not None}, + operation="GetEverythingFiles", ) async def get_everything_forwards(self, *, page: int | None = None) -> ListResult: @@ -208,6 +235,7 @@ async def get_everything_forwards(self, *, page: int | None = None) -> ListResul OperationInfo(service="everything", operation="get_everything_forwards", is_mutation=False), "/forwards.json", params=self._compact(page=page), + operation="GetEverythingForwards", ) async def get_everything_messages(self, *, page: int | None = None) -> ListResult: @@ -215,6 +243,7 @@ async def get_everything_messages(self, *, page: int | None = None) -> ListResul OperationInfo(service="everything", operation="get_everything_messages", is_mutation=False), "/messages.json", params=self._compact(page=page), + operation="GetEverythingMessages", ) async def get_everything_completed_todos(self, *, page: int | None = None) -> ListResult: @@ -222,6 +251,7 @@ async def get_everything_completed_todos(self, *, page: int | None = None) -> Li OperationInfo(service="everything", operation="get_everything_completed_todos", is_mutation=False), "/todos/completed.json", params=self._compact(page=page), + operation="GetEverythingCompletedTodos", ) async def get_everything_no_due_date_todos(self, *, page: int | None = None) -> ListResult: @@ -229,6 +259,7 @@ async def get_everything_no_due_date_todos(self, *, page: int | None = None) -> OperationInfo(service="everything", operation="get_everything_no_due_date_todos", is_mutation=False), "/todos/no_due_date.json", params=self._compact(page=page), + operation="GetEverythingNoDueDateTodos", ) async def get_everything_open_todos(self, *, page: int | None = None) -> ListResult: @@ -236,12 +267,14 @@ async def get_everything_open_todos(self, *, page: int | None = None) -> ListRes OperationInfo(service="everything", operation="get_everything_open_todos", is_mutation=False), "/todos/open.json", params=self._compact(page=page), + operation="GetEverythingOpenTodos", ) async def get_everything_overdue_todos(self) -> ListResult: return await self._request_list( OperationInfo(service="everything", operation="get_everything_overdue_todos", is_mutation=False), "/todos/overdue.json", + operation="GetEverythingOverdueTodos", ) async def get_everything_unassigned_todos(self, *, page: int | None = None) -> ListResult: @@ -249,4 +282,5 @@ async def get_everything_unassigned_todos(self, *, page: int | None = None) -> L OperationInfo(service="everything", operation="get_everything_unassigned_todos", is_mutation=False), "/todos/unassigned.json", params=self._compact(page=page), + operation="GetEverythingUnassignedTodos", ) diff --git a/python/src/basecamp/generated/services/forwards.py b/python/src/basecamp/generated/services/forwards.py index b4171a323..63038baa6 100644 --- a/python/src/basecamp/generated/services/forwards.py +++ b/python/src/basecamp/generated/services/forwards.py @@ -16,12 +16,14 @@ def get(self, *, forward_id: int) -> dict[str, Any]: OperationInfo(service="forwards", operation="get", is_mutation=False, resource_id=forward_id), "GET", f"/inbox_forwards/{forward_id}", + operation="GetForward", ) def list_replies(self, *, forward_id: int) -> ListResult: return self._request_paginated( OperationInfo(service="forwards", operation="list_replies", is_mutation=False, resource_id=forward_id), f"/inbox_forwards/{forward_id}/replies.json", + operation="ListForwardReplies", ) def create_reply(self, *, forward_id: int, content: str) -> dict[str, Any]: @@ -38,6 +40,7 @@ def get_reply(self, *, forward_id: int, reply_id: int) -> dict[str, Any]: OperationInfo(service="forwards", operation="get_reply", is_mutation=False, resource_id=reply_id), "GET", f"/inbox_forwards/{forward_id}/replies/{reply_id}", + operation="GetForwardReply", ) def get_inbox(self, *, inbox_id: int) -> dict[str, Any]: @@ -45,6 +48,7 @@ def get_inbox(self, *, inbox_id: int) -> dict[str, Any]: OperationInfo(service="forwards", operation="get_inbox", is_mutation=False, resource_id=inbox_id), "GET", f"/inboxes/{inbox_id}", + operation="GetInbox", ) def list(self, *, inbox_id: int, sort: str | None = None, direction: str | None = None) -> ListResult: @@ -52,6 +56,7 @@ def list(self, *, inbox_id: int, sort: str | None = None, direction: str | None OperationInfo(service="forwards", operation="list", is_mutation=False, resource_id=inbox_id), f"/inboxes/{inbox_id}/forwards.json", params=self._compact(sort=sort, direction=direction), + operation="ListForwards", ) @@ -61,12 +66,14 @@ async def get(self, *, forward_id: int) -> dict[str, Any]: OperationInfo(service="forwards", operation="get", is_mutation=False, resource_id=forward_id), "GET", f"/inbox_forwards/{forward_id}", + operation="GetForward", ) async def list_replies(self, *, forward_id: int) -> ListResult: return await self._request_paginated( OperationInfo(service="forwards", operation="list_replies", is_mutation=False, resource_id=forward_id), f"/inbox_forwards/{forward_id}/replies.json", + operation="ListForwardReplies", ) async def create_reply(self, *, forward_id: int, content: str) -> dict[str, Any]: @@ -83,6 +90,7 @@ async def get_reply(self, *, forward_id: int, reply_id: int) -> dict[str, Any]: OperationInfo(service="forwards", operation="get_reply", is_mutation=False, resource_id=reply_id), "GET", f"/inbox_forwards/{forward_id}/replies/{reply_id}", + operation="GetForwardReply", ) async def get_inbox(self, *, inbox_id: int) -> dict[str, Any]: @@ -90,6 +98,7 @@ async def get_inbox(self, *, inbox_id: int) -> dict[str, Any]: OperationInfo(service="forwards", operation="get_inbox", is_mutation=False, resource_id=inbox_id), "GET", f"/inboxes/{inbox_id}", + operation="GetInbox", ) async def list(self, *, inbox_id: int, sort: str | None = None, direction: str | None = None) -> ListResult: @@ -97,4 +106,5 @@ async def list(self, *, inbox_id: int, sort: str | None = None, direction: str | OperationInfo(service="forwards", operation="list", is_mutation=False, resource_id=inbox_id), f"/inboxes/{inbox_id}/forwards.json", params=self._compact(sort=sort, direction=direction), + operation="ListForwards", ) diff --git a/python/src/basecamp/generated/services/gauges.py b/python/src/basecamp/generated/services/gauges.py index 5a92a6785..17cbbd771 100644 --- a/python/src/basecamp/generated/services/gauges.py +++ b/python/src/basecamp/generated/services/gauges.py @@ -16,6 +16,7 @@ def get_gauge_needle(self, *, needle_id: int) -> dict[str, Any]: OperationInfo(service="gauges", operation="get_gauge_needle", is_mutation=False, resource_id=needle_id), "GET", f"/gauge_needles/{needle_id}", + operation="GetGaugeNeedle", ) def update_gauge_needle(self, *, needle_id: int, gauge_needle: dict | None = None) -> dict[str, Any]: @@ -48,6 +49,7 @@ def list_gauge_needles(self, *, project_id: int) -> ListResult: return self._request_paginated( OperationInfo(service="gauges", operation="list_gauge_needles", is_mutation=False, project_id=project_id), f"/projects/{project_id}/gauge/needles.json", + operation="ListGaugeNeedles", ) def create_gauge_needle( @@ -66,6 +68,7 @@ def list_gauges(self, *, bucket_ids: str | None = None) -> ListResult: OperationInfo(service="gauges", operation="list_gauges", is_mutation=False), "/reports/gauges.json", params=self._compact(bucket_ids=bucket_ids), + operation="ListGauges", ) @@ -75,6 +78,7 @@ async def get_gauge_needle(self, *, needle_id: int) -> dict[str, Any]: OperationInfo(service="gauges", operation="get_gauge_needle", is_mutation=False, resource_id=needle_id), "GET", f"/gauge_needles/{needle_id}", + operation="GetGaugeNeedle", ) async def update_gauge_needle(self, *, needle_id: int, gauge_needle: dict | None = None) -> dict[str, Any]: @@ -107,6 +111,7 @@ async def list_gauge_needles(self, *, project_id: int) -> ListResult: return await self._request_paginated( OperationInfo(service="gauges", operation="list_gauge_needles", is_mutation=False, project_id=project_id), f"/projects/{project_id}/gauge/needles.json", + operation="ListGaugeNeedles", ) async def create_gauge_needle( @@ -125,4 +130,5 @@ async def list_gauges(self, *, bucket_ids: str | None = None) -> ListResult: OperationInfo(service="gauges", operation="list_gauges", is_mutation=False), "/reports/gauges.json", params=self._compact(bucket_ids=bucket_ids), + operation="ListGauges", ) diff --git a/python/src/basecamp/generated/services/hill_charts.py b/python/src/basecamp/generated/services/hill_charts.py index 9cd214d28..abb56110e 100644 --- a/python/src/basecamp/generated/services/hill_charts.py +++ b/python/src/basecamp/generated/services/hill_charts.py @@ -16,6 +16,7 @@ def get(self, *, todoset_id: int) -> dict[str, Any]: OperationInfo(service="hillcharts", operation="get", is_mutation=False, resource_id=todoset_id), "GET", f"/todosets/{todoset_id}/hill.json", + operation="GetHillChart", ) def update_settings( @@ -36,6 +37,7 @@ async def get(self, *, todoset_id: int) -> dict[str, Any]: OperationInfo(service="hillcharts", operation="get", is_mutation=False, resource_id=todoset_id), "GET", f"/todosets/{todoset_id}/hill.json", + operation="GetHillChart", ) async def update_settings( diff --git a/python/src/basecamp/generated/services/message_boards.py b/python/src/basecamp/generated/services/message_boards.py index 4fe33c1ac..06c005a0c 100644 --- a/python/src/basecamp/generated/services/message_boards.py +++ b/python/src/basecamp/generated/services/message_boards.py @@ -16,6 +16,7 @@ def get(self, *, board_id: int) -> dict[str, Any]: OperationInfo(service="messageboards", operation="get", is_mutation=False, resource_id=board_id), "GET", f"/message_boards/{board_id}", + operation="GetMessageBoard", ) @@ -25,4 +26,5 @@ async def get(self, *, board_id: int) -> dict[str, Any]: OperationInfo(service="messageboards", operation="get", is_mutation=False, resource_id=board_id), "GET", f"/message_boards/{board_id}", + operation="GetMessageBoard", ) diff --git a/python/src/basecamp/generated/services/message_types.py b/python/src/basecamp/generated/services/message_types.py index bd5f05053..15d102dcc 100644 --- a/python/src/basecamp/generated/services/message_types.py +++ b/python/src/basecamp/generated/services/message_types.py @@ -15,6 +15,7 @@ def list(self, *, bucket_id: int) -> ListResult: return self._request_paginated( OperationInfo(service="messagetypes", operation="list", is_mutation=False, project_id=bucket_id), f"/buckets/{bucket_id}/categories.json", + operation="ListMessageTypes", ) def create(self, *, bucket_id: int, name: str, icon: str) -> dict[str, Any]: @@ -33,6 +34,7 @@ def get(self, *, bucket_id: int, type_id: int) -> dict[str, Any]: ), "GET", f"/buckets/{bucket_id}/categories/{type_id}", + operation="GetMessageType", ) def update( @@ -64,6 +66,7 @@ async def list(self, *, bucket_id: int) -> ListResult: return await self._request_paginated( OperationInfo(service="messagetypes", operation="list", is_mutation=False, project_id=bucket_id), f"/buckets/{bucket_id}/categories.json", + operation="ListMessageTypes", ) async def create(self, *, bucket_id: int, name: str, icon: str) -> dict[str, Any]: @@ -82,6 +85,7 @@ async def get(self, *, bucket_id: int, type_id: int) -> dict[str, Any]: ), "GET", f"/buckets/{bucket_id}/categories/{type_id}", + operation="GetMessageType", ) async def update( diff --git a/python/src/basecamp/generated/services/messages.py b/python/src/basecamp/generated/services/messages.py index d8a4e1546..72afbed29 100644 --- a/python/src/basecamp/generated/services/messages.py +++ b/python/src/basecamp/generated/services/messages.py @@ -16,6 +16,7 @@ def list(self, *, board_id: int, sort: str | None = None, direction: str | None OperationInfo(service="messages", operation="list", is_mutation=False, resource_id=board_id), f"/message_boards/{board_id}/messages.json", params=self._compact(sort=sort, direction=direction), + operation="ListMessages", ) def create( @@ -49,6 +50,7 @@ def get(self, *, message_id: int) -> dict[str, Any]: OperationInfo(service="messages", operation="get", is_mutation=False, resource_id=message_id), "GET", f"/messages/{message_id}", + operation="GetMessage", ) def update( @@ -91,6 +93,7 @@ async def list(self, *, board_id: int, sort: str | None = None, direction: str | OperationInfo(service="messages", operation="list", is_mutation=False, resource_id=board_id), f"/message_boards/{board_id}/messages.json", params=self._compact(sort=sort, direction=direction), + operation="ListMessages", ) async def create( @@ -124,6 +127,7 @@ async def get(self, *, message_id: int) -> dict[str, Any]: OperationInfo(service="messages", operation="get", is_mutation=False, resource_id=message_id), "GET", f"/messages/{message_id}", + operation="GetMessage", ) async def update( diff --git a/python/src/basecamp/generated/services/my_assignments.py b/python/src/basecamp/generated/services/my_assignments.py index 16377a596..37346dd82 100644 --- a/python/src/basecamp/generated/services/my_assignments.py +++ b/python/src/basecamp/generated/services/my_assignments.py @@ -16,12 +16,14 @@ def get_my_assignments(self) -> dict[str, Any]: OperationInfo(service="myassignments", operation="get_my_assignments", is_mutation=False), "GET", "/my/assignments.json", + operation="GetMyAssignments", ) def get_my_completed_assignments(self) -> ListResult: return self._request_list( OperationInfo(service="myassignments", operation="get_my_completed_assignments", is_mutation=False), "/my/assignments/completed.json", + operation="GetMyCompletedAssignments", ) def get_my_due_assignments(self, *, scope: str | None = None) -> ListResult: @@ -29,6 +31,7 @@ def get_my_due_assignments(self, *, scope: str | None = None) -> ListResult: OperationInfo(service="myassignments", operation="get_my_due_assignments", is_mutation=False), "/my/assignments/due.json", params=self._compact(scope=scope), + operation="GetMyDueAssignments", ) @@ -38,12 +41,14 @@ async def get_my_assignments(self) -> dict[str, Any]: OperationInfo(service="myassignments", operation="get_my_assignments", is_mutation=False), "GET", "/my/assignments.json", + operation="GetMyAssignments", ) async def get_my_completed_assignments(self) -> ListResult: return await self._request_list( OperationInfo(service="myassignments", operation="get_my_completed_assignments", is_mutation=False), "/my/assignments/completed.json", + operation="GetMyCompletedAssignments", ) async def get_my_due_assignments(self, *, scope: str | None = None) -> ListResult: @@ -51,4 +56,5 @@ async def get_my_due_assignments(self, *, scope: str | None = None) -> ListResul OperationInfo(service="myassignments", operation="get_my_due_assignments", is_mutation=False), "/my/assignments/due.json", params=self._compact(scope=scope), + operation="GetMyDueAssignments", ) diff --git a/python/src/basecamp/generated/services/my_notifications.py b/python/src/basecamp/generated/services/my_notifications.py index 2ba8a1f55..ff7f410d7 100644 --- a/python/src/basecamp/generated/services/my_notifications.py +++ b/python/src/basecamp/generated/services/my_notifications.py @@ -17,6 +17,7 @@ def get_my_notifications(self, *, page: int | None = None, limit_bubble_ups: boo "GET", "/my/readings.json", params=self._compact(page=page, limit_bubble_ups=limit_bubble_ups), + operation="GetMyNotifications", ) def get_bubble_ups(self, *, page: int | None = None) -> ListResult: @@ -24,6 +25,7 @@ def get_bubble_ups(self, *, page: int | None = None) -> ListResult: OperationInfo(service="mynotifications", operation="get_bubble_ups", is_mutation=False), "/my/readings/bubble_ups.json", params=self._compact(page=page), + operation="GetBubbleUps", ) def mark_as_read(self, *, readables: list[str]) -> None: @@ -45,6 +47,7 @@ async def get_my_notifications( "GET", "/my/readings.json", params=self._compact(page=page, limit_bubble_ups=limit_bubble_ups), + operation="GetMyNotifications", ) async def get_bubble_ups(self, *, page: int | None = None) -> ListResult: @@ -52,6 +55,7 @@ async def get_bubble_ups(self, *, page: int | None = None) -> ListResult: OperationInfo(service="mynotifications", operation="get_bubble_ups", is_mutation=False), "/my/readings/bubble_ups.json", params=self._compact(page=page), + operation="GetBubbleUps", ) async def mark_as_read(self, *, readables: list[str]) -> None: diff --git a/python/src/basecamp/generated/services/people.py b/python/src/basecamp/generated/services/people.py index 2ff534f98..6d53dcb95 100644 --- a/python/src/basecamp/generated/services/people.py +++ b/python/src/basecamp/generated/services/people.py @@ -13,7 +13,9 @@ class PeopleService(BaseService): def list_pingable(self) -> ListResult: return self._request_paginated( - OperationInfo(service="people", operation="list_pingable", is_mutation=False), "/circles/people.json" + OperationInfo(service="people", operation="list_pingable", is_mutation=False), + "/circles/people.json", + operation="ListPingablePeople", ) def get_my_preferences(self) -> dict[str, Any]: @@ -21,6 +23,7 @@ def get_my_preferences(self) -> dict[str, Any]: OperationInfo(service="people", operation="get_my_preferences", is_mutation=False), "GET", "/my/preferences.json", + operation="GetMyPreferences", ) def update_my_preferences(self, *, person: dict) -> dict[str, Any]: @@ -34,7 +37,10 @@ def update_my_preferences(self, *, person: dict) -> dict[str, Any]: def my_profile(self) -> dict[str, Any]: return self._request( - OperationInfo(service="people", operation="my_profile", is_mutation=False), "GET", "/my/profile.json" + OperationInfo(service="people", operation="my_profile", is_mutation=False), + "GET", + "/my/profile.json", + operation="GetMyProfile", ) def update_my_profile( @@ -68,7 +74,7 @@ def update_my_profile( def list(self) -> ListResult: return self._request_paginated( - OperationInfo(service="people", operation="list", is_mutation=False), "/people.json" + OperationInfo(service="people", operation="list", is_mutation=False), "/people.json", operation="ListPeople" ) def get(self, *, person_id: int) -> dict[str, Any]: @@ -76,6 +82,7 @@ def get(self, *, person_id: int) -> dict[str, Any]: OperationInfo(service="people", operation="get", is_mutation=False, resource_id=person_id), "GET", f"/people/{person_id}", + operation="GetPerson", ) def get_out_of_office(self, *, person_id: int) -> dict[str, Any]: @@ -83,6 +90,7 @@ def get_out_of_office(self, *, person_id: int) -> dict[str, Any]: OperationInfo(service="people", operation="get_out_of_office", is_mutation=False, resource_id=person_id), "GET", f"/people/{person_id}/out_of_office.json", + operation="GetOutOfOffice", ) def enable_out_of_office(self, *, person_id: int, out_of_office: dict) -> dict[str, Any]: @@ -106,6 +114,7 @@ def list_for_project(self, *, project_id: int) -> ListResult: return self._request_paginated( OperationInfo(service="people", operation="list_for_project", is_mutation=False, project_id=project_id), f"/projects/{project_id}/people.json", + operation="ListProjectPeople", ) def update_project_access( @@ -128,13 +137,16 @@ def list_assignable(self) -> ListResult: return self._request_list( OperationInfo(service="people", operation="list_assignable", is_mutation=False), "/reports/todos/assigned.json", + operation="ListAssignablePeople", ) class AsyncPeopleService(AsyncBaseService): async def list_pingable(self) -> ListResult: return await self._request_paginated( - OperationInfo(service="people", operation="list_pingable", is_mutation=False), "/circles/people.json" + OperationInfo(service="people", operation="list_pingable", is_mutation=False), + "/circles/people.json", + operation="ListPingablePeople", ) async def get_my_preferences(self) -> dict[str, Any]: @@ -142,6 +154,7 @@ async def get_my_preferences(self) -> dict[str, Any]: OperationInfo(service="people", operation="get_my_preferences", is_mutation=False), "GET", "/my/preferences.json", + operation="GetMyPreferences", ) async def update_my_preferences(self, *, person: dict) -> dict[str, Any]: @@ -155,7 +168,10 @@ async def update_my_preferences(self, *, person: dict) -> dict[str, Any]: async def my_profile(self) -> dict[str, Any]: return await self._request( - OperationInfo(service="people", operation="my_profile", is_mutation=False), "GET", "/my/profile.json" + OperationInfo(service="people", operation="my_profile", is_mutation=False), + "GET", + "/my/profile.json", + operation="GetMyProfile", ) async def update_my_profile( @@ -189,7 +205,7 @@ async def update_my_profile( async def list(self) -> ListResult: return await self._request_paginated( - OperationInfo(service="people", operation="list", is_mutation=False), "/people.json" + OperationInfo(service="people", operation="list", is_mutation=False), "/people.json", operation="ListPeople" ) async def get(self, *, person_id: int) -> dict[str, Any]: @@ -197,6 +213,7 @@ async def get(self, *, person_id: int) -> dict[str, Any]: OperationInfo(service="people", operation="get", is_mutation=False, resource_id=person_id), "GET", f"/people/{person_id}", + operation="GetPerson", ) async def get_out_of_office(self, *, person_id: int) -> dict[str, Any]: @@ -204,6 +221,7 @@ async def get_out_of_office(self, *, person_id: int) -> dict[str, Any]: OperationInfo(service="people", operation="get_out_of_office", is_mutation=False, resource_id=person_id), "GET", f"/people/{person_id}/out_of_office.json", + operation="GetOutOfOffice", ) async def enable_out_of_office(self, *, person_id: int, out_of_office: dict) -> dict[str, Any]: @@ -227,6 +245,7 @@ async def list_for_project(self, *, project_id: int) -> ListResult: return await self._request_paginated( OperationInfo(service="people", operation="list_for_project", is_mutation=False, project_id=project_id), f"/projects/{project_id}/people.json", + operation="ListProjectPeople", ) async def update_project_access( @@ -249,4 +268,5 @@ async def list_assignable(self) -> ListResult: return await self._request_list( OperationInfo(service="people", operation="list_assignable", is_mutation=False), "/reports/todos/assigned.json", + operation="ListAssignablePeople", ) diff --git a/python/src/basecamp/generated/services/projects.py b/python/src/basecamp/generated/services/projects.py index 340139846..c1c6fbb05 100644 --- a/python/src/basecamp/generated/services/projects.py +++ b/python/src/basecamp/generated/services/projects.py @@ -16,6 +16,7 @@ def list(self, *, status: str | None = None) -> ListResult: OperationInfo(service="projects", operation="list", is_mutation=False), "/projects.json", params=self._compact(status=status), + operation="ListProjects", ) def create(self, *, name: str, description: str | None = None) -> dict[str, Any]: @@ -32,6 +33,7 @@ def get(self, *, project_id: int) -> dict[str, Any]: OperationInfo(service="projects", operation="get", is_mutation=False, project_id=project_id), "GET", f"/projects/{project_id}", + operation="GetProject", ) def update( @@ -68,6 +70,7 @@ async def list(self, *, status: str | None = None) -> ListResult: OperationInfo(service="projects", operation="list", is_mutation=False), "/projects.json", params=self._compact(status=status), + operation="ListProjects", ) async def create(self, *, name: str, description: str | None = None) -> dict[str, Any]: @@ -84,6 +87,7 @@ async def get(self, *, project_id: int) -> dict[str, Any]: OperationInfo(service="projects", operation="get", is_mutation=False, project_id=project_id), "GET", f"/projects/{project_id}", + operation="GetProject", ) async def update( diff --git a/python/src/basecamp/generated/services/recordings.py b/python/src/basecamp/generated/services/recordings.py index a5597099a..6e7acbe11 100644 --- a/python/src/basecamp/generated/services/recordings.py +++ b/python/src/basecamp/generated/services/recordings.py @@ -24,6 +24,7 @@ def list( OperationInfo(service="recordings", operation="list", is_mutation=False), "/projects/recordings.json", params=self._compact(type=type, bucket=bucket, status=status, sort=sort, direction=direction), + operation="ListRecordings", ) def get(self, *, recording_id: int) -> dict[str, Any]: @@ -31,6 +32,7 @@ def get(self, *, recording_id: int) -> dict[str, Any]: OperationInfo(service="recordings", operation="get", is_mutation=False, resource_id=recording_id), "GET", f"/recordings/{recording_id}", + operation="GetRecording", ) def unarchive(self, *, recording_id: int) -> None: @@ -72,6 +74,7 @@ async def list( OperationInfo(service="recordings", operation="list", is_mutation=False), "/projects/recordings.json", params=self._compact(type=type, bucket=bucket, status=status, sort=sort, direction=direction), + operation="ListRecordings", ) async def get(self, *, recording_id: int) -> dict[str, Any]: @@ -79,6 +82,7 @@ async def get(self, *, recording_id: int) -> dict[str, Any]: OperationInfo(service="recordings", operation="get", is_mutation=False, resource_id=recording_id), "GET", f"/recordings/{recording_id}", + operation="GetRecording", ) async def unarchive(self, *, recording_id: int) -> None: diff --git a/python/src/basecamp/generated/services/reports.py b/python/src/basecamp/generated/services/reports.py index 6a011ba4d..564404f9f 100644 --- a/python/src/basecamp/generated/services/reports.py +++ b/python/src/basecamp/generated/services/reports.py @@ -13,7 +13,9 @@ class ReportsService(BaseService): def progress(self) -> ListResult: return self._request_paginated( - OperationInfo(service="reports", operation="progress", is_mutation=False), "/reports/progress.json" + OperationInfo(service="reports", operation="progress", is_mutation=False), + "/reports/progress.json", + operation="GetProgressReport", ) def upcoming(self, *, window_starts_on: str | None = None, window_ends_on: str | None = None) -> dict[str, Any]: @@ -22,6 +24,7 @@ def upcoming(self, *, window_starts_on: str | None = None, window_ends_on: str | "GET", "/reports/schedules/upcoming.json", params=self._compact(window_starts_on=window_starts_on, window_ends_on=window_ends_on), + operation="GetUpcomingSchedule", ) def assigned(self, *, person_id: int, group_by: str | None = None) -> dict[str, Any]: @@ -30,6 +33,7 @@ def assigned(self, *, person_id: int, group_by: str | None = None) -> dict[str, "GET", f"/reports/todos/assigned/{person_id}", params=self._compact(group_by=group_by), + operation="GetAssignedTodos", ) def overdue(self) -> dict[str, Any]: @@ -37,6 +41,7 @@ def overdue(self) -> dict[str, Any]: OperationInfo(service="reports", operation="overdue", is_mutation=False), "GET", "/reports/todos/overdue.json", + operation="GetOverdueTodos", ) def person_progress(self, *, person_id: int) -> dict[str, Any]: @@ -44,13 +49,16 @@ def person_progress(self, *, person_id: int) -> dict[str, Any]: OperationInfo(service="reports", operation="person_progress", is_mutation=False, resource_id=person_id), f"/reports/users/progress/{person_id}.json", "events", + operation="GetPersonProgress", ) class AsyncReportsService(AsyncBaseService): async def progress(self) -> ListResult: return await self._request_paginated( - OperationInfo(service="reports", operation="progress", is_mutation=False), "/reports/progress.json" + OperationInfo(service="reports", operation="progress", is_mutation=False), + "/reports/progress.json", + operation="GetProgressReport", ) async def upcoming( @@ -61,6 +69,7 @@ async def upcoming( "GET", "/reports/schedules/upcoming.json", params=self._compact(window_starts_on=window_starts_on, window_ends_on=window_ends_on), + operation="GetUpcomingSchedule", ) async def assigned(self, *, person_id: int, group_by: str | None = None) -> dict[str, Any]: @@ -69,6 +78,7 @@ async def assigned(self, *, person_id: int, group_by: str | None = None) -> dict "GET", f"/reports/todos/assigned/{person_id}", params=self._compact(group_by=group_by), + operation="GetAssignedTodos", ) async def overdue(self) -> dict[str, Any]: @@ -76,6 +86,7 @@ async def overdue(self) -> dict[str, Any]: OperationInfo(service="reports", operation="overdue", is_mutation=False), "GET", "/reports/todos/overdue.json", + operation="GetOverdueTodos", ) async def person_progress(self, *, person_id: int) -> dict[str, Any]: @@ -83,4 +94,5 @@ async def person_progress(self, *, person_id: int) -> dict[str, Any]: OperationInfo(service="reports", operation="person_progress", is_mutation=False, resource_id=person_id), f"/reports/users/progress/{person_id}.json", "events", + operation="GetPersonProgress", ) diff --git a/python/src/basecamp/generated/services/schedules.py b/python/src/basecamp/generated/services/schedules.py index 4087164b4..c80c5c193 100644 --- a/python/src/basecamp/generated/services/schedules.py +++ b/python/src/basecamp/generated/services/schedules.py @@ -16,6 +16,7 @@ def get_entry(self, *, entry_id: int) -> dict[str, Any]: OperationInfo(service="schedules", operation="get_entry", is_mutation=False, resource_id=entry_id), "GET", f"/schedule_entries/{entry_id}", + operation="GetScheduleEntry", ) def update_entry( @@ -53,6 +54,7 @@ def get_entry_occurrence(self, *, entry_id: int, date: str) -> dict[str, Any]: ), "GET", f"/schedule_entries/{entry_id}/occurrences/{date}", + operation="GetScheduleEntryOccurrence", ) def get(self, *, schedule_id: int) -> dict[str, Any]: @@ -60,6 +62,7 @@ def get(self, *, schedule_id: int) -> dict[str, Any]: OperationInfo(service="schedules", operation="get", is_mutation=False, resource_id=schedule_id), "GET", f"/schedules/{schedule_id}", + operation="GetSchedule", ) def update_settings(self, *, schedule_id: int, include_due_assignments: bool) -> dict[str, Any]: @@ -76,6 +79,7 @@ def list_entries(self, *, schedule_id: int, status: str | None = None) -> ListRe OperationInfo(service="schedules", operation="list_entries", is_mutation=False, resource_id=schedule_id), f"/schedules/{schedule_id}/entries.json", params=self._compact(status=status), + operation="ListScheduleEntries", ) def create_entry( @@ -117,6 +121,7 @@ async def get_entry(self, *, entry_id: int) -> dict[str, Any]: OperationInfo(service="schedules", operation="get_entry", is_mutation=False, resource_id=entry_id), "GET", f"/schedule_entries/{entry_id}", + operation="GetScheduleEntry", ) async def update_entry( @@ -154,6 +159,7 @@ async def get_entry_occurrence(self, *, entry_id: int, date: str) -> dict[str, A ), "GET", f"/schedule_entries/{entry_id}/occurrences/{date}", + operation="GetScheduleEntryOccurrence", ) async def get(self, *, schedule_id: int) -> dict[str, Any]: @@ -161,6 +167,7 @@ async def get(self, *, schedule_id: int) -> dict[str, Any]: OperationInfo(service="schedules", operation="get", is_mutation=False, resource_id=schedule_id), "GET", f"/schedules/{schedule_id}", + operation="GetSchedule", ) async def update_settings(self, *, schedule_id: int, include_due_assignments: bool) -> dict[str, Any]: @@ -177,6 +184,7 @@ async def list_entries(self, *, schedule_id: int, status: str | None = None) -> OperationInfo(service="schedules", operation="list_entries", is_mutation=False, resource_id=schedule_id), f"/schedules/{schedule_id}/entries.json", params=self._compact(status=status), + operation="ListScheduleEntries", ) async def create_entry( diff --git a/python/src/basecamp/generated/services/search.py b/python/src/basecamp/generated/services/search.py index 356ec3c5d..471555026 100644 --- a/python/src/basecamp/generated/services/search.py +++ b/python/src/basecamp/generated/services/search.py @@ -52,11 +52,15 @@ def search( }.items() if v is not None }, + operation="Search", ) def metadata(self) -> dict[str, Any]: return self._request( - OperationInfo(service="search", operation="metadata", is_mutation=False), "GET", "/searches/metadata.json" + OperationInfo(service="search", operation="metadata", is_mutation=False), + "GET", + "/searches/metadata.json", + operation="GetSearchMetadata", ) @@ -102,9 +106,13 @@ async def search( }.items() if v is not None }, + operation="Search", ) async def metadata(self) -> dict[str, Any]: return await self._request( - OperationInfo(service="search", operation="metadata", is_mutation=False), "GET", "/searches/metadata.json" + OperationInfo(service="search", operation="metadata", is_mutation=False), + "GET", + "/searches/metadata.json", + operation="GetSearchMetadata", ) diff --git a/python/src/basecamp/generated/services/subscriptions.py b/python/src/basecamp/generated/services/subscriptions.py index 71f4f7423..283f8d677 100644 --- a/python/src/basecamp/generated/services/subscriptions.py +++ b/python/src/basecamp/generated/services/subscriptions.py @@ -16,6 +16,7 @@ def get(self, *, recording_id: int) -> dict[str, Any]: OperationInfo(service="subscriptions", operation="get", is_mutation=False, resource_id=recording_id), "GET", f"/recordings/{recording_id}/subscription.json", + operation="GetSubscription", ) def subscribe(self, *, recording_id: int) -> dict[str, Any]: @@ -52,6 +53,7 @@ async def get(self, *, recording_id: int) -> dict[str, Any]: OperationInfo(service="subscriptions", operation="get", is_mutation=False, resource_id=recording_id), "GET", f"/recordings/{recording_id}/subscription.json", + operation="GetSubscription", ) async def subscribe(self, *, recording_id: int) -> dict[str, Any]: diff --git a/python/src/basecamp/generated/services/templates.py b/python/src/basecamp/generated/services/templates.py index 2a1f04cbb..93bef136a 100644 --- a/python/src/basecamp/generated/services/templates.py +++ b/python/src/basecamp/generated/services/templates.py @@ -16,6 +16,7 @@ def list(self, *, status: str | None = None) -> ListResult: OperationInfo(service="templates", operation="list", is_mutation=False), "/templates.json", params=self._compact(status=status), + operation="ListTemplates", ) def create(self, *, name: str, description: str | None = None) -> dict[str, Any]: @@ -32,6 +33,7 @@ def get(self, *, template_id: int) -> dict[str, Any]: OperationInfo(service="templates", operation="get", is_mutation=False, resource_id=template_id), "GET", f"/templates/{template_id}", + operation="GetTemplate", ) def update(self, *, template_id: int, name: str | None = None, description: str | None = None) -> dict[str, Any]: @@ -67,6 +69,7 @@ def get_construction(self, *, template_id: int, construction_id: int) -> dict[st ), "GET", f"/templates/{template_id}/project_constructions/{construction_id}", + operation="GetProjectConstruction", ) @@ -76,6 +79,7 @@ async def list(self, *, status: str | None = None) -> ListResult: OperationInfo(service="templates", operation="list", is_mutation=False), "/templates.json", params=self._compact(status=status), + operation="ListTemplates", ) async def create(self, *, name: str, description: str | None = None) -> dict[str, Any]: @@ -92,6 +96,7 @@ async def get(self, *, template_id: int) -> dict[str, Any]: OperationInfo(service="templates", operation="get", is_mutation=False, resource_id=template_id), "GET", f"/templates/{template_id}", + operation="GetTemplate", ) async def update( @@ -129,4 +134,5 @@ async def get_construction(self, *, template_id: int, construction_id: int) -> d ), "GET", f"/templates/{template_id}/project_constructions/{construction_id}", + operation="GetProjectConstruction", ) diff --git a/python/src/basecamp/generated/services/timeline.py b/python/src/basecamp/generated/services/timeline.py index d76618529..f9879fd70 100644 --- a/python/src/basecamp/generated/services/timeline.py +++ b/python/src/basecamp/generated/services/timeline.py @@ -17,6 +17,7 @@ def get_project_timeline(self, *, project_id: int) -> ListResult: service="timeline", operation="get_project_timeline", is_mutation=False, project_id=project_id ), f"/projects/{project_id}/timeline.json", + operation="GetProjectTimeline", ) @@ -27,4 +28,5 @@ async def get_project_timeline(self, *, project_id: int) -> ListResult: service="timeline", operation="get_project_timeline", is_mutation=False, project_id=project_id ), f"/projects/{project_id}/timeline.json", + operation="GetProjectTimeline", ) diff --git a/python/src/basecamp/generated/services/timesheets.py b/python/src/basecamp/generated/services/timesheets.py index 2700dd28b..3fabbd131 100644 --- a/python/src/basecamp/generated/services/timesheets.py +++ b/python/src/basecamp/generated/services/timesheets.py @@ -18,6 +18,7 @@ def for_project( OperationInfo(service="timesheets", operation="for_project", is_mutation=False, project_id=project_id), f"/projects/{project_id}/timesheet.json", params={k: v for k, v in {"from": from_, "to": to, "person_id": person_id}.items() if v is not None}, + operation="GetProjectTimesheet", ) def for_recording( @@ -27,6 +28,7 @@ def for_recording( OperationInfo(service="timesheets", operation="for_recording", is_mutation=False, resource_id=recording_id), f"/recordings/{recording_id}/timesheet.json", params={k: v for k, v in {"from": from_, "to": to, "person_id": person_id}.items() if v is not None}, + operation="GetRecordingTimesheet", ) def create( @@ -45,6 +47,7 @@ def report(self, *, from_: str | None = None, to: str | None = None, person_id: OperationInfo(service="timesheets", operation="report", is_mutation=False), "/reports/timesheet.json", params={k: v for k, v in {"from": from_, "to": to, "person_id": person_id}.items() if v is not None}, + operation="GetTimesheetReport", ) def get(self, *, entry_id: int) -> dict[str, Any]: @@ -52,6 +55,7 @@ def get(self, *, entry_id: int) -> dict[str, Any]: OperationInfo(service="timesheets", operation="get", is_mutation=False, resource_id=entry_id), "GET", f"/timesheet_entries/{entry_id}", + operation="GetTimesheetEntry", ) def update( @@ -80,6 +84,7 @@ async def for_project( OperationInfo(service="timesheets", operation="for_project", is_mutation=False, project_id=project_id), f"/projects/{project_id}/timesheet.json", params={k: v for k, v in {"from": from_, "to": to, "person_id": person_id}.items() if v is not None}, + operation="GetProjectTimesheet", ) async def for_recording( @@ -89,6 +94,7 @@ async def for_recording( OperationInfo(service="timesheets", operation="for_recording", is_mutation=False, resource_id=recording_id), f"/recordings/{recording_id}/timesheet.json", params={k: v for k, v in {"from": from_, "to": to, "person_id": person_id}.items() if v is not None}, + operation="GetRecordingTimesheet", ) async def create( @@ -109,6 +115,7 @@ async def report( OperationInfo(service="timesheets", operation="report", is_mutation=False), "/reports/timesheet.json", params={k: v for k, v in {"from": from_, "to": to, "person_id": person_id}.items() if v is not None}, + operation="GetTimesheetReport", ) async def get(self, *, entry_id: int) -> dict[str, Any]: @@ -116,6 +123,7 @@ async def get(self, *, entry_id: int) -> dict[str, Any]: OperationInfo(service="timesheets", operation="get", is_mutation=False, resource_id=entry_id), "GET", f"/timesheet_entries/{entry_id}", + operation="GetTimesheetEntry", ) async def update( diff --git a/python/src/basecamp/generated/services/todolist_groups.py b/python/src/basecamp/generated/services/todolist_groups.py index 4a7b7f3d9..edf9c6a91 100644 --- a/python/src/basecamp/generated/services/todolist_groups.py +++ b/python/src/basecamp/generated/services/todolist_groups.py @@ -24,6 +24,7 @@ def list(self, *, todolist_id: int) -> ListResult: return self._request_paginated( OperationInfo(service="todolistgroups", operation="list", is_mutation=False, resource_id=todolist_id), f"/todolists/{todolist_id}/groups.json", + operation="ListTodolistGroups", ) def create(self, *, todolist_id: int, name: str) -> dict[str, Any]: @@ -50,6 +51,7 @@ async def list(self, *, todolist_id: int) -> ListResult: return await self._request_paginated( OperationInfo(service="todolistgroups", operation="list", is_mutation=False, resource_id=todolist_id), f"/todolists/{todolist_id}/groups.json", + operation="ListTodolistGroups", ) async def create(self, *, todolist_id: int, name: str) -> dict[str, Any]: diff --git a/python/src/basecamp/generated/services/todolists.py b/python/src/basecamp/generated/services/todolists.py index dec410152..a45decf63 100644 --- a/python/src/basecamp/generated/services/todolists.py +++ b/python/src/basecamp/generated/services/todolists.py @@ -16,6 +16,7 @@ def get(self, *, id: int) -> dict[str, Any]: OperationInfo(service="todolists", operation="get", is_mutation=False, resource_id=id), "GET", f"/todolists/{id}", + operation="GetTodolistOrGroup", ) def update(self, *, id: int, name: str | None = None, description: str | None = None) -> dict[str, Any]: @@ -41,6 +42,7 @@ def list(self, *, todoset_id: int, status: str | None = None) -> ListResult: OperationInfo(service="todolists", operation="list", is_mutation=False, resource_id=todoset_id), f"/todosets/{todoset_id}/todolists.json", params=self._compact(status=status), + operation="ListTodolists", ) def create( @@ -61,6 +63,7 @@ async def get(self, *, id: int) -> dict[str, Any]: OperationInfo(service="todolists", operation="get", is_mutation=False, resource_id=id), "GET", f"/todolists/{id}", + operation="GetTodolistOrGroup", ) async def update(self, *, id: int, name: str | None = None, description: str | None = None) -> dict[str, Any]: @@ -86,6 +89,7 @@ async def list(self, *, todoset_id: int, status: str | None = None) -> ListResul OperationInfo(service="todolists", operation="list", is_mutation=False, resource_id=todoset_id), f"/todosets/{todoset_id}/todolists.json", params=self._compact(status=status), + operation="ListTodolists", ) async def create( diff --git a/python/src/basecamp/generated/services/todos.py b/python/src/basecamp/generated/services/todos.py index 371feae39..5e55b73b4 100644 --- a/python/src/basecamp/generated/services/todos.py +++ b/python/src/basecamp/generated/services/todos.py @@ -16,6 +16,7 @@ def list(self, *, todolist_id: int, status: str | None = None, completed: bool | OperationInfo(service="todos", operation="list", is_mutation=False, resource_id=todolist_id), f"/todolists/{todolist_id}/todos.json", params=self._compact(status=status, completed=completed), + operation="ListTodos", ) def create( @@ -51,6 +52,7 @@ def get(self, *, todo_id: int) -> dict[str, Any]: OperationInfo(service="todos", operation="get", is_mutation=False, resource_id=todo_id), "GET", f"/todos/{todo_id}", + operation="GetTodo", ) def replace( @@ -121,6 +123,7 @@ async def list(self, *, todolist_id: int, status: str | None = None, completed: OperationInfo(service="todos", operation="list", is_mutation=False, resource_id=todolist_id), f"/todolists/{todolist_id}/todos.json", params=self._compact(status=status, completed=completed), + operation="ListTodos", ) async def create( @@ -156,6 +159,7 @@ async def get(self, *, todo_id: int) -> dict[str, Any]: OperationInfo(service="todos", operation="get", is_mutation=False, resource_id=todo_id), "GET", f"/todos/{todo_id}", + operation="GetTodo", ) async def replace( diff --git a/python/src/basecamp/generated/services/todosets.py b/python/src/basecamp/generated/services/todosets.py index 506607ef1..b6b0812ad 100644 --- a/python/src/basecamp/generated/services/todosets.py +++ b/python/src/basecamp/generated/services/todosets.py @@ -16,6 +16,7 @@ def get(self, *, todoset_id: int) -> dict[str, Any]: OperationInfo(service="todosets", operation="get", is_mutation=False, resource_id=todoset_id), "GET", f"/todosets/{todoset_id}", + operation="GetTodoset", ) @@ -25,4 +26,5 @@ async def get(self, *, todoset_id: int) -> dict[str, Any]: OperationInfo(service="todosets", operation="get", is_mutation=False, resource_id=todoset_id), "GET", f"/todosets/{todoset_id}", + operation="GetTodoset", ) diff --git a/python/src/basecamp/generated/services/tools.py b/python/src/basecamp/generated/services/tools.py index 7206351ce..5a1c2dd0e 100644 --- a/python/src/basecamp/generated/services/tools.py +++ b/python/src/basecamp/generated/services/tools.py @@ -27,6 +27,7 @@ def get(self, *, tool_id: int) -> dict[str, Any]: OperationInfo(service="tools", operation="get", is_mutation=False, resource_id=tool_id), "GET", f"/dock/tools/{tool_id}", + operation="GetTool", ) def update(self, *, tool_id: int, title: str) -> dict[str, Any]: @@ -89,6 +90,7 @@ async def get(self, *, tool_id: int) -> dict[str, Any]: OperationInfo(service="tools", operation="get", is_mutation=False, resource_id=tool_id), "GET", f"/dock/tools/{tool_id}", + operation="GetTool", ) async def update(self, *, tool_id: int, title: str) -> dict[str, Any]: diff --git a/python/src/basecamp/generated/services/uploads.py b/python/src/basecamp/generated/services/uploads.py index cb74d1e5e..98b959c55 100644 --- a/python/src/basecamp/generated/services/uploads.py +++ b/python/src/basecamp/generated/services/uploads.py @@ -16,6 +16,7 @@ def get(self, *, upload_id: int) -> dict[str, Any]: OperationInfo(service="uploads", operation="get", is_mutation=False, resource_id=upload_id), "GET", f"/uploads/{upload_id}", + operation="GetUpload", ) def update(self, *, upload_id: int, description: str | None = None, base_name: str | None = None) -> dict[str, Any]: @@ -31,12 +32,14 @@ def list_versions(self, *, upload_id: int) -> ListResult: return self._request_paginated( OperationInfo(service="uploads", operation="list_versions", is_mutation=False, resource_id=upload_id), f"/uploads/{upload_id}/versions.json", + operation="ListUploadVersions", ) def list(self, *, vault_id: int) -> ListResult: return self._request_paginated( OperationInfo(service="uploads", operation="list", is_mutation=False, resource_id=vault_id), f"/vaults/{vault_id}/uploads.json", + operation="ListUploads", ) def create( @@ -70,6 +73,7 @@ async def get(self, *, upload_id: int) -> dict[str, Any]: OperationInfo(service="uploads", operation="get", is_mutation=False, resource_id=upload_id), "GET", f"/uploads/{upload_id}", + operation="GetUpload", ) async def update( @@ -87,12 +91,14 @@ async def list_versions(self, *, upload_id: int) -> ListResult: return await self._request_paginated( OperationInfo(service="uploads", operation="list_versions", is_mutation=False, resource_id=upload_id), f"/uploads/{upload_id}/versions.json", + operation="ListUploadVersions", ) async def list(self, *, vault_id: int) -> ListResult: return await self._request_paginated( OperationInfo(service="uploads", operation="list", is_mutation=False, resource_id=vault_id), f"/vaults/{vault_id}/uploads.json", + operation="ListUploads", ) async def create( diff --git a/python/src/basecamp/generated/services/vaults.py b/python/src/basecamp/generated/services/vaults.py index 1678b252d..172908d60 100644 --- a/python/src/basecamp/generated/services/vaults.py +++ b/python/src/basecamp/generated/services/vaults.py @@ -16,6 +16,7 @@ def get(self, *, vault_id: int) -> dict[str, Any]: OperationInfo(service="vaults", operation="get", is_mutation=False, resource_id=vault_id), "GET", f"/vaults/{vault_id}", + operation="GetVault", ) def update(self, *, vault_id: int, title: str | None = None) -> dict[str, Any]: @@ -31,6 +32,7 @@ def list(self, *, vault_id: int) -> ListResult: return self._request_paginated( OperationInfo(service="vaults", operation="list", is_mutation=False, resource_id=vault_id), f"/vaults/{vault_id}/vaults.json", + operation="ListVaults", ) def create(self, *, vault_id: int, title: str) -> dict[str, Any]: @@ -49,6 +51,7 @@ async def get(self, *, vault_id: int) -> dict[str, Any]: OperationInfo(service="vaults", operation="get", is_mutation=False, resource_id=vault_id), "GET", f"/vaults/{vault_id}", + operation="GetVault", ) async def update(self, *, vault_id: int, title: str | None = None) -> dict[str, Any]: @@ -64,6 +67,7 @@ async def list(self, *, vault_id: int) -> ListResult: return await self._request_paginated( OperationInfo(service="vaults", operation="list", is_mutation=False, resource_id=vault_id), f"/vaults/{vault_id}/vaults.json", + operation="ListVaults", ) async def create(self, *, vault_id: int, title: str) -> dict[str, Any]: diff --git a/python/src/basecamp/generated/services/webhooks_service.py b/python/src/basecamp/generated/services/webhooks_service.py index 7f846ea8b..6874835cb 100644 --- a/python/src/basecamp/generated/services/webhooks_service.py +++ b/python/src/basecamp/generated/services/webhooks_service.py @@ -15,6 +15,7 @@ def list(self, *, bucket_id: int) -> ListResult: return self._request_paginated( OperationInfo(service="webhooks", operation="list", is_mutation=False, project_id=bucket_id), f"/buckets/{bucket_id}/webhooks.json", + operation="ListWebhooks", ) def create( @@ -33,6 +34,7 @@ def get(self, *, webhook_id: int) -> dict[str, Any]: OperationInfo(service="webhooks", operation="get", is_mutation=False, resource_id=webhook_id), "GET", f"/webhooks/{webhook_id}", + operation="GetWebhook", ) def update( @@ -65,6 +67,7 @@ async def list(self, *, bucket_id: int) -> ListResult: return await self._request_paginated( OperationInfo(service="webhooks", operation="list", is_mutation=False, project_id=bucket_id), f"/buckets/{bucket_id}/webhooks.json", + operation="ListWebhooks", ) async def create( @@ -83,6 +86,7 @@ async def get(self, *, webhook_id: int) -> dict[str, Any]: OperationInfo(service="webhooks", operation="get", is_mutation=False, resource_id=webhook_id), "GET", f"/webhooks/{webhook_id}", + operation="GetWebhook", ) async def update( diff --git a/python/tests/test_http.py b/python/tests/test_http.py index 8bdac765c..0d88822b0 100644 --- a/python/tests/test_http.py +++ b/python/tests/test_http.py @@ -389,3 +389,138 @@ def test_adversarial_urls_never_egress_token_to_foreign_host(self): host = call.request.url.host.lower() auth = call.request.headers.get("authorization") assert self._token_may_reach(host) or auth is None, f"Bearer token egressed to foreign host {host!r}" + + +# Per-operation retry ceiling: metadata carries a retry.max per operation, and +# the effective attempts are min(client cap, op max). The ceiling can only +# reduce attempts below the client cap, never raise them, matching how TS/Swift/ +# Kotlin drive their loops from the per-op max while still honoring a Go/Python +# client that lowered its cap. Sync and async transports must agree. +_PEROP_META = { + "CapTwoOp": {"idempotent": True, "retry": {"max": 2}}, + "CapThreeOp": {"idempotent": True, "retry": {"max": 3}}, +} + + +def make_client_meta(metadata, max_retries=3): + config = Config( + base_url="https://3.basecampapi.com", + max_retries=max_retries, + base_delay=0.001, + max_jitter=0.0, + timeout=30.0, + ) + auth = BearerAuth(StaticTokenProvider("test-token")) + return HttpClient(config, auth, BasecampHooks(), metadata=metadata) + + +def make_async_client_meta(metadata, max_retries=3): + config = Config( + base_url="https://3.basecampapi.com", + max_retries=max_retries, + base_delay=0.001, + max_jitter=0.0, + timeout=30.0, + ) + auth = AsyncBearerAuth(AsyncStaticTokenProvider("test-token")) + return AsyncHttpClient(config, auth, BasecampHooks(), metadata=metadata) + + +class TestPerOperationRetryMax: + # (operation, client_cap, want_attempts) + CASES = [ + ("CapTwoOp", 3, 2), # op ceiling binds below default cap: min(3, 2) + ("CapTwoOp", 5, 2), # op ceiling binds below a raised cap: min(5, 2) + ("CapTwoOp", 1, 1), # client cap below op ceiling honored: min(1, 2) + ("CapThreeOp", 3, 3), # op ceiling equals cap (control): min(3, 3) + ] + + @pytest.mark.parametrize("operation,cap,want", CASES) + @respx.mock + def test_sync_ceiling(self, operation, cap, want): + route = respx.get("https://3.basecampapi.com/test").mock(return_value=httpx.Response(503)) + client = make_client_meta(_PEROP_META, max_retries=cap) + with pytest.raises(ApiError): + client.get("/test", operation=operation) + assert route.call_count == want + + @pytest.mark.parametrize("operation,cap,want", CASES) + @pytest.mark.asyncio + @respx.mock + async def test_async_ceiling(self, operation, cap, want): + route = respx.get("https://3.basecampapi.com/test").mock(return_value=httpx.Response(503)) + client = make_async_client_meta(_PEROP_META, max_retries=cap) + with pytest.raises(ApiError): + await client.get("/test", operation=operation) + assert route.call_count == want + + @respx.mock + def test_no_operation_uses_client_cap(self): + # A GET with no operation name (or an unknown one) keeps the client cap. + route = respx.get("https://3.basecampapi.com/test").mock(return_value=httpx.Response(503)) + client = make_client_meta(_PEROP_META, max_retries=3) + with pytest.raises(ApiError): + client.get("/test") + assert route.call_count == 3 + + +# Integration: exercise the per-op ceiling through REAL generated services with +# the REAL bundled metadata.json, so the canonical (PascalCase) operationId +# plumbing is covered end-to-end. OperationInfo.operation is snake_case and is +# NOT a metadata key; the transport must receive the canonical operationId +# (threaded via the generated `operation=` kwarg) for GET/list/pagination too — +# otherwise a client cap above an op's max would over-retry those reads. +from basecamp.async_client import AsyncClient # noqa: E402 +from basecamp.client import Client # noqa: E402 + + +def _integration_config(max_retries): + return Config( + base_url="https://3.basecampapi.com", + max_retries=max_retries, + base_delay=0.001, + max_jitter=0.0, + timeout=30.0, + ) + + +class TestPerOperationRetryMaxIntegration: + @respx.mock + def test_sync_get_list_respects_op_ceiling(self): + # ListProjects has max:3; a client cap of 5 must still stop at 3. + route = respx.route(method="GET").mock(return_value=httpx.Response(503)) + client = Client(config=_integration_config(5), access_token="tok") + with pytest.raises(ApiError): + client.for_account("999").projects.list() + assert route.call_count == 3 + client.close() + + @respx.mock + def test_sync_mutation_respects_op_ceiling(self): + # UpdateAccountName has max:2; a client cap of 5 must still stop at 2. + route = respx.route(method="PUT").mock(return_value=httpx.Response(503)) + client = Client(config=_integration_config(5), access_token="tok") + with pytest.raises(ApiError): + client.for_account("999").account.update_account_name(name="x") + assert route.call_count == 2 + client.close() + + @pytest.mark.asyncio + @respx.mock + async def test_async_get_list_respects_op_ceiling(self): + route = respx.route(method="GET").mock(return_value=httpx.Response(503)) + client = AsyncClient(config=_integration_config(5), access_token="tok") + with pytest.raises(ApiError): + await client.for_account("999").projects.list() + assert route.call_count == 3 + await client.close() + + @pytest.mark.asyncio + @respx.mock + async def test_async_mutation_respects_op_ceiling(self): + route = respx.route(method="PUT").mock(return_value=httpx.Response(503)) + client = AsyncClient(config=_integration_config(5), access_token="tok") + with pytest.raises(ApiError): + await client.for_account("999").account.update_account_name(name="x") + assert route.call_count == 2 + await client.close() diff --git a/scripts/check-retry-metadata-parity.py b/scripts/check-retry-metadata-parity.py new file mode 100755 index 000000000..11f7bb7c7 --- /dev/null +++ b/scripts/check-retry-metadata-parity.py @@ -0,0 +1,280 @@ +#!/usr/bin/env python3 +"""check-retry-metadata-parity — assert per-operation retry metadata matches the +source of truth across every SDK that emits it, and record which fields each SDK +actually consumes at runtime. + +Source of truth: behavior-model.json `operations[opId].retry`: + { "max": int, "base_delay_ms": int, "backoff": str, "retry_on": [int, ...] } + +This guard is DISTINCT from the regenerate-and-diff freshness gates. Those prove +each emitter reproduces its committed bytes (no stale/hand-edit). This proves the +emitted VALUES equal behavior-model.json — catching a generator that reproducibly +emits the wrong retry semantics. + +Two acceptance criteria: + + (1) Metadata parity (static). Every file that emits the per-op retry block must + carry, for all 226 operations, values equal to behavior-model.json: + * Full tuple (max, base_delay_ms, backoff, retry_on): + - python/src/basecamp/generated/metadata.json (snake_case) + - ruby/lib/basecamp/generated/metadata.json (camelCase) + - typescript/src/generated/metadata.ts (camelCase) + - kotlin/.../generated/Metadata.kt (positional) + - swift/Sources/Basecamp/Generated/Metadata.swift (labelled) + * max only: + - go/pkg/generated/client.gen.go operationRetryMax map + + (2) Runtime consumption (TOKEN-SMOKE classification, NOT behavioral proof). + Which emitted fields are read at runtime, per SDK, checked by the presence + of the actual usage-form token in each consuming source (and the absence of + a token for inert fields). This catches gross removal of a consumption site + but does NOT prove the field governs a retry decision — a token surviving + in a comment would pass. Behavioral proof lives in each SDK's retry tests. + Fields emitted but never read are guarded for PARITY only (criterion 1) and + classified emitted-but-runtime-inert — NOT claimed as runtime parity: + * TypeScript / Swift / Kotlin: consume the full tuple. + * Go / Python: consume only `max` (per-op ceiling); the + other fields are emitted-but-inert. + * Ruby: consumes NONE (GET-only retry); every + emitted retry field is inert. + +Exit non-zero on any parity mismatch. +""" + +from __future__ import annotations + +import json +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent + + +def fail(msg: str) -> None: + print(f" ✗ {msg}") + + +# --- source of truth --------------------------------------------------------- + + +def load_model() -> dict[str, tuple]: + d = json.loads((ROOT / "behavior-model.json").read_text()) + out: dict[str, tuple] = {} + for op, meta in d["operations"].items(): + r = meta.get("retry") + if not r: + continue + out[op] = (r["max"], r["base_delay_ms"], r["backoff"], tuple(r["retry_on"])) + return out + + +# --- emitter extractors: each returns {opId: (max, base_delay_ms, backoff, retry_on)} --- + + +def _from_json_ops(path: Path, ops_key: str | None, camel: bool) -> dict[str, tuple]: + d = json.loads(path.read_text()) + ops = d[ops_key] if ops_key else d + mx, bd, ro = ("maxAttempts", "baseDelayMs", "retryOn") if camel else ("max", "base_delay_ms", "retry_on") + out: dict[str, tuple] = {} + for op, meta in ops.items(): + if not isinstance(meta, dict) or "retry" not in meta: + continue + r = meta["retry"] + out[op] = (r[mx], r[bd], r["backoff"], tuple(r[ro])) + return out + + +def from_python() -> dict[str, tuple]: + return _from_json_ops(ROOT / "python/src/basecamp/generated/metadata.json", None, camel=False) + + +def from_ruby() -> dict[str, tuple]: + return _from_json_ops(ROOT / "ruby/lib/basecamp/generated/metadata.json", "operations", camel=True) + + +def from_typescript() -> dict[str, tuple]: + text = (ROOT / "typescript/src/generated/metadata.ts").read_text() + # The metadata value is a JSON object literal preceded by TS interface + # declarations. Anchor on the `metadata` const, then extract the balanced + # braces of its object and json.loads it. + decl = re.search(r"const metadata\b[^=]*=\s*", text) + if not decl: + raise ValueError("metadata const not found in metadata.ts") + start = text.index("{", decl.end()) + depth = 0 + for i in range(start, len(text)): + if text[i] == "{": + depth += 1 + elif text[i] == "}": + depth -= 1 + if depth == 0: + end = i + 1 + break + else: + raise ValueError("unbalanced braces in metadata.ts") + d = json.loads(text[start:end]) + ops = d["operations"] + out: dict[str, tuple] = {} + for op, meta in ops.items(): + if "retry" not in meta: + continue + r = meta["retry"] + out[op] = (r["maxAttempts"], r["baseDelayMs"], r["backoff"], tuple(r["retryOn"])) + return out + + +def from_kotlin() -> dict[str, tuple]: + text = (ROOT / "kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/Metadata.kt").read_text() + # "OpId" to OperationConfig(, RetryConfig(, L, "", setOf())), + pat = re.compile( + r'"(?P\w+)"\s+to\s+OperationConfig\([^,]+,\s*RetryConfig\(' + r"(?P\d+),\s*(?P\d+)L,\s*\"(?P\w+)\",\s*setOf\((?P[^)]*)\)\)" + ) + out: dict[str, tuple] = {} + for m in pat.finditer(text): + ro = tuple(int(x) for x in re.findall(r"\d+", m.group("ro"))) + out[m.group("op")] = (int(m.group("max")), int(m.group("delay")), m.group("backoff"), ro) + return out + + +def from_swift() -> dict[str, tuple]: + text = (ROOT / "swift/Sources/Basecamp/Generated/Metadata.swift").read_text() + # "OpId": RetryConfig(maxAttempts: N, baseDelayMs: N, backoff: .exponential, retryOn: [429, 503]), + pat = re.compile( + r'"(?P\w+)"\s*:\s*RetryConfig\(maxAttempts:\s*(?P\d+),\s*' + r"baseDelayMs:\s*(?P\d+),\s*backoff:\s*\.(?P\w+),\s*retryOn:\s*\[(?P[^\]]*)\]\)" + ) + out: dict[str, tuple] = {} + for m in pat.finditer(text): + ro = tuple(int(x) for x in re.findall(r"\d+", m.group("ro"))) + out[m.group("op")] = (int(m.group("max")), int(m.group("delay")), m.group("backoff"), ro) + return out + + +def from_go_max() -> dict[str, int]: + # Go emits the per-op retry ceiling as a separate `operationRetryMax` map + # (kept off the exported OperationMetadata struct to avoid a source break). + text = (ROOT / "go/pkg/generated/client.gen.go").read_text() + block = re.search(r"var operationRetryMax = map\[string\]int\{(.*?)\n\}", text, re.DOTALL) + if not block: + raise ValueError("operationRetryMax map not found in client.gen.go") + pat = re.compile(r'"(?P\w+)":\s*(?P\d+),') + return {m.group("op"): int(m.group("max")) for m in pat.finditer(block.group(1))} + + +# --- checks ------------------------------------------------------------------ + + +def check_full_tuple(name: str, emitted: dict[str, tuple], model: dict[str, tuple]) -> int: + errors = 0 + missing = sorted(set(model) - set(emitted)) + extra = sorted(set(emitted) - set(model)) + if missing: + fail(f"{name}: {len(missing)} operation(s) missing a retry block, e.g. {missing[:3]}") + errors += len(missing) + if extra: + fail(f"{name}: {len(extra)} operation(s) not in behavior-model.json, e.g. {extra[:3]}") + errors += len(extra) + for op in sorted(set(model) & set(emitted)): + if emitted[op] != model[op]: + fail(f"{name}: {op} retry {emitted[op]} != model {model[op]}") + errors += 1 + if errors == 0: + print(f" ✓ {name}: {len(emitted)} operations match behavior-model.json (full tuple)") + return errors + + +def check_max_only(name: str, emitted: dict[str, int], model: dict[str, tuple]) -> int: + errors = 0 + missing = sorted(set(model) - set(emitted)) + extra = sorted(set(emitted) - set(model)) + if missing: + fail(f"{name}: {len(missing)} operation(s) missing RetryMax, e.g. {missing[:3]}") + errors += len(missing) + if extra: + # A stale RetryMax entry for an op no longer in behavior-model.json must + # fail, so the emitter cannot retain obsolete retry metadata unnoticed. + fail(f"{name}: {len(extra)} operation(s) not in behavior-model.json, e.g. {extra[:3]}") + errors += len(extra) + for op in sorted(set(model) & set(emitted)): + if emitted[op] != model[op][0]: + fail(f"{name}: {op} RetryMax {emitted[op]} != model max {model[op][0]}") + errors += 1 + if errors == 0: + print(f" ✓ {name}: {len(emitted)} operations match behavior-model.json (max only)") + return errors + + +# (sdk, level, consuming file, required tokens, forbidden tokens, note). Tokens +# are the ACTUAL usage forms (e.g. `retryOn.includes`, not bare `retryOn`) so +# deleting the behavior — not just renaming a field — trips the check. This is a +# TOKEN-SMOKE classification, NOT a behavioral proof: it catches gross removal of +# a consumption site but cannot prove the field governs a retry decision (a token +# surviving in a comment would pass). Behavioral proof lives in each SDK's own +# retry test suite (go generated_*retry_test.go, python tests/test_http.py, +# swift RetryTests.swift, the conformance runners). +RUNTIME_CONSUMPTION = [ + ("TypeScript", "full tuple", "typescript/src/services/base.ts", + ["retryOn.includes", "maxAttempts", "baseDelayMs ??"], [], + "base.ts: retryOn.includes(status), attempt vs maxAttempts, baseDelayMs backoff"), + ("Swift", "full tuple", "swift/Sources/Basecamp/HTTP/HTTPClient.swift", + ["retryOn.contains", "< maxAttempts", "baseDelayMs"], [], + "HTTPClient.swift: retryOn.contains(status), attempt < maxAttempts, baseDelayMs"), + ("Kotlin", "full tuple", "kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/http/BasecampHttpClient.kt", + ["opRetry.retryOn", "opRetry?.maxRetries", "opRetry?.baseDelayMs"], [], + "BasecampHttpClient.kt: status in opRetry.retryOn, opRetry.maxRetries/baseDelayMs"), + ("Go", "max only", "go/templates/client.tmpl", + ["opMax < maxAttempts", "operationRetryMax[operationId]"], ["RetryBaseDelayMs", "RetryOn"], + "doWithRetry applies min(client cap, operationRetryMax); base_delay/retry_on/backoff emitted-but-inert"), + ("Python", "max only", "python/src/basecamp/_http.py", + ['.get("retry", {}).get("max")'], [], + "_request_with_retry applies min(client cap, retry.max); other fields emitted-but-inert"), + ("Ruby", "none", "ruby/lib/basecamp/http.rb", + [], ["maxAttempts", "maxRetries"], + "http.rb retries GET only; every emitted per-op retry field is inert"), +] + + +def check_runtime_consumption() -> int: + errors = 0 + for sdk, level, rel, required, forbidden, note in RUNTIME_CONSUMPTION: + text = (ROOT / rel).read_text() + missing = [t for t in required if t not in text] + present_forbidden = [t for t in forbidden if t in text] + if missing: + fail(f"{sdk}: {rel} is missing expected usage token(s) {missing} — a consumption site was removed") + errors += len(missing) + if present_forbidden: + fail(f"{sdk}: consuming file {rel} references {present_forbidden}, contradicting 'consumes {level}'") + errors += len(present_forbidden) + if not missing and not present_forbidden: + print(f" ✓ {sdk:<10} consumes {level:<10} — {note}") + return errors + + +def main() -> int: + model = load_model() + print(f"Source of truth: behavior-model.json ({len(model)} operations with a retry block)\n") + + print("Criterion 1 — static metadata parity:") + errors = 0 + errors += check_full_tuple("Python metadata.json", from_python(), model) + errors += check_full_tuple("Ruby metadata.json", from_ruby(), model) + errors += check_full_tuple("TypeScript metadata.ts", from_typescript(), model) + errors += check_full_tuple("Kotlin Metadata.kt", from_kotlin(), model) + errors += check_full_tuple("Swift Metadata.swift", from_swift(), model) + errors += check_max_only("Go operationRetryMax", from_go_max(), model) + + print("\nCriterion 2 — runtime consumption (token-smoke classification, not behavioral proof):") + errors += check_runtime_consumption() + + if errors: + print(f"\nFAIL: {errors} retry-metadata parity error(s).") + return 1 + print("\nOK: retry metadata is consistent across all SDKs and matches behavior-model.json.") + return 0 + + +if __name__ == "__main__": + sys.exit(main())