Skip to content

Commit 0ddce5d

Browse files
committed
feat: type nullable request params precisely
Blueprint now reports isNullable for request parameters, so codegen can distinguish the params the Seam API documents as nullable from the rest. Type a nullable param as Union[T, Null] so it accepts the NULL sentinel, and leave every other param as it was. This makes NULL checkable. Previously NULL had to be typed as Any to be passed anywhere, which meant a type checker could not report sending null to a param that does not accept it. NULL is now typed as Null, so passing it to a non-nullable param such as devices.update(is_managed=...) is an error while access_grants.list(access_grant_key=NULL) is accepted. Reading isNullable requires blueprint 1.4.0 or later, which turns an untyped property from a warning into an error. The pinned types release leaves submit_args untyped for /seam/connect_webview/v1/submit, so generation fails against it; bump types to the next release, which defines that type and adds the between parameter to events.list. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PpqwDhZmyikGbjmCPqFW2A
1 parent ca39b44 commit 0ddce5d

52 files changed

Lines changed: 186 additions & 132 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,9 @@ Python has a single absence value, so this SDK maps the two cases as follows:
444444

445445
Sending null is rarely intended and unsetting a value cannot be undone,
446446
so ``None`` means the safe option of omitting the param
447-
and sending null is always explicit:
447+
and sending null is always explicit.
448+
Route methods accept ``NULL`` only for the params the Seam API documents as
449+
nullable, so a type checker reports passing it to any other param as an error:
448450

449451
.. code-block:: python
450452
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{{name}}(self{{#if params}}, *{{else}}{{#if (eq returnType "ActionAttempt")}}, *{{/if}}{{/if}}{{#each params}}, {{name}}: {{#if required}}{{type}}{{else}}Optional[{{type}}] = None{{/if}}{{/each}}{{#if (eq returnType "ActionAttempt")}}, wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None{{/if}}) -> {{returnType}}
1+
{{name}}(self{{#if params}}, *{{else}}{{#if (eq returnType "ActionAttempt")}}, *{{/if}}{{/if}}{{#each params}}, {{name}}: {{#if required}}{{nullableType type isNullable}}{{else}}Optional[{{nullableType type isNullable}}] = None{{/if}}{{/each}}{{#if (eq returnType "ActionAttempt")}}, wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None{{/if}}) -> {{returnType}}

codegen/layouts/route.hbs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import Optional, Any, List, Dict, Union
22
import abc
33
from ..client import SeamHttpClient
4+
from ..null import Null
45
{{#if resourceClasses}}
56
from ..resources import ({{#each resourceClasses}}{{this}}{{#unless @last}},{{/unless}}{{/each}})
67
{{/if}}

codegen/lib/class-model.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export interface ClassMethodParameter {
99
deprecationMessage: string
1010
position?: number | undefined
1111
required?: boolean | undefined
12+
isNullable?: boolean | undefined
1213
}
1314

1415
export interface ClassMethod {

codegen/lib/handlebars-helpers.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,8 @@ export const pythonIdentifier = (name: string): string =>
5656
export const isListType = (type: string): boolean => type.startsWith('List[')
5757

5858
export const listItemType = (type: string): string => type.slice(5, -1)
59+
60+
// A nullable param accepts the NULL sentinel, which is sent as null.
61+
// A param set to None is omitted from the request instead.
62+
export const nullableType = (type: string, isNullable: boolean): string =>
63+
isNullable ? `Union[${type}, Null]` : type

codegen/lib/layouts/route.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export interface MethodLayoutContext {
2222
isDeprecated: boolean
2323
deprecationMessage: string
2424
required: boolean
25+
isNullable: boolean
2526
}>
2627
returnPath: string[]
2728
returnType: string
@@ -67,6 +68,7 @@ export const getMethodLayoutContext = (
6768
isDeprecated: parameter.isDeprecated,
6869
deprecationMessage: parameter.deprecationMessage,
6970
required: parameter.required ?? false,
71+
isNullable: parameter.isNullable ?? false,
7072
})),
7173
returnPath: method.returnPath,
7274
returnType: method.returnResource,

codegen/lib/routes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ export const routes = (
101101
deprecationMessage: parameter.deprecationMessage,
102102
position: parameter.name === idParameterName ? 0 : undefined,
103103
required: parameter.isRequired,
104+
isNullable: parameter.isNullable,
104105
})),
105106
...resolveResponse(response),
106107
})

package-lock.json

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@
2828
}
2929
},
3030
"devDependencies": {
31-
"@seamapi/blueprint": "^1.1.0",
31+
"@seamapi/blueprint": "^1.4.0",
3232
"@seamapi/fake-seam-connect": "1.86.0",
3333
"@seamapi/smith": "^1.1.0",
34-
"@seamapi/types": "1.983.0",
34+
"@seamapi/types": "1.984.0",
3535
"change-case": "^5.4.4",
3636
"prettier": "^3.2.5"
3737
}

seam/null.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def __bool__(self):
3131
return False
3232

3333

34-
NULL: Any = Null()
34+
NULL = Null()
3535
"""Sentinel for a param explicitly set to null.
3636
3737
Params set to this sentinel are sent as null,
@@ -52,8 +52,8 @@ def __bool__(self):
5252
# Lists only the Access Grants which have no access_grant_key.
5353
seam.access_grants.list(access_grant_key=NULL)
5454
55-
This sentinel is typed as ``Any`` so that it may be passed
56-
to any param without a type error.
55+
Route methods accept this sentinel only for params the Seam API
56+
documents as nullable, so passing it to any other param is a type error.
5757
"""
5858

5959

0 commit comments

Comments
 (0)