Skip to content

Commit 3f4e960

Browse files
jwfingclaude
andcommitted
Add Pydantic validation constraints across all modules
Enforce Literal types, Field min/max length, and model validators aligned with OpenAPI specs for auth, database, AI, email, and functions models. Adds 19 validation tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4f8ca5c commit 3f4e960

12 files changed

Lines changed: 278 additions & 68 deletions

File tree

insforge/ai/client.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
from datetime import datetime
4+
from typing import Literal
45

56
from .._base_client import BaseClient
67
from .._utils import quote_path_segment
@@ -54,8 +55,8 @@ async def list_configurations(
5455
async def create_configuration(
5556
self,
5657
*,
57-
input_modality: list[str],
58-
output_modality: list[str],
58+
input_modality: list[Literal["text", "image"]],
59+
output_modality: list[Literal["text", "image"]],
5960
provider: str,
6061
model_id: str,
6162
system_prompt: str | None = None,
@@ -240,7 +241,7 @@ async def generate_embeddings(
240241
model: str,
241242
input: str | list[str],
242243
access_token: str | None = None,
243-
encoding_format: str | None = None,
244+
encoding_format: Literal["float", "base64"] | None = None,
244245
dimensions: int | None = None,
245246
) -> AIEmbeddingsResponse:
246247
payload = AIEmbeddingsRequest(

insforge/ai/models.py

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from datetime import datetime
44
from typing import Any
5+
from typing import Literal
56

67
from pydantic import BaseModel
78
from pydantic import ConfigDict
@@ -20,13 +21,16 @@ class AIConfiguration(BaseModel):
2021
updated_at: datetime = Field(alias="updatedAt")
2122

2223

24+
Modality = Literal["text", "image"]
25+
26+
2327
class AIConfigurationCreateRequest(BaseModel):
2428
model_config = ConfigDict(extra="ignore", populate_by_name=True)
2529

26-
input_modality: list[str] = Field(alias="inputModality")
27-
output_modality: list[str] = Field(alias="outputModality")
28-
provider: str
29-
model_id: str = Field(alias="modelId")
30+
input_modality: list[Modality] = Field(min_length=1, alias="inputModality")
31+
output_modality: list[Modality] = Field(min_length=1, alias="outputModality")
32+
provider: str = Field(min_length=1)
33+
model_id: str = Field(min_length=1, alias="modelId")
3034
system_prompt: str | None = Field(default=None, alias="systemPrompt")
3135

3236

@@ -99,7 +103,7 @@ class AICreditsResponse(BaseModel):
99103
class AIChatMessage(BaseModel):
100104
model_config = ConfigDict(extra="ignore", populate_by_name=True)
101105

102-
role: str
106+
role: Literal["user", "assistant", "system", "tool"]
103107
content: str | None = None
104108
tool_calls: list[dict[str, Any]] | None = Field(default=None, alias="tool_calls")
105109
tool_call_id: str | None = Field(default=None, alias="tool_call_id")
@@ -108,8 +112,8 @@ class AIChatMessage(BaseModel):
108112
class AIChatCompletionRequest(BaseModel):
109113
model_config = ConfigDict(extra="ignore", populate_by_name=True)
110114

111-
model: str
112-
messages: list[AIChatMessage]
115+
model: str = Field(min_length=1)
116+
messages: list[AIChatMessage] = Field(min_length=1)
113117
stream: bool = False
114118

115119

@@ -140,8 +144,8 @@ class AIChatCompletionResponse(BaseModel):
140144
class AIImageGenerationRequest(BaseModel):
141145
model_config = ConfigDict(extra="ignore", populate_by_name=True)
142146

143-
model: str
144-
prompt: str
147+
model: str = Field(min_length=1)
148+
prompt: str = Field(min_length=1)
145149

146150

147151
class AIImageURL(BaseModel):
@@ -187,10 +191,10 @@ class AIImageGenerationResponse(BaseModel):
187191
class AIEmbeddingsRequest(BaseModel):
188192
model_config = ConfigDict(extra="ignore", populate_by_name=True)
189193

190-
model: str
194+
model: str = Field(min_length=1)
191195
input: str | list[str]
192-
encoding_format: str | None = Field(default=None, alias="encoding_format")
193-
dimensions: int | None = None
196+
encoding_format: Literal["float", "base64"] | None = Field(default=None, alias="encoding_format")
197+
dimensions: int | None = Field(default=None, ge=0)
194198

195199

196200
class AIEmbeddingObject(BaseModel):

insforge/auth/models.py

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
from datetime import datetime
44
from typing import Any
5+
from typing import Literal
56

6-
from pydantic import BaseModel, ConfigDict, Field
7+
from pydantic import BaseModel, ConfigDict, Field, model_validator
78

89

910
class SignInResponse(BaseModel):
@@ -23,22 +24,22 @@ class AuthEmailActionResponse(BaseModel):
2324
class AuthEmailVerificationRequest(BaseModel):
2425
model_config = ConfigDict(extra="ignore", populate_by_name=True)
2526

26-
email: str
27+
email: str = Field(min_length=1)
2728
redirect_to: str | None = Field(default=None, alias="redirectTo")
2829

2930

3031
class AuthEmailVerifyRequest(BaseModel):
3132
model_config = ConfigDict(extra="ignore", populate_by_name=True)
3233

33-
email: str
34-
otp: str
34+
email: str = Field(min_length=1)
35+
otp: str = Field(min_length=1)
3536

3637

3738
class AuthResetPasswordExchangeRequest(BaseModel):
3839
model_config = ConfigDict(extra="ignore", populate_by_name=True)
3940

40-
email: str
41-
code: str
41+
email: str = Field(min_length=1)
42+
code: str = Field(min_length=1)
4243

4344

4445
class AuthResetPasswordExchangeResponse(BaseModel):
@@ -51,8 +52,8 @@ class AuthResetPasswordExchangeResponse(BaseModel):
5152
class AuthResetPasswordRequest(BaseModel):
5253
model_config = ConfigDict(extra="ignore", populate_by_name=True)
5354

54-
new_password: str = Field(alias="newPassword")
55-
token: str = Field(alias="otp")
55+
new_password: str = Field(min_length=1, alias="newPassword")
56+
token: str = Field(min_length=1, alias="otp")
5657

5758

5859
class CurrentProfileResponse(BaseModel):
@@ -68,13 +69,13 @@ class PublicAuthConfigResponse(BaseModel):
6869
o_auth_providers: list[str] = Field(default_factory=list, alias="oAuthProviders")
6970
custom_o_auth_providers: list[str] = Field(default_factory=list, alias="customOAuthProviders")
7071
require_email_verification: bool | None = Field(default=None, alias="requireEmailVerification")
71-
password_min_length: int | None = Field(default=None, alias="passwordMinLength")
72+
password_min_length: int | None = Field(default=None, ge=4, le=128, alias="passwordMinLength")
7273
require_number: bool | None = Field(default=None, alias="requireNumber")
7374
require_lowercase: bool | None = Field(default=None, alias="requireLowercase")
7475
require_uppercase: bool | None = Field(default=None, alias="requireUppercase")
7576
require_special_char: bool | None = Field(default=None, alias="requireSpecialChar")
76-
verify_email_method: str | None = Field(default=None, alias="verifyEmailMethod")
77-
reset_password_method: str | None = Field(default=None, alias="resetPasswordMethod")
77+
verify_email_method: Literal["code", "link"] | None = Field(default=None, alias="verifyEmailMethod")
78+
reset_password_method: Literal["code", "link"] | None = Field(default=None, alias="resetPasswordMethod")
7879

7980

8081
class ProfileResponse(BaseModel):
@@ -89,13 +90,13 @@ class AuthConfigResponse(BaseModel):
8990

9091
id: str | None = None
9192
require_email_verification: bool | None = Field(default=None, alias="requireEmailVerification")
92-
password_min_length: int | None = Field(default=None, alias="passwordMinLength")
93+
password_min_length: int | None = Field(default=None, ge=4, le=128, alias="passwordMinLength")
9394
require_number: bool | None = Field(default=None, alias="requireNumber")
9495
require_lowercase: bool | None = Field(default=None, alias="requireLowercase")
9596
require_uppercase: bool | None = Field(default=None, alias="requireUppercase")
9697
require_special_char: bool | None = Field(default=None, alias="requireSpecialChar")
97-
verify_email_method: str | None = Field(default=None, alias="verifyEmailMethod")
98-
reset_password_method: str | None = Field(default=None, alias="resetPasswordMethod")
98+
verify_email_method: Literal["code", "link"] | None = Field(default=None, alias="verifyEmailMethod")
99+
reset_password_method: Literal["code", "link"] | None = Field(default=None, alias="resetPasswordMethod")
99100
allowed_redirect_urls: list[str] = Field(default_factory=list, alias="allowedRedirectUrls")
100101
created_at: datetime | None = Field(default=None, alias="createdAt")
101102
updated_at: datetime | None = Field(default=None, alias="updatedAt")
@@ -105,15 +106,32 @@ class AuthConfigUpdateRequest(BaseModel):
105106
model_config = ConfigDict(extra="ignore", populate_by_name=True)
106107

107108
require_email_verification: bool | None = Field(default=None, alias="requireEmailVerification")
108-
password_min_length: int | None = Field(default=None, alias="passwordMinLength")
109+
password_min_length: int | None = Field(default=None, ge=4, le=128, alias="passwordMinLength")
109110
require_number: bool | None = Field(default=None, alias="requireNumber")
110111
require_lowercase: bool | None = Field(default=None, alias="requireLowercase")
111112
require_uppercase: bool | None = Field(default=None, alias="requireUppercase")
112113
require_special_char: bool | None = Field(default=None, alias="requireSpecialChar")
113-
verify_email_method: str | None = Field(default=None, alias="verifyEmailMethod")
114-
reset_password_method: str | None = Field(default=None, alias="resetPasswordMethod")
114+
verify_email_method: Literal["code", "link"] | None = Field(default=None, alias="verifyEmailMethod")
115+
reset_password_method: Literal["code", "link"] | None = Field(default=None, alias="resetPasswordMethod")
115116
allowed_redirect_urls: list[str] | None = Field(default=None, alias="allowedRedirectUrls")
116117

118+
@model_validator(mode="before")
119+
def require_field(cls, values):
120+
fields = (
121+
"requireEmailVerification", "require_email_verification",
122+
"passwordMinLength", "password_min_length",
123+
"requireNumber", "require_number",
124+
"requireLowercase", "require_lowercase",
125+
"requireUppercase", "require_uppercase",
126+
"requireSpecialChar", "require_special_char",
127+
"verifyEmailMethod", "verify_email_method",
128+
"resetPasswordMethod", "reset_password_method",
129+
"allowedRedirectUrls", "allowed_redirect_urls",
130+
)
131+
if not any(values.get(f) is not None for f in fields):
132+
raise ValueError("update_config requires at least one field")
133+
return values
134+
117135

118136
class UserResponse(BaseModel):
119137
model_config = ConfigDict(extra="ignore", populate_by_name=True)
@@ -131,8 +149,8 @@ class UserResponse(BaseModel):
131149
class AuthUserCreateRequest(BaseModel):
132150
model_config = ConfigDict(extra="ignore", populate_by_name=True)
133151

134-
email: str
135-
password: str
152+
email: str = Field(min_length=1)
153+
password: str = Field(min_length=1)
136154
name: str | None = None
137155
redirect_to: str | None = Field(default=None, alias="redirectTo")
138156

@@ -155,7 +173,7 @@ class AuthUsersResponse(BaseModel):
155173
class AuthDeleteUsersRequest(BaseModel):
156174
model_config = ConfigDict(extra="ignore", populate_by_name=True)
157175

158-
user_ids: list[str] = Field(alias="userIds")
176+
user_ids: list[str] = Field(min_length=1, alias="userIds")
159177

160178

161179
class AuthDeleteUsersResponse(BaseModel):
@@ -192,13 +210,13 @@ class AuthSessionResponse(BaseModel):
192210
class RefreshRequest(BaseModel):
193211
model_config = ConfigDict(extra="ignore", populate_by_name=True)
194212

195-
refresh_token: str = Field(alias="refreshToken")
213+
refresh_token: str = Field(min_length=1, alias="refreshToken")
196214

197215

198216
class AdminSessionExchangeRequest(BaseModel):
199217
model_config = ConfigDict(extra="ignore", populate_by_name=True)
200218

201-
code: str
219+
code: str = Field(min_length=1)
202220

203221

204222
class LogoutResponse(BaseModel):

insforge/database/models.py

Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
from __future__ import annotations
22

33
from typing import Any
4+
from typing import Literal
45

5-
from pydantic import BaseModel, ConfigDict, Field
6+
from pydantic import BaseModel, ConfigDict, Field, model_validator
67

78

89
class DatabaseQueryResponse(BaseModel):
@@ -38,19 +39,23 @@ class DatabaseTableSchemaResponse(BaseModel):
3839
columns: list[DatabaseTableColumn] = Field(default_factory=list)
3940

4041

42+
ColumnType = Literal["string", "datetime", "integer", "float", "boolean", "uuid", "json", "file"]
43+
ForeignKeyAction = Literal["CASCADE", "SET NULL", "NO ACTION", "RESTRICT"]
44+
45+
4146
class DatabaseTableCreateForeignKey(BaseModel):
4247
model_config = ConfigDict(extra="ignore", populate_by_name=True)
4348

44-
table: str
45-
column: str
46-
on_delete: str = Field(default="NO ACTION", alias="onDelete")
49+
table: str = Field(min_length=1)
50+
column: str = Field(min_length=1)
51+
on_delete: ForeignKeyAction = Field(default="NO ACTION", alias="onDelete")
4752

4853

4954
class DatabaseTableCreateColumn(BaseModel):
5055
model_config = ConfigDict(extra="ignore", populate_by_name=True)
5156

52-
name: str
53-
type: str
57+
name: str = Field(min_length=1)
58+
type: ColumnType
5459
nullable: bool
5560
unique: bool | None = None
5661
default_value: str | None = Field(default=None, alias="defaultValue")
@@ -60,16 +65,16 @@ class DatabaseTableCreateColumn(BaseModel):
6065
class DatabaseCreateTableRequest(BaseModel):
6166
model_config = ConfigDict(extra="ignore", populate_by_name=True)
6267

63-
table_name: str = Field(alias="tableName")
64-
columns: list[DatabaseTableCreateColumn]
68+
table_name: str = Field(min_length=1, alias="tableName")
69+
columns: list[DatabaseTableCreateColumn] = Field(min_length=1)
6570
rls_enabled: bool | None = Field(default=None, alias="rlsEnabled")
6671

6772

6873
class DatabaseTableSchemaAddColumn(BaseModel):
6974
model_config = ConfigDict(extra="ignore", populate_by_name=True)
7075

71-
column_name: str = Field(alias="columnName")
72-
type: str
76+
column_name: str = Field(min_length=1, alias="columnName")
77+
type: ColumnType
7378
is_nullable: bool | None = Field(default=None, alias="isNullable")
7479
is_unique: bool | None = Field(default=None, alias="isUnique")
7580
default_value: str | None = Field(default=None, alias="defaultValue")
@@ -78,31 +83,31 @@ class DatabaseTableSchemaAddColumn(BaseModel):
7883
class DatabaseTableSchemaUpdateColumn(BaseModel):
7984
model_config = ConfigDict(extra="ignore", populate_by_name=True)
8085

81-
column_name: str = Field(alias="columnName")
82-
new_column_name: str | None = Field(default=None, alias="newColumnName")
86+
column_name: str = Field(min_length=1, alias="columnName")
87+
new_column_name: str | None = Field(default=None, min_length=1, max_length=64, alias="newColumnName")
8388
default_value: str | None = Field(default=None, alias="defaultValue")
8489

8590

8691
class DatabaseTableSchemaUpdateForeignKey(BaseModel):
8792
model_config = ConfigDict(extra="ignore", populate_by_name=True)
8893

89-
reference_table: str = Field(alias="referenceTable")
90-
reference_column: str = Field(alias="referenceColumn")
91-
on_delete: str | None = Field(default=None, alias="onDelete")
92-
on_update: str | None = Field(default=None, alias="onUpdate")
94+
reference_table: str = Field(min_length=1, alias="referenceTable")
95+
reference_column: str = Field(min_length=1, alias="referenceColumn")
96+
on_delete: ForeignKeyAction | None = Field(default=None, alias="onDelete")
97+
on_update: ForeignKeyAction | None = Field(default=None, alias="onUpdate")
9398

9499

95100
class DatabaseTableSchemaAddForeignKey(BaseModel):
96101
model_config = ConfigDict(extra="ignore", populate_by_name=True)
97102

98-
column_name: str = Field(alias="columnName")
103+
column_name: str = Field(min_length=1, alias="columnName")
99104
foreign_key: DatabaseTableSchemaUpdateForeignKey = Field(alias="foreignKey")
100105

101106

102107
class DatabaseTableSchemaRenameRequest(BaseModel):
103108
model_config = ConfigDict(extra="ignore", populate_by_name=True)
104109

105-
new_table_name: str = Field(alias="newTableName")
110+
new_table_name: str = Field(min_length=1, max_length=64, alias="newTableName")
106111

107112

108113
class DatabaseTableSchemaUpdateRequest(BaseModel):
@@ -115,6 +120,20 @@ class DatabaseTableSchemaUpdateRequest(BaseModel):
115120
drop_foreign_keys: list[str] | None = Field(default=None, alias="dropForeignKeys")
116121
rename_table: DatabaseTableSchemaRenameRequest | None = Field(default=None, alias="renameTable")
117122

123+
@model_validator(mode="before")
124+
def require_operation(cls, values):
125+
fields = (
126+
"addColumns", "add_columns",
127+
"dropColumns", "drop_columns",
128+
"updateColumns", "update_columns",
129+
"addForeignKeys", "add_foreign_keys",
130+
"dropForeignKeys", "drop_foreign_keys",
131+
"renameTable", "rename_table",
132+
)
133+
if not any(values.get(f) is not None for f in fields):
134+
raise ValueError("update_table_schema requires at least one schema operation")
135+
return values
136+
118137

119138
class DatabaseTableMutationResponse(BaseModel):
120139
model_config = ConfigDict(extra="ignore", populate_by_name=True)

insforge/email/models.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ class SendRawEmailRequest(BaseModel):
99
model_config = ConfigDict(extra="ignore", populate_by_name=True)
1010

1111
to: str | list[str]
12-
subject: str
13-
html: str
12+
subject: str = Field(min_length=1, max_length=500)
13+
html: str = Field(min_length=1)
1414
cc: str | list[str] | None = None
1515
bcc: str | list[str] | None = None
16-
from_: str | None = Field(default=None, alias="from")
16+
from_: str | None = Field(default=None, max_length=100, alias="from")
1717
reply_to: str | None = Field(default=None, alias="replyTo")

0 commit comments

Comments
 (0)