@josephyan/qingflow-cli 0.2.0-beta.1000

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +31 -0
  2. package/docs/local-agent-install.md +309 -0
  3. package/entry_point.py +13 -0
  4. package/npm/bin/qingflow.mjs +5 -0
  5. package/npm/lib/runtime.mjs +346 -0
  6. package/npm/scripts/postinstall.mjs +16 -0
  7. package/package.json +34 -0
  8. package/pyproject.toml +67 -0
  9. package/qingflow +15 -0
  10. package/src/qingflow_mcp/__init__.py +37 -0
  11. package/src/qingflow_mcp/__main__.py +5 -0
  12. package/src/qingflow_mcp/backend_client.py +649 -0
  13. package/src/qingflow_mcp/builder_facade/__init__.py +3 -0
  14. package/src/qingflow_mcp/builder_facade/models.py +1846 -0
  15. package/src/qingflow_mcp/builder_facade/service.py +16502 -0
  16. package/src/qingflow_mcp/cli/__init__.py +1 -0
  17. package/src/qingflow_mcp/cli/commands/__init__.py +18 -0
  18. package/src/qingflow_mcp/cli/commands/app.py +40 -0
  19. package/src/qingflow_mcp/cli/commands/auth.py +112 -0
  20. package/src/qingflow_mcp/cli/commands/builder.py +539 -0
  21. package/src/qingflow_mcp/cli/commands/chart.py +18 -0
  22. package/src/qingflow_mcp/cli/commands/common.py +62 -0
  23. package/src/qingflow_mcp/cli/commands/imports.py +96 -0
  24. package/src/qingflow_mcp/cli/commands/portal.py +25 -0
  25. package/src/qingflow_mcp/cli/commands/record.py +331 -0
  26. package/src/qingflow_mcp/cli/commands/repo.py +80 -0
  27. package/src/qingflow_mcp/cli/commands/task.py +141 -0
  28. package/src/qingflow_mcp/cli/commands/view.py +18 -0
  29. package/src/qingflow_mcp/cli/commands/workspace.py +110 -0
  30. package/src/qingflow_mcp/cli/context.py +60 -0
  31. package/src/qingflow_mcp/cli/formatters.py +573 -0
  32. package/src/qingflow_mcp/cli/json_io.py +50 -0
  33. package/src/qingflow_mcp/cli/main.py +186 -0
  34. package/src/qingflow_mcp/cli/qingflow_login.py +116 -0
  35. package/src/qingflow_mcp/cli/terminal_ui.py +173 -0
  36. package/src/qingflow_mcp/config.py +407 -0
  37. package/src/qingflow_mcp/errors.py +66 -0
  38. package/src/qingflow_mcp/id_utils.py +49 -0
  39. package/src/qingflow_mcp/import_store.py +121 -0
  40. package/src/qingflow_mcp/json_types.py +18 -0
  41. package/src/qingflow_mcp/list_type_labels.py +76 -0
  42. package/src/qingflow_mcp/public_surface.py +243 -0
  43. package/src/qingflow_mcp/repository_store.py +71 -0
  44. package/src/qingflow_mcp/response_trim.py +841 -0
  45. package/src/qingflow_mcp/server.py +216 -0
  46. package/src/qingflow_mcp/server_app_builder.py +543 -0
  47. package/src/qingflow_mcp/server_app_user.py +386 -0
  48. package/src/qingflow_mcp/session_store.py +369 -0
  49. package/src/qingflow_mcp/solution/__init__.py +6 -0
  50. package/src/qingflow_mcp/solution/build_assembly_store.py +181 -0
  51. package/src/qingflow_mcp/solution/compiler/__init__.py +282 -0
  52. package/src/qingflow_mcp/solution/compiler/chart_compiler.py +96 -0
  53. package/src/qingflow_mcp/solution/compiler/form_compiler.py +495 -0
  54. package/src/qingflow_mcp/solution/compiler/icon_utils.py +187 -0
  55. package/src/qingflow_mcp/solution/compiler/navigation_compiler.py +57 -0
  56. package/src/qingflow_mcp/solution/compiler/package_compiler.py +19 -0
  57. package/src/qingflow_mcp/solution/compiler/portal_compiler.py +60 -0
  58. package/src/qingflow_mcp/solution/compiler/view_compiler.py +51 -0
  59. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
  60. package/src/qingflow_mcp/solution/design_session.py +222 -0
  61. package/src/qingflow_mcp/solution/design_store.py +100 -0
  62. package/src/qingflow_mcp/solution/executor.py +2398 -0
  63. package/src/qingflow_mcp/solution/normalizer.py +23 -0
  64. package/src/qingflow_mcp/solution/requirements_builder.py +536 -0
  65. package/src/qingflow_mcp/solution/run_store.py +244 -0
  66. package/src/qingflow_mcp/solution/spec_models.py +855 -0
  67. package/src/qingflow_mcp/tools/__init__.py +1 -0
  68. package/src/qingflow_mcp/tools/ai_builder_tools.py +3449 -0
  69. package/src/qingflow_mcp/tools/app_tools.py +926 -0
  70. package/src/qingflow_mcp/tools/approval_tools.py +1062 -0
  71. package/src/qingflow_mcp/tools/auth_tools.py +1133 -0
  72. package/src/qingflow_mcp/tools/base.py +281 -0
  73. package/src/qingflow_mcp/tools/code_block_tools.py +777 -0
  74. package/src/qingflow_mcp/tools/custom_button_tools.py +202 -0
  75. package/src/qingflow_mcp/tools/directory_tools.py +675 -0
  76. package/src/qingflow_mcp/tools/feedback_tools.py +238 -0
  77. package/src/qingflow_mcp/tools/file_tools.py +409 -0
  78. package/src/qingflow_mcp/tools/import_tools.py +2223 -0
  79. package/src/qingflow_mcp/tools/navigation_tools.py +210 -0
  80. package/src/qingflow_mcp/tools/package_tools.py +326 -0
  81. package/src/qingflow_mcp/tools/portal_tools.py +158 -0
  82. package/src/qingflow_mcp/tools/qingbi_report_tools.py +374 -0
  83. package/src/qingflow_mcp/tools/record_tools.py +14291 -0
  84. package/src/qingflow_mcp/tools/repository_dev_tools.py +552 -0
  85. package/src/qingflow_mcp/tools/resource_read_tools.py +503 -0
  86. package/src/qingflow_mcp/tools/role_tools.py +112 -0
  87. package/src/qingflow_mcp/tools/solution_tools.py +4054 -0
  88. package/src/qingflow_mcp/tools/task_context_tools.py +2986 -0
  89. package/src/qingflow_mcp/tools/task_tools.py +889 -0
  90. package/src/qingflow_mcp/tools/view_tools.py +335 -0
  91. package/src/qingflow_mcp/tools/workflow_tools.py +376 -0
  92. package/src/qingflow_mcp/tools/workspace_tools.py +266 -0
@@ -0,0 +1,1846 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum
4
+ from typing import Any
5
+
6
+ from pydantic import AliasChoices, Field, model_validator
7
+
8
+ from ..solution.spec_models import StrictModel
9
+
10
+
11
+ class PublicFieldType(str, Enum):
12
+ text = "text"
13
+ long_text = "long_text"
14
+ number = "number"
15
+ amount = "amount"
16
+ date = "date"
17
+ datetime = "datetime"
18
+ member = "member"
19
+ department = "department"
20
+ single_select = "single_select"
21
+ multi_select = "multi_select"
22
+ phone = "phone"
23
+ email = "email"
24
+ address = "address"
25
+ attachment = "attachment"
26
+ boolean = "boolean"
27
+ q_linker = "q_linker"
28
+ code_block = "code_block"
29
+ relation = "relation"
30
+ subtable = "subtable"
31
+
32
+
33
+ class PublicRelationMode(str, Enum):
34
+ single = "single"
35
+ multiple = "multiple"
36
+
37
+
38
+ class PublicDepartmentScopeMode(str, Enum):
39
+ all = "all"
40
+ custom = "custom"
41
+
42
+
43
+ class PublicVisibilityMode(str, Enum):
44
+ workspace = "workspace"
45
+ everyone = "everyone"
46
+ specific = "specific"
47
+
48
+
49
+ class PublicExternalVisibilityMode(str, Enum):
50
+ not_ = "not"
51
+ workspace = "workspace"
52
+ specific = "specific"
53
+
54
+
55
+ FIELD_TYPE_ALIASES: dict[str, PublicFieldType] = {
56
+ "textarea": PublicFieldType.long_text,
57
+ "amount": PublicFieldType.amount,
58
+ "currency": PublicFieldType.amount,
59
+ "mobile": PublicFieldType.phone,
60
+ "user": PublicFieldType.member,
61
+ "users": PublicFieldType.member,
62
+ "select": PublicFieldType.single_select,
63
+ "radio": PublicFieldType.single_select,
64
+ "checkbox": PublicFieldType.multi_select,
65
+ "multi_select": PublicFieldType.multi_select,
66
+ "multi-select": PublicFieldType.multi_select,
67
+ "departments": PublicFieldType.department,
68
+ "qlinker": PublicFieldType.q_linker,
69
+ "q_linker": PublicFieldType.q_linker,
70
+ "codeblock": PublicFieldType.code_block,
71
+ "code_block": PublicFieldType.code_block,
72
+ }
73
+
74
+ FIELD_TYPE_ID_ALIASES: dict[int, PublicFieldType] = {
75
+ 2: PublicFieldType.text,
76
+ 3: PublicFieldType.long_text,
77
+ 4: PublicFieldType.date,
78
+ 5: PublicFieldType.member,
79
+ 6: PublicFieldType.email,
80
+ 7: PublicFieldType.phone,
81
+ 8: PublicFieldType.number,
82
+ 10: PublicFieldType.boolean,
83
+ 11: PublicFieldType.single_select,
84
+ 12: PublicFieldType.multi_select,
85
+ 13: PublicFieldType.attachment,
86
+ 20: PublicFieldType.q_linker,
87
+ 26: PublicFieldType.code_block,
88
+ 18: PublicFieldType.subtable,
89
+ 21: PublicFieldType.address,
90
+ 22: PublicFieldType.department,
91
+ 25: PublicFieldType.relation,
92
+ }
93
+
94
+
95
+ class PublicViewType(str, Enum):
96
+ table = "table"
97
+ card = "card"
98
+ board = "board"
99
+ gantt = "gantt"
100
+
101
+
102
+ class PublicButtonTriggerAction(str, Enum):
103
+ add_data = "addData"
104
+ link = "link"
105
+ qrobot = "qRobot"
106
+ wings = "wings"
107
+
108
+
109
+ class PublicViewButtonType(str, Enum):
110
+ system = "SYSTEM"
111
+ custom = "CUSTOM"
112
+
113
+
114
+ class PublicViewButtonConfigType(str, Enum):
115
+ top = "TOP"
116
+ detail = "DETAIL"
117
+
118
+
119
+ class PublicChartType(str, Enum):
120
+ target = "target"
121
+ pie = "pie"
122
+ bar = "bar"
123
+ line = "line"
124
+ table = "table"
125
+
126
+
127
+ class LayoutApplyMode(str, Enum):
128
+ merge = "merge"
129
+ replace = "replace"
130
+
131
+
132
+ class LayoutPreset(str, Enum):
133
+ balanced = "balanced"
134
+ compact = "compact"
135
+ single_section = "single_section"
136
+
137
+
138
+ class FlowPreset(str, Enum):
139
+ basic_approval = "basic_approval"
140
+ basic_fill_then_approve = "basic_fill_then_approve"
141
+
142
+
143
+ class ViewsPreset(str, Enum):
144
+ default_table = "default_table"
145
+ status_board = "status_board"
146
+ default_gantt = "default_gantt"
147
+
148
+
149
+ class PublicFlowNodeType(str, Enum):
150
+ start = "start"
151
+ approve = "approve"
152
+ fill = "fill"
153
+ copy = "copy"
154
+ branch = "branch"
155
+ condition = "condition"
156
+ webhook = "webhook"
157
+ end = "end"
158
+
159
+
160
+ class FlowConditionOperator(str, Enum):
161
+ eq = "eq"
162
+ neq = "neq"
163
+ in_ = "in"
164
+ contains = "contains"
165
+ gte = "gte"
166
+ lte = "lte"
167
+ is_empty = "is_empty"
168
+ not_empty = "not_empty"
169
+
170
+
171
+ class ViewFilterOperator(str, Enum):
172
+ eq = "eq"
173
+ neq = "neq"
174
+ in_ = "in"
175
+ contains = "contains"
176
+ gte = "gte"
177
+ lte = "lte"
178
+ is_empty = "is_empty"
179
+ not_empty = "not_empty"
180
+
181
+
182
+ class FlowAssigneePatch(StrictModel):
183
+ role_ids: list[int] = Field(default_factory=list)
184
+ role_names: list[str] = Field(default_factory=list)
185
+ member_uids: list[int] = Field(default_factory=list)
186
+ member_emails: list[str] = Field(default_factory=list)
187
+ member_names: list[str] = Field(default_factory=list)
188
+ include_sub_departs: bool | None = None
189
+
190
+
191
+ class FlowNodePermissionsPatch(StrictModel):
192
+ editable_fields: list[str] = Field(default_factory=list)
193
+
194
+
195
+ class FlowConditionRulePatch(StrictModel):
196
+ field_name: str = Field(validation_alias=AliasChoices("field_name", "fieldName", "field", "name"))
197
+ operator: FlowConditionOperator = Field(validation_alias=AliasChoices("operator", "op"))
198
+ values: list[Any] = Field(default_factory=list)
199
+
200
+ @model_validator(mode="before")
201
+ @classmethod
202
+ def normalize_aliases(cls, value: Any) -> Any:
203
+ if not isinstance(value, dict):
204
+ return value
205
+ payload = dict(value)
206
+ if "value" in payload and "values" not in payload:
207
+ raw_value = payload.pop("value")
208
+ payload["values"] = list(raw_value) if isinstance(raw_value, list) else [raw_value]
209
+ raw_operator = payload.get("operator", payload.get("op"))
210
+ if isinstance(raw_operator, str):
211
+ normalized = raw_operator.strip().lower()
212
+ operator_aliases = {
213
+ "equals": FlowConditionOperator.eq.value,
214
+ "equal": FlowConditionOperator.eq.value,
215
+ "=": FlowConditionOperator.eq.value,
216
+ "not_equals": FlowConditionOperator.neq.value,
217
+ "not_equal": FlowConditionOperator.neq.value,
218
+ "!=": FlowConditionOperator.neq.value,
219
+ ">=": FlowConditionOperator.gte.value,
220
+ "<=": FlowConditionOperator.lte.value,
221
+ "any_of": FlowConditionOperator.in_.value,
222
+ "one_of": FlowConditionOperator.in_.value,
223
+ "between_any": FlowConditionOperator.in_.value,
224
+ "empty": FlowConditionOperator.is_empty.value,
225
+ "is blank": FlowConditionOperator.is_empty.value,
226
+ "blank": FlowConditionOperator.is_empty.value,
227
+ "not_empty": FlowConditionOperator.not_empty.value,
228
+ "not blank": FlowConditionOperator.not_empty.value,
229
+ }
230
+ if normalized in operator_aliases:
231
+ payload["operator"] = operator_aliases[normalized]
232
+ elif "operator" not in payload:
233
+ payload["operator"] = normalized
234
+ payload.pop("op", None)
235
+ return payload
236
+
237
+ @model_validator(mode="after")
238
+ def validate_shape(self) -> "FlowConditionRulePatch":
239
+ if self.operator in {FlowConditionOperator.is_empty, FlowConditionOperator.not_empty}:
240
+ self.values = []
241
+ return self
242
+ if not self.values:
243
+ raise ValueError("condition rule requires values")
244
+ return self
245
+
246
+
247
+ class ViewFilterRulePatch(StrictModel):
248
+ field_name: str = Field(validation_alias=AliasChoices("field_name", "fieldName", "field", "name"))
249
+ operator: ViewFilterOperator = Field(validation_alias=AliasChoices("operator", "op"))
250
+ values: list[Any] = Field(default_factory=list)
251
+
252
+ @model_validator(mode="before")
253
+ @classmethod
254
+ def normalize_aliases(cls, value: Any) -> Any:
255
+ if not isinstance(value, dict):
256
+ return value
257
+ payload = dict(value)
258
+ if "value" in payload and "values" not in payload:
259
+ raw_value = payload.pop("value")
260
+ payload["values"] = list(raw_value) if isinstance(raw_value, list) else [raw_value]
261
+ raw_operator = payload.get("operator", payload.get("op"))
262
+ if isinstance(raw_operator, str):
263
+ normalized = raw_operator.strip().lower()
264
+ operator_aliases = {
265
+ "equals": ViewFilterOperator.eq.value,
266
+ "equal": ViewFilterOperator.eq.value,
267
+ "=": ViewFilterOperator.eq.value,
268
+ "not_equals": ViewFilterOperator.neq.value,
269
+ "not_equal": ViewFilterOperator.neq.value,
270
+ "!=": ViewFilterOperator.neq.value,
271
+ ">=": ViewFilterOperator.gte.value,
272
+ "<=": ViewFilterOperator.lte.value,
273
+ "any_of": ViewFilterOperator.in_.value,
274
+ "one_of": ViewFilterOperator.in_.value,
275
+ "between_any": ViewFilterOperator.in_.value,
276
+ "empty": ViewFilterOperator.is_empty.value,
277
+ "is blank": ViewFilterOperator.is_empty.value,
278
+ "blank": ViewFilterOperator.is_empty.value,
279
+ "not_empty": ViewFilterOperator.not_empty.value,
280
+ "not blank": ViewFilterOperator.not_empty.value,
281
+ }
282
+ if normalized in operator_aliases:
283
+ payload["operator"] = operator_aliases[normalized]
284
+ elif "operator" not in payload:
285
+ payload["operator"] = normalized
286
+ payload.pop("op", None)
287
+ return payload
288
+
289
+ @model_validator(mode="after")
290
+ def validate_shape(self) -> "ViewFilterRulePatch":
291
+ if self.operator in {ViewFilterOperator.is_empty, ViewFilterOperator.not_empty}:
292
+ self.values = []
293
+ return self
294
+ if not self.values:
295
+ raise ValueError("view filter rule requires values")
296
+ return self
297
+
298
+
299
+ class FieldSelector(StrictModel):
300
+ field_id: str | None = None
301
+ que_id: int | None = None
302
+ name: str | None = Field(default=None, validation_alias=AliasChoices("name", "title", "label"))
303
+
304
+ @model_validator(mode="after")
305
+ def validate_selector(self) -> "FieldSelector":
306
+ if self.field_id is None and self.que_id is None and self.name is None:
307
+ raise ValueError("selector must include field_id, que_id, or name")
308
+ return self
309
+
310
+
311
+ class DepartmentSelectorPatch(StrictModel):
312
+ dept_id: int | None = Field(default=None, validation_alias=AliasChoices("dept_id", "deptId", "id"))
313
+ dept_name: str | None = Field(default=None, validation_alias=AliasChoices("dept_name", "deptName", "name", "value"))
314
+
315
+ @model_validator(mode="after")
316
+ def validate_selector(self) -> "DepartmentSelectorPatch":
317
+ if self.dept_id is None and not str(self.dept_name or "").strip():
318
+ raise ValueError("department selector must include dept_id or dept_name")
319
+ return self
320
+
321
+
322
+ class DepartmentScopePatch(StrictModel):
323
+ mode: PublicDepartmentScopeMode | None = None
324
+ departments: list[DepartmentSelectorPatch] = Field(
325
+ default_factory=list,
326
+ validation_alias=AliasChoices("departments", "departs", "depart"),
327
+ )
328
+ include_sub_departs: bool | None = Field(
329
+ default=None,
330
+ validation_alias=AliasChoices("include_sub_departs", "includeSubDeparts"),
331
+ )
332
+
333
+ @model_validator(mode="before")
334
+ @classmethod
335
+ def normalize_aliases(cls, value: Any) -> Any:
336
+ if not isinstance(value, dict):
337
+ return value
338
+ payload = dict(value)
339
+ if "depart" in payload and "departments" not in payload:
340
+ payload["departments"] = payload.pop("depart")
341
+ if "departs" in payload and "departments" not in payload:
342
+ payload["departments"] = payload.pop("departs")
343
+ normalized_mode = _normalize_public_department_scope_mode(payload.get("mode"))
344
+ if normalized_mode is None:
345
+ if "departments" in payload:
346
+ normalized_mode = PublicDepartmentScopeMode.custom.value
347
+ else:
348
+ normalized_mode = PublicDepartmentScopeMode.all.value
349
+ payload["mode"] = normalized_mode
350
+ return payload
351
+
352
+ @model_validator(mode="after")
353
+ def validate_shape(self) -> "DepartmentScopePatch":
354
+ if self.mode == PublicDepartmentScopeMode.custom and not self.departments:
355
+ raise ValueError("custom department scope requires departments")
356
+ if self.mode == PublicDepartmentScopeMode.custom and any(item.dept_id is None for item in self.departments):
357
+ raise ValueError("custom department scope requires departments[].dept_id")
358
+ return self
359
+
360
+
361
+ class VisibilitySelectorsPatch(StrictModel):
362
+ member_uids: list[int] = Field(default_factory=list, validation_alias=AliasChoices("member_uids", "memberUids"))
363
+ member_emails: list[str] = Field(default_factory=list, validation_alias=AliasChoices("member_emails", "memberEmails"))
364
+ member_names: list[str] = Field(default_factory=list, validation_alias=AliasChoices("member_names", "memberNames"))
365
+ dept_ids: list[int] = Field(default_factory=list, validation_alias=AliasChoices("dept_ids", "deptIds"))
366
+ dept_names: list[str] = Field(default_factory=list, validation_alias=AliasChoices("dept_names", "deptNames"))
367
+ role_ids: list[int] = Field(default_factory=list, validation_alias=AliasChoices("role_ids", "roleIds"))
368
+ role_names: list[str] = Field(default_factory=list, validation_alias=AliasChoices("role_names", "roleNames"))
369
+ include_sub_departs: bool | None = Field(
370
+ default=None,
371
+ validation_alias=AliasChoices("include_sub_departs", "includeSubDeparts"),
372
+ )
373
+
374
+ def has_any_selector(self) -> bool:
375
+ return any(
376
+ (
377
+ self.member_uids,
378
+ self.member_emails,
379
+ self.member_names,
380
+ self.dept_ids,
381
+ self.dept_names,
382
+ self.role_ids,
383
+ self.role_names,
384
+ )
385
+ )
386
+
387
+
388
+ class ExternalVisibilitySelectorsPatch(StrictModel):
389
+ member_ids: list[int] = Field(default_factory=list, validation_alias=AliasChoices("member_ids", "memberIds"))
390
+ member_emails: list[str] = Field(default_factory=list, validation_alias=AliasChoices("member_emails", "memberEmails"))
391
+ dept_ids: list[int] = Field(default_factory=list, validation_alias=AliasChoices("dept_ids", "deptIds"))
392
+
393
+ def has_any_selector(self) -> bool:
394
+ return any((self.member_ids, self.member_emails, self.dept_ids))
395
+
396
+
397
+ class VisibilityPatch(StrictModel):
398
+ mode: PublicVisibilityMode | None = None
399
+ selectors: VisibilitySelectorsPatch = Field(default_factory=VisibilitySelectorsPatch)
400
+ external_mode: PublicExternalVisibilityMode | None = Field(
401
+ default=None,
402
+ validation_alias=AliasChoices("external_mode", "externalMode"),
403
+ )
404
+ external_selectors: ExternalVisibilitySelectorsPatch = Field(
405
+ default_factory=ExternalVisibilitySelectorsPatch,
406
+ validation_alias=AliasChoices("external_selectors", "externalSelectors"),
407
+ )
408
+
409
+ @model_validator(mode="before")
410
+ @classmethod
411
+ def normalize_aliases(cls, value: Any) -> Any:
412
+ if not isinstance(value, dict):
413
+ return value
414
+ payload = dict(value)
415
+ selectors = payload.get("selectors")
416
+ external_selectors = payload.get("external_selectors", payload.get("externalSelectors"))
417
+ if payload.get("mode") is None:
418
+ if isinstance(selectors, dict) and selectors:
419
+ payload["mode"] = PublicVisibilityMode.specific.value
420
+ else:
421
+ payload["mode"] = PublicVisibilityMode.workspace.value
422
+ if payload.get("mode") == PublicVisibilityMode.everyone.value and payload.get("external_mode", payload.get("externalMode")) is None:
423
+ payload["external_mode"] = PublicExternalVisibilityMode.workspace.value
424
+ if payload.get("external_mode", payload.get("externalMode")) is None:
425
+ if isinstance(external_selectors, dict) and external_selectors:
426
+ payload["external_mode"] = PublicExternalVisibilityMode.specific.value
427
+ else:
428
+ payload["external_mode"] = PublicExternalVisibilityMode.not_.value
429
+ return payload
430
+
431
+ @model_validator(mode="after")
432
+ def validate_shape(self) -> "VisibilityPatch":
433
+ if self.mode == PublicVisibilityMode.specific and not self.selectors.has_any_selector():
434
+ raise ValueError("specific visibility requires selectors")
435
+ if self.mode != PublicVisibilityMode.specific and self.selectors.has_any_selector():
436
+ raise ValueError("selectors are only allowed when mode=specific")
437
+ if self.mode == PublicVisibilityMode.everyone and self.external_mode != PublicExternalVisibilityMode.workspace:
438
+ raise ValueError("mode=everyone requires external_mode=workspace")
439
+ if self.mode == PublicVisibilityMode.everyone and self.external_selectors.has_any_selector():
440
+ raise ValueError("external_selectors are not allowed when mode=everyone")
441
+ if self.external_mode == PublicExternalVisibilityMode.specific and not self.external_selectors.has_any_selector():
442
+ raise ValueError("external_mode=specific requires external_selectors")
443
+ if self.external_mode != PublicExternalVisibilityMode.specific and self.external_selectors.has_any_selector():
444
+ raise ValueError("external_selectors are only allowed when external_mode=specific")
445
+ return self
446
+
447
+
448
+ class CodeBlockAliasPathPatch(StrictModel):
449
+ alias_name: str = Field(validation_alias=AliasChoices("alias_name", "aliasName"))
450
+ alias_path: str = Field(validation_alias=AliasChoices("alias_path", "aliasPath"))
451
+ alias_type: int = Field(default=1, validation_alias=AliasChoices("alias_type", "aliasType"))
452
+ alias_id: int | None = Field(default=None, validation_alias=AliasChoices("alias_id", "aliasId"))
453
+ sub_alias: list["CodeBlockAliasPathPatch"] = Field(
454
+ default_factory=list,
455
+ validation_alias=AliasChoices("sub_alias", "subAlias"),
456
+ )
457
+
458
+
459
+ class CodeBlockConfigPatch(StrictModel):
460
+ config_mode: int = Field(default=1, validation_alias=AliasChoices("config_mode", "configMode"))
461
+ code_content: str = Field(default="", validation_alias=AliasChoices("code_content", "codeContent"))
462
+ being_hide_on_form: bool = Field(
463
+ default=False,
464
+ validation_alias=AliasChoices("being_hide_on_form", "beingHideOnForm"),
465
+ )
466
+ result_alias_path: list[CodeBlockAliasPathPatch] = Field(
467
+ default_factory=list,
468
+ validation_alias=AliasChoices("result_alias_path", "resultAliasPath", "alias_config", "aliasConfig"),
469
+ )
470
+
471
+
472
+ class CodeBlockInputBindingPatch(StrictModel):
473
+ field: FieldSelector
474
+ var: str | None = None
475
+
476
+ @model_validator(mode="before")
477
+ @classmethod
478
+ def normalize_aliases(cls, value: Any) -> Any:
479
+ if isinstance(value, str):
480
+ return {"field": {"name": value}}
481
+ if not isinstance(value, dict):
482
+ return value
483
+ payload = dict(value)
484
+ raw_field = payload.get("field")
485
+ if isinstance(raw_field, str):
486
+ payload["field"] = {"name": raw_field}
487
+ elif raw_field is None:
488
+ for key in ("field_name", "fieldName", "name", "title", "label"):
489
+ raw_value = payload.get(key)
490
+ if isinstance(raw_value, str) and raw_value.strip():
491
+ payload["field"] = {"name": raw_value}
492
+ break
493
+ return payload
494
+
495
+
496
+ class QLinkerInputSource(str, Enum):
497
+ query_param = "query_param"
498
+ header = "header"
499
+ url_encoded = "url_encoded"
500
+ json_path = "json_path"
501
+
502
+
503
+ class QLinkerKeyValuePatch(StrictModel):
504
+ key: str
505
+ value: str | None = None
506
+
507
+
508
+ class QLinkerAliasPathPatch(StrictModel):
509
+ alias_name: str = Field(validation_alias=AliasChoices("alias_name", "aliasName"))
510
+ alias_path: str = Field(validation_alias=AliasChoices("alias_path", "aliasPath"))
511
+ alias_id: int | None = Field(default=None, validation_alias=AliasChoices("alias_id", "aliasId"))
512
+
513
+
514
+ class RemoteLookupConfigPatch(StrictModel):
515
+ config_mode: int = Field(default=1, validation_alias=AliasChoices("config_mode", "configMode"))
516
+ url: str = ""
517
+ method: str = "GET"
518
+ headers: list[QLinkerKeyValuePatch] = Field(default_factory=list)
519
+ body_type: int = Field(default=1, validation_alias=AliasChoices("body_type", "bodyType"))
520
+ url_encoded_value: list[QLinkerKeyValuePatch] = Field(
521
+ default_factory=list,
522
+ validation_alias=AliasChoices("url_encoded_value", "urlEncodedValue"),
523
+ )
524
+ json_value: str | None = Field(default=None, validation_alias=AliasChoices("json_value", "jsonValue"))
525
+ xml_value: str | None = Field(default=None, validation_alias=AliasChoices("xml_value", "xmlValue"))
526
+ result_type: int = Field(default=1, validation_alias=AliasChoices("result_type", "resultType"))
527
+ result_format_path: list[QLinkerAliasPathPatch] = Field(
528
+ default_factory=list,
529
+ validation_alias=AliasChoices("result_format_path", "resultFormatPath"),
530
+ )
531
+ query_params: list[QLinkerKeyValuePatch] = Field(
532
+ default_factory=list,
533
+ validation_alias=AliasChoices("query_params", "queryParams"),
534
+ )
535
+ auto_trigger: bool | None = Field(default=None, validation_alias=AliasChoices("auto_trigger", "autoTrigger"))
536
+ custom_button_text_enabled: bool | None = Field(
537
+ default=None,
538
+ validation_alias=AliasChoices("custom_button_text_enabled", "customButtonTextEnabled", "custom_btn_text_status", "customBtnTextStatus"),
539
+ )
540
+ custom_button_text: str | None = Field(
541
+ default=None,
542
+ validation_alias=AliasChoices("custom_button_text", "customButtonText", "custom_btn_text", "customBtnText"),
543
+ )
544
+ being_insert_value_directly: bool | None = Field(
545
+ default=None,
546
+ validation_alias=AliasChoices("being_insert_value_directly", "beingInsertValueDirectly"),
547
+ )
548
+ being_hide_on_form: bool | None = Field(
549
+ default=None,
550
+ validation_alias=AliasChoices("being_hide_on_form", "beingHideOnForm"),
551
+ )
552
+
553
+
554
+ class QLinkerInputBindingPatch(StrictModel):
555
+ field: FieldSelector
556
+ key: str
557
+ source: QLinkerInputSource
558
+
559
+ @model_validator(mode="before")
560
+ @classmethod
561
+ def normalize_aliases(cls, value: Any) -> Any:
562
+ if isinstance(value, str):
563
+ return {"field": {"name": value}, "key": value, "source": QLinkerInputSource.query_param.value}
564
+ if not isinstance(value, dict):
565
+ return value
566
+ payload = dict(value)
567
+ raw_field = payload.get("field")
568
+ if isinstance(raw_field, str):
569
+ payload["field"] = {"name": raw_field}
570
+ elif raw_field is None:
571
+ for key in ("field_name", "fieldName", "name", "title", "label"):
572
+ raw_value = payload.get(key)
573
+ if isinstance(raw_value, str) and raw_value.strip():
574
+ payload["field"] = {"name": raw_value}
575
+ break
576
+ raw_source = payload.get("source")
577
+ if isinstance(raw_source, str):
578
+ payload["source"] = raw_source.strip().lower()
579
+ return payload
580
+
581
+
582
+ class QLinkerOutputBindingPatch(StrictModel):
583
+ alias: str
584
+ path: str
585
+ target_field: FieldSelector = Field(validation_alias=AliasChoices("target_field", "targetField"))
586
+
587
+ @model_validator(mode="before")
588
+ @classmethod
589
+ def normalize_aliases(cls, value: Any) -> Any:
590
+ if not isinstance(value, dict):
591
+ return value
592
+ payload = dict(value)
593
+ if "alias_name" in payload and "alias" not in payload:
594
+ payload["alias"] = payload.pop("alias_name")
595
+ if "aliasName" in payload and "alias" not in payload:
596
+ payload["alias"] = payload.pop("aliasName")
597
+ if "alias_path" in payload and "path" not in payload:
598
+ payload["path"] = payload.pop("alias_path")
599
+ if "aliasPath" in payload and "path" not in payload:
600
+ payload["path"] = payload.pop("aliasPath")
601
+ raw_target = payload.get("target_field", payload.get("targetField"))
602
+ if isinstance(raw_target, str):
603
+ payload["target_field"] = {"name": raw_target}
604
+ return payload
605
+
606
+
607
+ class QLinkerRequestPatch(StrictModel):
608
+ url: str = ""
609
+ method: str = "GET"
610
+ headers: list[QLinkerKeyValuePatch] = Field(default_factory=list)
611
+ query_params: list[QLinkerKeyValuePatch] = Field(
612
+ default_factory=list,
613
+ validation_alias=AliasChoices("query_params", "queryParams"),
614
+ )
615
+ body_type: int = Field(default=1, validation_alias=AliasChoices("body_type", "bodyType"))
616
+ url_encoded_value: list[QLinkerKeyValuePatch] = Field(
617
+ default_factory=list,
618
+ validation_alias=AliasChoices("url_encoded_value", "urlEncodedValue"),
619
+ )
620
+ json_value: str | None = Field(default=None, validation_alias=AliasChoices("json_value", "jsonValue"))
621
+ xml_value: str | None = Field(default=None, validation_alias=AliasChoices("xml_value", "xmlValue"))
622
+ result_type: int = Field(default=1, validation_alias=AliasChoices("result_type", "resultType"))
623
+ auto_trigger: bool | None = Field(default=None, validation_alias=AliasChoices("auto_trigger", "autoTrigger"))
624
+ custom_button_text_enabled: bool | None = Field(
625
+ default=None,
626
+ validation_alias=AliasChoices("custom_button_text_enabled", "customButtonTextEnabled", "custom_btn_text_status", "customBtnTextStatus"),
627
+ )
628
+ custom_button_text: str | None = Field(
629
+ default=None,
630
+ validation_alias=AliasChoices("custom_button_text", "customButtonText", "custom_btn_text", "customBtnText"),
631
+ )
632
+ being_insert_value_directly: bool | None = Field(
633
+ default=None,
634
+ validation_alias=AliasChoices("being_insert_value_directly", "beingInsertValueDirectly"),
635
+ )
636
+ being_hide_on_form: bool | None = Field(
637
+ default=None,
638
+ validation_alias=AliasChoices("being_hide_on_form", "beingHideOnForm"),
639
+ )
640
+
641
+
642
+ class QLinkerBindingPatch(StrictModel):
643
+ inputs: list[QLinkerInputBindingPatch] = Field(default_factory=list)
644
+ request: QLinkerRequestPatch
645
+ outputs: list[QLinkerOutputBindingPatch] = Field(default_factory=list)
646
+
647
+
648
+ class CodeBlockOutputBindingPatch(StrictModel):
649
+ alias: str
650
+ path: str
651
+ target_field: FieldSelector = Field(
652
+ validation_alias=AliasChoices("target_field", "targetField"),
653
+ )
654
+
655
+ @model_validator(mode="before")
656
+ @classmethod
657
+ def normalize_aliases(cls, value: Any) -> Any:
658
+ if not isinstance(value, dict):
659
+ return value
660
+ payload = dict(value)
661
+ if "alias_name" in payload and "alias" not in payload:
662
+ payload["alias"] = payload.pop("alias_name")
663
+ if "aliasName" in payload and "alias" not in payload:
664
+ payload["alias"] = payload.pop("aliasName")
665
+ if "alias_path" in payload and "path" not in payload:
666
+ payload["path"] = payload.pop("alias_path")
667
+ if "aliasPath" in payload and "path" not in payload:
668
+ payload["path"] = payload.pop("aliasPath")
669
+ raw_target = payload.get("target_field", payload.get("targetField"))
670
+ if isinstance(raw_target, str):
671
+ payload["target_field"] = {"name": raw_target}
672
+ return payload
673
+
674
+
675
+ class CodeBlockBindingPatch(StrictModel):
676
+ inputs: list[CodeBlockInputBindingPatch] = Field(default_factory=list)
677
+ code: str = ""
678
+ auto_trigger: bool | None = Field(default=None, validation_alias=AliasChoices("auto_trigger", "autoTrigger"))
679
+ custom_button_text_enabled: bool | None = Field(
680
+ default=None,
681
+ validation_alias=AliasChoices("custom_button_text_enabled", "customButtonTextEnabled", "custom_btn_text_status", "customBtnTextStatus"),
682
+ )
683
+ custom_button_text: str | None = Field(
684
+ default=None,
685
+ validation_alias=AliasChoices("custom_button_text", "customButtonText", "custom_btn_text", "customBtnText"),
686
+ )
687
+ outputs: list[CodeBlockOutputBindingPatch] = Field(default_factory=list)
688
+
689
+ @model_validator(mode="before")
690
+ @classmethod
691
+ def normalize_aliases(cls, value: Any) -> Any:
692
+ if not isinstance(value, dict):
693
+ return value
694
+ payload = dict(value)
695
+ if "code_content" in payload and "code" not in payload:
696
+ payload["code"] = payload.pop("code_content")
697
+ if "codeContent" in payload and "code" not in payload:
698
+ payload["code"] = payload.pop("codeContent")
699
+ return payload
700
+
701
+
702
+ class FieldPatch(StrictModel):
703
+ name: str = Field(validation_alias=AliasChoices("name", "title", "label"))
704
+ type: PublicFieldType
705
+ required: bool = False
706
+ description: str | None = None
707
+ options: list[str] = Field(default_factory=list)
708
+ target_app_key: str | None = None
709
+ display_field: FieldSelector | None = None
710
+ visible_fields: list[FieldSelector] = Field(default_factory=list)
711
+ relation_mode: PublicRelationMode | None = Field(default=None, validation_alias=AliasChoices("relation_mode", "relationMode", "selection_mode", "selectionMode"))
712
+ department_scope: DepartmentScopePatch | None = Field(
713
+ default=None,
714
+ validation_alias=AliasChoices("department_scope", "departmentScope"),
715
+ )
716
+ remote_lookup_config: RemoteLookupConfigPatch | None = Field(
717
+ default=None,
718
+ validation_alias=AliasChoices("remote_lookup_config", "remoteLookupConfig"),
719
+ )
720
+ q_linker_binding: QLinkerBindingPatch | None = Field(
721
+ default=None,
722
+ validation_alias=AliasChoices("q_linker_binding", "qLinkerBinding"),
723
+ )
724
+ code_block_config: CodeBlockConfigPatch | None = Field(
725
+ default=None,
726
+ validation_alias=AliasChoices("code_block_config", "codeBlockConfig"),
727
+ )
728
+ code_block_binding: CodeBlockBindingPatch | None = Field(
729
+ default=None,
730
+ validation_alias=AliasChoices("code_block_binding", "codeBlockBinding"),
731
+ )
732
+ auto_trigger: bool | None = Field(default=None, validation_alias=AliasChoices("auto_trigger", "autoTrigger"))
733
+ custom_button_text_enabled: bool | None = Field(
734
+ default=None,
735
+ validation_alias=AliasChoices("custom_button_text_enabled", "customButtonTextEnabled", "custom_btn_text_status", "customBtnTextStatus"),
736
+ )
737
+ custom_button_text: str | None = Field(
738
+ default=None,
739
+ validation_alias=AliasChoices("custom_button_text", "customButtonText", "custom_btn_text", "customBtnText"),
740
+ )
741
+ subfields: list["FieldPatch"] = Field(default_factory=list)
742
+
743
+ @model_validator(mode="after")
744
+ def validate_shape(self) -> "FieldPatch":
745
+ if self.type == PublicFieldType.relation and not self.target_app_key:
746
+ raise ValueError("relation field requires target_app_key")
747
+ if self.type == PublicFieldType.relation and self.display_field is None:
748
+ raise ValueError("relation field requires display_field")
749
+ if self.type == PublicFieldType.relation and not self.visible_fields:
750
+ raise ValueError("relation field requires visible_fields")
751
+ if self.type != PublicFieldType.relation and self.target_app_key:
752
+ raise ValueError("target_app_key is only allowed for relation fields")
753
+ if self.type != PublicFieldType.relation and (self.display_field is not None or self.visible_fields or self.relation_mode is not None):
754
+ raise ValueError("display_field, visible_fields, and relation_mode are only allowed for relation fields")
755
+ if self.type != PublicFieldType.department and self.department_scope is not None:
756
+ raise ValueError("department_scope is only allowed for department fields")
757
+ if self.type != PublicFieldType.q_linker and (
758
+ self.remote_lookup_config is not None
759
+ or self.q_linker_binding is not None
760
+ ):
761
+ raise ValueError("remote_lookup_config and q_linker_binding are only allowed for q_linker fields")
762
+ if self.type != PublicFieldType.code_block and (
763
+ self.code_block_config is not None
764
+ or self.code_block_binding is not None
765
+ or self.auto_trigger is not None
766
+ or self.custom_button_text_enabled is not None
767
+ or self.custom_button_text is not None
768
+ ):
769
+ raise ValueError("code_block_config, code_block_binding, auto_trigger, custom_button_text_enabled, and custom_button_text are only allowed for code_block fields")
770
+ if self.type == PublicFieldType.subtable and not self.subfields:
771
+ raise ValueError("subtable field requires subfields")
772
+ if self.type != PublicFieldType.subtable and self.subfields:
773
+ raise ValueError("subfields are only allowed for subtable fields")
774
+ return self
775
+
776
+ @model_validator(mode="before")
777
+ @classmethod
778
+ def normalize_aliases(cls, value: Any) -> Any:
779
+ return _normalize_field_payload(value)
780
+
781
+
782
+ class FieldMutation(StrictModel):
783
+ name: str | None = Field(default=None, validation_alias=AliasChoices("name", "title", "label"))
784
+ type: PublicFieldType | None = None
785
+ required: bool | None = None
786
+ description: str | None = None
787
+ options: list[str] | None = None
788
+ target_app_key: str | None = None
789
+ display_field: FieldSelector | None = None
790
+ visible_fields: list[FieldSelector] | None = None
791
+ relation_mode: PublicRelationMode | None = Field(default=None, validation_alias=AliasChoices("relation_mode", "relationMode", "selection_mode", "selectionMode"))
792
+ department_scope: DepartmentScopePatch | None = Field(
793
+ default=None,
794
+ validation_alias=AliasChoices("department_scope", "departmentScope"),
795
+ )
796
+ remote_lookup_config: RemoteLookupConfigPatch | None = Field(
797
+ default=None,
798
+ validation_alias=AliasChoices("remote_lookup_config", "remoteLookupConfig"),
799
+ )
800
+ q_linker_binding: QLinkerBindingPatch | None = Field(
801
+ default=None,
802
+ validation_alias=AliasChoices("q_linker_binding", "qLinkerBinding"),
803
+ )
804
+ code_block_config: CodeBlockConfigPatch | None = Field(
805
+ default=None,
806
+ validation_alias=AliasChoices("code_block_config", "codeBlockConfig"),
807
+ )
808
+ code_block_binding: CodeBlockBindingPatch | None = Field(
809
+ default=None,
810
+ validation_alias=AliasChoices("code_block_binding", "codeBlockBinding"),
811
+ )
812
+ auto_trigger: bool | None = Field(default=None, validation_alias=AliasChoices("auto_trigger", "autoTrigger"))
813
+ custom_button_text_enabled: bool | None = Field(
814
+ default=None,
815
+ validation_alias=AliasChoices("custom_button_text_enabled", "customButtonTextEnabled", "custom_btn_text_status", "customBtnTextStatus"),
816
+ )
817
+ custom_button_text: str | None = Field(
818
+ default=None,
819
+ validation_alias=AliasChoices("custom_button_text", "customButtonText", "custom_btn_text", "customBtnText"),
820
+ )
821
+ subfields: list[FieldPatch] | None = None
822
+ subfield_updates: list["FieldUpdatePatch"] | None = Field(
823
+ default=None,
824
+ validation_alias=AliasChoices("subfield_updates", "subfieldUpdates"),
825
+ )
826
+
827
+ @model_validator(mode="after")
828
+ def validate_shape(self) -> "FieldMutation":
829
+ if self.type == PublicFieldType.relation and not self.target_app_key:
830
+ raise ValueError("relation field requires target_app_key")
831
+ relation_patch = (
832
+ self.type == PublicFieldType.relation
833
+ or self.target_app_key is not None
834
+ or self.display_field is not None
835
+ or self.visible_fields is not None
836
+ or self.relation_mode is not None
837
+ )
838
+ if self.type is not None and self.type != PublicFieldType.relation and (self.display_field is not None or self.visible_fields or self.relation_mode is not None):
839
+ raise ValueError("display_field, visible_fields, and relation_mode are only allowed for relation fields")
840
+ if self.type is not None and self.type != PublicFieldType.department and self.department_scope is not None:
841
+ raise ValueError("department_scope is only allowed for department fields")
842
+ if self.type is not None and self.type != PublicFieldType.q_linker and (
843
+ self.remote_lookup_config is not None
844
+ or self.q_linker_binding is not None
845
+ ):
846
+ raise ValueError("remote_lookup_config and q_linker_binding are only allowed for q_linker fields")
847
+ if self.type is not None and self.type != PublicFieldType.code_block and (
848
+ self.code_block_config is not None
849
+ or self.code_block_binding is not None
850
+ or self.auto_trigger is not None
851
+ or self.custom_button_text_enabled is not None
852
+ or self.custom_button_text is not None
853
+ ):
854
+ raise ValueError("code_block_config, code_block_binding, auto_trigger, custom_button_text_enabled, and custom_button_text are only allowed for code_block fields")
855
+ if self.type == PublicFieldType.subtable and not self.subfields and not self.subfield_updates:
856
+ raise ValueError("subtable field requires subfields or subfield_updates")
857
+ if self.type is not None and self.type != PublicFieldType.subtable and self.subfield_updates:
858
+ raise ValueError("subfield_updates are only allowed for subtable fields")
859
+ if self.subfields and self.subfield_updates:
860
+ raise ValueError("subfields and subfield_updates cannot be used together")
861
+ return self
862
+
863
+ @model_validator(mode="before")
864
+ @classmethod
865
+ def normalize_aliases(cls, value: Any) -> Any:
866
+ return _normalize_field_payload(value)
867
+
868
+
869
+ class FieldUpdatePatch(StrictModel):
870
+ selector: FieldSelector
871
+ set: FieldMutation
872
+
873
+
874
+ class FieldRemovePatch(StrictModel):
875
+ field_id: str | None = None
876
+ que_id: int | None = None
877
+ name: str | None = Field(default=None, validation_alias=AliasChoices("name", "title", "label"))
878
+
879
+ @model_validator(mode="after")
880
+ def validate_shape(self) -> "FieldRemovePatch":
881
+ if not any((self.field_id, self.que_id, self.name)):
882
+ raise ValueError("remove patch must include field_id, que_id, or name")
883
+ return self
884
+
885
+
886
+ def _coerce_layout_columns(value: Any) -> int | None:
887
+ if isinstance(value, bool):
888
+ return None
889
+ if isinstance(value, int):
890
+ return value if value > 0 else None
891
+ if isinstance(value, str):
892
+ stripped = value.strip()
893
+ if stripped.isdigit():
894
+ parsed = int(stripped)
895
+ return parsed if parsed > 0 else None
896
+ return None
897
+
898
+
899
+ def _normalize_layout_rows(value: Any, *, columns: int | None = None) -> Any:
900
+ if not isinstance(value, list):
901
+ return value
902
+ if value and all(isinstance(item, list) for item in value):
903
+ return value
904
+ if not value:
905
+ return []
906
+ width = columns if columns and columns > 0 else None
907
+ if width is None:
908
+ return [list(value)]
909
+ return [list(value[index : index + width]) for index in range(0, len(value), width) if value[index : index + width]]
910
+
911
+
912
+ class LayoutSectionPatch(StrictModel):
913
+ type: str | None = Field(default=None, validation_alias=AliasChoices("type", "kind", "block_type", "blockType"))
914
+ section_id: str | None = Field(default=None, validation_alias=AliasChoices("section_id", "sectionId", "paragraph_id", "paragraphId"))
915
+ title: str = Field(validation_alias=AliasChoices("title", "name", "paragraph_title", "paragraphTitle"))
916
+ rows: list[list[Any]] = Field(default_factory=list)
917
+
918
+ @model_validator(mode="before")
919
+ @classmethod
920
+ def normalize_aliases(cls, value: Any) -> Any:
921
+ if not isinstance(value, dict):
922
+ return value
923
+ payload = dict(value)
924
+ if "name" in payload and "title" not in payload:
925
+ payload["title"] = payload.pop("name")
926
+ if "paragraph_title" in payload and "title" not in payload:
927
+ payload["title"] = payload.pop("paragraph_title")
928
+ if "paragraphTitle" in payload and "title" not in payload:
929
+ payload["title"] = payload.pop("paragraphTitle")
930
+ shorthand: Any | None = None
931
+ if "rows" not in payload:
932
+ if "fields" in payload:
933
+ shorthand = payload.pop("fields")
934
+ elif "field_ids" in payload:
935
+ shorthand = payload.pop("field_ids")
936
+ if shorthand is not None:
937
+ payload["rows"] = _normalize_layout_rows(
938
+ shorthand,
939
+ columns=_coerce_layout_columns(payload.pop("columns", None)),
940
+ )
941
+ return payload
942
+
943
+ @model_validator(mode="after")
944
+ def validate_rows(self) -> "LayoutSectionPatch":
945
+ if self.type is not None and str(self.type).strip().lower() != "paragraph":
946
+ raise ValueError("layout section type must be 'paragraph'")
947
+ if not self.rows:
948
+ raise ValueError("section rows must be a non-empty list")
949
+ for row in self.rows:
950
+ if not isinstance(row, list) or not row:
951
+ raise ValueError("section rows must be a non-empty list")
952
+ if not self.section_id:
953
+ self.section_id = _slugify_title(self.title)
954
+ return self
955
+
956
+
957
+ class FlowNodePatch(StrictModel):
958
+ id: str
959
+ type: PublicFlowNodeType
960
+ name: str
961
+ assignees: FlowAssigneePatch = Field(default_factory=FlowAssigneePatch)
962
+ permissions: FlowNodePermissionsPatch = Field(default_factory=FlowNodePermissionsPatch)
963
+ conditions: list[FlowConditionRulePatch] = Field(default_factory=list)
964
+ condition_groups: list[list[FlowConditionRulePatch]] = Field(default_factory=list)
965
+ config: dict[str, Any] = Field(default_factory=dict)
966
+
967
+ @model_validator(mode="before")
968
+ @classmethod
969
+ def normalize_aliases(cls, value: Any) -> Any:
970
+ if not isinstance(value, dict):
971
+ return value
972
+ payload = dict(value)
973
+ assignees = dict(payload.get("assignees") or {})
974
+ permissions = dict(payload.get("permissions") or {})
975
+
976
+ for key in ("role_ids", "role_names", "member_uids", "member_emails", "member_names", "include_sub_departs"):
977
+ if key in payload and key not in assignees:
978
+ assignees[key] = payload.pop(key)
979
+ for key in ("editable_fields",):
980
+ if key in payload and key not in permissions:
981
+ permissions[key] = payload.pop(key)
982
+ if "filters" in payload and "conditions" not in payload:
983
+ payload["conditions"] = payload.pop("filters")
984
+ if "rules" in payload and "conditions" not in payload:
985
+ payload["conditions"] = payload.pop("rules")
986
+ if "conditionRules" in payload and "condition_groups" not in payload:
987
+ payload["condition_groups"] = payload.pop("conditionRules")
988
+ if "conditionGroups" in payload and "condition_groups" not in payload:
989
+ payload["condition_groups"] = payload.pop("conditionGroups")
990
+ if "owners" in payload and "member_names" not in assignees:
991
+ assignees["member_names"] = payload.pop("owners")
992
+ if "approvers" in payload and "role_names" not in assignees:
993
+ assignees["role_names"] = payload.pop("approvers")
994
+ if assignees:
995
+ payload["assignees"] = assignees
996
+ if permissions:
997
+ payload["permissions"] = permissions
998
+ return payload
999
+
1000
+ @model_validator(mode="after")
1001
+ def validate_branch_conditions(self) -> "FlowNodePatch":
1002
+ if self.conditions:
1003
+ self.condition_groups = [list(self.conditions), *self.condition_groups]
1004
+ self.conditions = []
1005
+ if self.type != PublicFlowNodeType.condition and self.condition_groups:
1006
+ raise ValueError("condition_groups are only allowed on condition nodes")
1007
+ return self
1008
+
1009
+
1010
+ class FlowTransitionPatch(StrictModel):
1011
+ source: str = Field(alias="from")
1012
+ target: str = Field(alias="to")
1013
+
1014
+
1015
+ class ViewUpsertPatch(StrictModel):
1016
+ name: str
1017
+ view_key: str | None = Field(default=None, validation_alias=AliasChoices("view_key", "viewKey"))
1018
+ type: PublicViewType
1019
+ columns: list[str] = Field(default_factory=list)
1020
+ group_by: str | None = None
1021
+ filters: list[ViewFilterRulePatch] = Field(default_factory=list)
1022
+ start_field: str | None = Field(default=None, validation_alias=AliasChoices("start_field", "startField"))
1023
+ end_field: str | None = Field(default=None, validation_alias=AliasChoices("end_field", "endField"))
1024
+ title_field: str | None = Field(default=None, validation_alias=AliasChoices("title_field", "titleField"))
1025
+ buttons: list["ViewButtonBindingPatch"] | None = None
1026
+ visibility: VisibilityPatch | None = None
1027
+
1028
+ @model_validator(mode="before")
1029
+ @classmethod
1030
+ def normalize_aliases(cls, value: Any) -> Any:
1031
+ if not isinstance(value, dict):
1032
+ return value
1033
+ payload = dict(value)
1034
+ if "fields" in payload and "columns" not in payload:
1035
+ payload["columns"] = payload.pop("fields")
1036
+ if "column_names" in payload and "columns" not in payload:
1037
+ payload["columns"] = payload.pop("column_names")
1038
+ if "columnNames" in payload and "columns" not in payload:
1039
+ payload["columns"] = payload.pop("columnNames")
1040
+ if "filter_rules" in payload and "filters" not in payload:
1041
+ payload["filters"] = payload.pop("filter_rules")
1042
+ if "filterRules" in payload and "filters" not in payload:
1043
+ payload["filters"] = payload.pop("filterRules")
1044
+ raw_type = payload.get("type")
1045
+ if isinstance(raw_type, str):
1046
+ normalized = raw_type.strip().lower()
1047
+ if normalized == "tableview":
1048
+ payload["type"] = "table"
1049
+ elif normalized == "cardview":
1050
+ payload["type"] = "card"
1051
+ elif normalized == "kanban":
1052
+ payload["type"] = "board"
1053
+ elif normalized == "ganttview":
1054
+ payload["type"] = "gantt"
1055
+ return payload
1056
+
1057
+ @model_validator(mode="after")
1058
+ def validate_shape(self) -> "ViewUpsertPatch":
1059
+ if self.type in {PublicViewType.table, PublicViewType.card} and not self.columns:
1060
+ raise ValueError("table/card views require columns")
1061
+ if self.type == PublicViewType.board and not self.group_by:
1062
+ raise ValueError("board view requires group_by")
1063
+ if self.type == PublicViewType.gantt and not (self.start_field and self.end_field):
1064
+ raise ValueError("gantt view requires start_field and end_field")
1065
+ return self
1066
+
1067
+
1068
+ class CustomButtonJudgeValuePatch(StrictModel):
1069
+ id: int | str | None = None
1070
+ value: Any | None = None
1071
+
1072
+ @model_validator(mode="before")
1073
+ @classmethod
1074
+ def normalize_aliases(cls, value: Any) -> Any:
1075
+ if not isinstance(value, dict):
1076
+ return value
1077
+ payload = dict(value)
1078
+ if "opt_id" in payload and "id" not in payload:
1079
+ payload["id"] = payload.pop("opt_id")
1080
+ if "member_id" in payload and "id" not in payload:
1081
+ payload["id"] = payload.pop("member_id")
1082
+ return payload
1083
+
1084
+
1085
+ class CustomButtonQuestionRefPatch(StrictModel):
1086
+ que_id: int = Field(validation_alias=AliasChoices("que_id", "queId"))
1087
+ que_title: str | None = Field(default=None, validation_alias=AliasChoices("que_title", "queTitle"))
1088
+ que_type: int | None = Field(default=None, validation_alias=AliasChoices("que_type", "queType"))
1089
+
1090
+
1091
+ class CustomButtonMatchRulePatch(StrictModel):
1092
+ que_id: int = Field(validation_alias=AliasChoices("que_id", "queId"))
1093
+ que_title: str | None = Field(default=None, validation_alias=AliasChoices("que_title", "queTitle"))
1094
+ que_type: int | None = Field(default=None, validation_alias=AliasChoices("que_type", "queType"))
1095
+ date_type: int | None = Field(default=None, validation_alias=AliasChoices("date_type", "dateType"))
1096
+ judge_type: int | None = Field(default=None, validation_alias=AliasChoices("judge_type", "judgeType"))
1097
+ match_type: int | None = Field(default=None, validation_alias=AliasChoices("match_type", "matchType"))
1098
+ judge_values: list[str] = Field(default_factory=list, validation_alias=AliasChoices("judge_values", "judgeValues"))
1099
+ judge_que_type: int | None = Field(default=None, validation_alias=AliasChoices("judge_que_type", "judgeQueType"))
1100
+ judge_que_id: int | None = Field(default=None, validation_alias=AliasChoices("judge_que_id", "judgeQueId"))
1101
+ judge_que_detail: CustomButtonQuestionRefPatch | None = Field(
1102
+ default=None,
1103
+ validation_alias=AliasChoices("judge_que_detail", "judgeQueDetail"),
1104
+ )
1105
+ judge_value_details: list[CustomButtonJudgeValuePatch] = Field(
1106
+ default_factory=list,
1107
+ validation_alias=AliasChoices("judge_value_details", "judgeValueDetails"),
1108
+ )
1109
+ path_value: str | None = Field(default=None, validation_alias=AliasChoices("path_value", "pathValue"))
1110
+ table_update_type: int | None = Field(default=None, validation_alias=AliasChoices("table_update_type", "tableUpdateType"))
1111
+ multi_value: bool | None = Field(default=None, validation_alias=AliasChoices("multi_value", "multiValue"))
1112
+ add_rule: str | None = Field(default=None, validation_alias=AliasChoices("add_rule", "addRule"))
1113
+ filter_condition: list[list["CustomButtonMatchRulePatch"]] = Field(
1114
+ default_factory=list,
1115
+ validation_alias=AliasChoices("filter_condition", "filterCondition"),
1116
+ )
1117
+ field_id_prefix: str | None = Field(default=None, validation_alias=AliasChoices("field_id_prefix", "fieldIdPrefix"))
1118
+
1119
+ @model_validator(mode="before")
1120
+ @classmethod
1121
+ def normalize_aliases(cls, value: Any) -> Any:
1122
+ if not isinstance(value, dict):
1123
+ return value
1124
+ payload = dict(value)
1125
+ if "judgeValue" in payload and "judge_values" not in payload and "judgeValues" not in payload:
1126
+ payload["judge_values"] = [payload.pop("judgeValue")]
1127
+ return payload
1128
+
1129
+
1130
+ class CustomButtonAddDataConfigPatch(StrictModel):
1131
+ related_app_key: str | None = Field(default=None, validation_alias=AliasChoices("related_app_key", "relatedAppKey"))
1132
+ related_app_name: str | None = Field(default=None, validation_alias=AliasChoices("related_app_name", "relatedAppName"))
1133
+ que_relation: list[CustomButtonMatchRulePatch] = Field(
1134
+ default_factory=list,
1135
+ validation_alias=AliasChoices("que_relation", "queRelation"),
1136
+ )
1137
+
1138
+
1139
+ class CustomButtonExternalQRobotConfigPatch(StrictModel):
1140
+ external_qrobot_config_id: int | None = Field(
1141
+ default=None,
1142
+ validation_alias=AliasChoices("external_qrobot_config_id", "externalQRobotConfigId"),
1143
+ )
1144
+ triggered_text: str | None = Field(default=None, validation_alias=AliasChoices("triggered_text", "triggeredText"))
1145
+
1146
+
1147
+ class CustomButtonWingsConfigPatch(StrictModel):
1148
+ wings_agent_id: int | None = Field(default=None, validation_alias=AliasChoices("wings_agent_id", "wingsAgentId"))
1149
+ wings_agent_name: str | None = Field(default=None, validation_alias=AliasChoices("wings_agent_name", "wingsAgentName"))
1150
+ bind_que_id_list: list[int] = Field(default_factory=list, validation_alias=AliasChoices("bind_que_id_list", "bindQueIdList"))
1151
+ bind_file_que_id_list: list[int] = Field(
1152
+ default_factory=list,
1153
+ validation_alias=AliasChoices("bind_file_que_id_list", "bindFileQueIdList"),
1154
+ )
1155
+ default_prompt: str | None = Field(default=None, validation_alias=AliasChoices("default_prompt", "defaultPrompt"))
1156
+ being_auto_send: bool | None = Field(default=None, validation_alias=AliasChoices("being_auto_send", "beingAutoSend"))
1157
+
1158
+ @model_validator(mode="before")
1159
+ @classmethod
1160
+ def normalize_aliases(cls, value: Any) -> Any:
1161
+ if not isinstance(value, dict):
1162
+ return value
1163
+ payload = dict(value)
1164
+ raw_agent_id = payload.get("wings_agent_id", payload.get("wingsAgentId"))
1165
+ if isinstance(raw_agent_id, str) and raw_agent_id.strip().isdigit():
1166
+ payload["wings_agent_id"] = int(raw_agent_id.strip())
1167
+ return payload
1168
+
1169
+
1170
+ def _is_white_button_color(value: str | None) -> bool:
1171
+ normalized = str(value or "").strip().lower().replace(" ", "")
1172
+ return normalized in {
1173
+ "#fff",
1174
+ "#ffffff",
1175
+ "white",
1176
+ "rgb(255,255,255)",
1177
+ "rgba(255,255,255,1)",
1178
+ }
1179
+
1180
+
1181
+ class CustomButtonPatch(StrictModel):
1182
+ button_text: str = Field(validation_alias=AliasChoices("button_text", "buttonText"))
1183
+ background_color: str = Field(validation_alias=AliasChoices("background_color", "backgroundColor"))
1184
+ text_color: str = Field(validation_alias=AliasChoices("text_color", "textColor"))
1185
+ button_icon: str = Field(validation_alias=AliasChoices("button_icon", "buttonIcon"))
1186
+ icon_color: str | None = Field(default=None, validation_alias=AliasChoices("icon_color", "iconColor"))
1187
+ trigger_action: PublicButtonTriggerAction = Field(validation_alias=AliasChoices("trigger_action", "triggerAction"))
1188
+ trigger_link_url: str | None = Field(default=None, validation_alias=AliasChoices("trigger_link_url", "triggerLinkUrl"))
1189
+ trigger_add_data_config: CustomButtonAddDataConfigPatch | None = Field(
1190
+ default=None,
1191
+ validation_alias=AliasChoices("trigger_add_data_config", "triggerAddDataConfig"),
1192
+ )
1193
+ external_qrobot_config: CustomButtonExternalQRobotConfigPatch | None = Field(
1194
+ default=None,
1195
+ validation_alias=AliasChoices(
1196
+ "external_qrobot_config",
1197
+ "externalQrobotConfig",
1198
+ "custom_button_external_qrobot_relation_vo",
1199
+ "customButtonExternalQRobotRelationVO",
1200
+ ),
1201
+ )
1202
+ trigger_wings_config: CustomButtonWingsConfigPatch | None = Field(
1203
+ default=None,
1204
+ validation_alias=AliasChoices("trigger_wings_config", "triggerWingsConfig"),
1205
+ )
1206
+
1207
+ @model_validator(mode="after")
1208
+ def validate_shape(self) -> "CustomButtonPatch":
1209
+ if self.trigger_action == PublicButtonTriggerAction.link and not str(self.trigger_link_url or "").strip():
1210
+ raise ValueError("link buttons require trigger_link_url")
1211
+ if self.trigger_action == PublicButtonTriggerAction.add_data and self.trigger_add_data_config is None:
1212
+ raise ValueError("addData buttons require trigger_add_data_config")
1213
+ if self.trigger_action == PublicButtonTriggerAction.qrobot and self.external_qrobot_config is None:
1214
+ raise ValueError("qRobot buttons require external_qrobot_config")
1215
+ if self.trigger_action == PublicButtonTriggerAction.wings and self.trigger_wings_config is None:
1216
+ raise ValueError("wings buttons require trigger_wings_config")
1217
+ if _is_white_button_color(self.background_color) and _is_white_button_color(self.text_color):
1218
+ raise ValueError("background_color and text_color cannot both be white")
1219
+ return self
1220
+
1221
+
1222
+ class ViewButtonBindingPatch(StrictModel):
1223
+ button_type: PublicViewButtonType = Field(validation_alias=AliasChoices("button_type", "buttonType"))
1224
+ config_type: PublicViewButtonConfigType = Field(validation_alias=AliasChoices("config_type", "configType"))
1225
+ button_id: int = Field(validation_alias=AliasChoices("button_id", "buttonId", "id"))
1226
+ button_text: str | None = Field(default=None, validation_alias=AliasChoices("button_text", "buttonText"))
1227
+ button_icon: str | None = Field(default=None, validation_alias=AliasChoices("button_icon", "buttonIcon"))
1228
+ icon_color: str | None = Field(default=None, validation_alias=AliasChoices("icon_color", "iconColor"))
1229
+ background_color: str | None = Field(default=None, validation_alias=AliasChoices("background_color", "backgroundColor"))
1230
+ text_color: str | None = Field(default=None, validation_alias=AliasChoices("text_color", "textColor"))
1231
+ trigger_action: str | None = Field(default=None, validation_alias=AliasChoices("trigger_action", "triggerAction"))
1232
+ print_tpls: list[Any] = Field(default_factory=list, validation_alias=AliasChoices("print_tpls", "printTpls"))
1233
+ being_main: bool = Field(default=False, validation_alias=AliasChoices("being_main", "beingMain"))
1234
+ button_limit: list[list[ViewFilterRulePatch]] = Field(
1235
+ default_factory=list,
1236
+ validation_alias=AliasChoices("button_limit", "buttonLimit"),
1237
+ )
1238
+ button_formula: str | None = Field(default=None, validation_alias=AliasChoices("button_formula", "buttonFormula"))
1239
+ button_formula_type: int = Field(default=1, validation_alias=AliasChoices("button_formula_type", "buttonFormulaType"))
1240
+
1241
+ @model_validator(mode="before")
1242
+ @classmethod
1243
+ def normalize_aliases(cls, value: Any) -> Any:
1244
+ if not isinstance(value, dict):
1245
+ return value
1246
+ payload = dict(value)
1247
+ raw_button_type = payload.get("button_type", payload.get("buttonType"))
1248
+ if isinstance(raw_button_type, str):
1249
+ normalized_type = raw_button_type.strip().lower()
1250
+ if normalized_type == "system":
1251
+ payload["button_type"] = "SYSTEM"
1252
+ elif normalized_type == "custom":
1253
+ payload["button_type"] = "CUSTOM"
1254
+ raw_config_type = payload.get("config_type", payload.get("configType"))
1255
+ if isinstance(raw_config_type, str):
1256
+ normalized_config = raw_config_type.strip().lower()
1257
+ if normalized_config == "top":
1258
+ payload["config_type"] = "TOP"
1259
+ elif normalized_config == "detail":
1260
+ payload["config_type"] = "DETAIL"
1261
+ raw_limits = payload.get("button_limit", payload.get("buttonLimit"))
1262
+ if isinstance(raw_limits, list) and raw_limits and all(isinstance(item, dict) for item in raw_limits):
1263
+ payload["button_limit"] = [raw_limits]
1264
+ return payload
1265
+
1266
+ @model_validator(mode="after")
1267
+ def validate_shape(self) -> "ViewButtonBindingPatch":
1268
+ if self.button_type == PublicViewButtonType.system:
1269
+ missing = [
1270
+ field_name
1271
+ for field_name, value in (
1272
+ ("button_icon", self.button_icon),
1273
+ ("background_color", self.background_color),
1274
+ ("text_color", self.text_color),
1275
+ ("trigger_action", self.trigger_action),
1276
+ )
1277
+ if not str(value or "").strip()
1278
+ ]
1279
+ if missing:
1280
+ raise ValueError(f"system button bindings require {', '.join(missing)}")
1281
+ if _is_white_button_color(self.background_color) and _is_white_button_color(self.text_color):
1282
+ raise ValueError("background_color and text_color cannot both be white")
1283
+ return self
1284
+
1285
+
1286
+ class ChartFilterRulePatch(StrictModel):
1287
+ field_name: str = Field(validation_alias=AliasChoices("field_name", "fieldName", "field", "name"))
1288
+ operator: ViewFilterOperator = Field(validation_alias=AliasChoices("operator", "op"))
1289
+ values: list[Any] = Field(default_factory=list)
1290
+
1291
+ @model_validator(mode="before")
1292
+ @classmethod
1293
+ def normalize_aliases(cls, value: Any) -> Any:
1294
+ if not isinstance(value, dict):
1295
+ return value
1296
+ payload = dict(value)
1297
+ if "value" in payload and "values" not in payload:
1298
+ raw_value = payload.pop("value")
1299
+ payload["values"] = list(raw_value) if isinstance(raw_value, list) else [raw_value]
1300
+ raw_operator = payload.get("operator", payload.get("op"))
1301
+ if isinstance(raw_operator, str):
1302
+ normalized = raw_operator.strip().lower()
1303
+ operator_aliases = {
1304
+ "equals": ViewFilterOperator.eq.value,
1305
+ "equal": ViewFilterOperator.eq.value,
1306
+ "=": ViewFilterOperator.eq.value,
1307
+ "not_equals": ViewFilterOperator.neq.value,
1308
+ "not_equal": ViewFilterOperator.neq.value,
1309
+ "!=": ViewFilterOperator.neq.value,
1310
+ ">=": ViewFilterOperator.gte.value,
1311
+ "<=": ViewFilterOperator.lte.value,
1312
+ "any_of": ViewFilterOperator.in_.value,
1313
+ "one_of": ViewFilterOperator.in_.value,
1314
+ "between_any": ViewFilterOperator.in_.value,
1315
+ "empty": ViewFilterOperator.is_empty.value,
1316
+ "is blank": ViewFilterOperator.is_empty.value,
1317
+ "blank": ViewFilterOperator.is_empty.value,
1318
+ "not_empty": ViewFilterOperator.not_empty.value,
1319
+ "not blank": ViewFilterOperator.not_empty.value,
1320
+ }
1321
+ if normalized in operator_aliases:
1322
+ payload["operator"] = operator_aliases[normalized]
1323
+ elif "operator" not in payload:
1324
+ payload["operator"] = normalized
1325
+ payload.pop("op", None)
1326
+ return payload
1327
+
1328
+ @model_validator(mode="after")
1329
+ def validate_shape(self) -> "ChartFilterRulePatch":
1330
+ if self.operator in {ViewFilterOperator.is_empty, ViewFilterOperator.not_empty}:
1331
+ self.values = []
1332
+ return self
1333
+ if not self.values:
1334
+ raise ValueError("chart filter rule requires values")
1335
+ return self
1336
+
1337
+
1338
+ class ChartUpsertPatch(StrictModel):
1339
+ chart_id: str | None = None
1340
+ name: str
1341
+ chart_type: PublicChartType
1342
+ dimension_field_ids: list[str] = Field(default_factory=list)
1343
+ indicator_field_ids: list[str] = Field(default_factory=list)
1344
+ filters: list[ChartFilterRulePatch] = Field(default_factory=list)
1345
+ question_config: list[dict[str, Any]] = Field(default_factory=list)
1346
+ user_config: list[dict[str, Any]] = Field(default_factory=list)
1347
+ config: dict[str, Any] = Field(default_factory=dict)
1348
+ visibility: VisibilityPatch | None = None
1349
+
1350
+ @model_validator(mode="before")
1351
+ @classmethod
1352
+ def normalize_aliases(cls, value: Any) -> Any:
1353
+ if not isinstance(value, dict):
1354
+ return value
1355
+ payload = dict(value)
1356
+ if "id" in payload and "chart_id" not in payload:
1357
+ payload["chart_id"] = payload.pop("id")
1358
+ if "type" in payload and "chart_type" not in payload:
1359
+ payload["chart_type"] = payload.pop("type")
1360
+ if "dimension_fields" in payload and "dimension_field_ids" not in payload:
1361
+ payload["dimension_field_ids"] = payload.pop("dimension_fields")
1362
+ if "indicator_fields" in payload and "indicator_field_ids" not in payload:
1363
+ payload["indicator_field_ids"] = payload.pop("indicator_fields")
1364
+ if "metric_field_ids" in payload and "indicator_field_ids" not in payload:
1365
+ payload["indicator_field_ids"] = payload.pop("metric_field_ids")
1366
+ raw_type = payload.get("chart_type")
1367
+ if isinstance(raw_type, str):
1368
+ normalized = raw_type.strip().lower()
1369
+ aliases = {
1370
+ "targetchart": PublicChartType.target.value,
1371
+ "piechart": PublicChartType.pie.value,
1372
+ "barchart": PublicChartType.bar.value,
1373
+ "linechart": PublicChartType.line.value,
1374
+ "tablechart": PublicChartType.table.value,
1375
+ }
1376
+ if normalized in aliases:
1377
+ payload["chart_type"] = aliases[normalized]
1378
+ if isinstance(payload.get("chart_id"), int):
1379
+ payload["chart_id"] = str(payload["chart_id"])
1380
+ if isinstance(payload.get("dimension_field_ids"), list):
1381
+ payload["dimension_field_ids"] = [str(item) for item in payload["dimension_field_ids"] if item is not None and str(item).strip()]
1382
+ if isinstance(payload.get("indicator_field_ids"), list):
1383
+ payload["indicator_field_ids"] = [str(item) for item in payload["indicator_field_ids"] if item is not None and str(item).strip()]
1384
+ return payload
1385
+
1386
+
1387
+ class ChartApplyRequest(StrictModel):
1388
+ app_key: str
1389
+ upsert_charts: list[ChartUpsertPatch] = Field(default_factory=list)
1390
+ remove_chart_ids: list[str] = Field(default_factory=list)
1391
+ reorder_chart_ids: list[str] = Field(default_factory=list)
1392
+
1393
+ @model_validator(mode="before")
1394
+ @classmethod
1395
+ def normalize_ids(cls, value: Any) -> Any:
1396
+ if not isinstance(value, dict):
1397
+ return value
1398
+ payload = dict(value)
1399
+ for key in ("remove_chart_ids", "reorder_chart_ids"):
1400
+ raw = payload.get(key)
1401
+ if isinstance(raw, list):
1402
+ payload[key] = [str(item) for item in raw if item is not None and str(item).strip()]
1403
+ return payload
1404
+
1405
+ @model_validator(mode="after")
1406
+ def validate_shape(self) -> "ChartApplyRequest":
1407
+ if not self.upsert_charts and not self.remove_chart_ids and not self.reorder_chart_ids:
1408
+ raise ValueError("chart apply requires at least one upsert, remove, or reorder operation")
1409
+ return self
1410
+
1411
+
1412
+ class PortalComponentPositionPatch(StrictModel):
1413
+ pc_x: int = Field(default=0, validation_alias=AliasChoices("pc_x", "pcX", "x"))
1414
+ pc_y: int = Field(default=0, validation_alias=AliasChoices("pc_y", "pcY", "y"))
1415
+ pc_w: int = Field(default=12, validation_alias=AliasChoices("pc_w", "pcW", "w"))
1416
+ pc_h: int = Field(default=8, validation_alias=AliasChoices("pc_h", "pcH", "h"))
1417
+ mobile_x: int = Field(default=0, validation_alias=AliasChoices("mobile_x", "mobileX"))
1418
+ mobile_y: int = Field(default=0, validation_alias=AliasChoices("mobile_y", "mobileY"))
1419
+ mobile_w: int = Field(default=12, validation_alias=AliasChoices("mobile_w", "mobileW"))
1420
+ mobile_h: int = Field(default=8, validation_alias=AliasChoices("mobile_h", "mobileH"))
1421
+
1422
+ @model_validator(mode="before")
1423
+ @classmethod
1424
+ def normalize_nested_layout(cls, value: Any) -> Any:
1425
+ if not isinstance(value, dict):
1426
+ return value
1427
+ payload = dict(value)
1428
+ pc = payload.pop("pc", None)
1429
+ mobile = payload.pop("mobile", None)
1430
+ if isinstance(pc, dict):
1431
+ if "pc_x" not in payload and "x" in pc:
1432
+ payload["pc_x"] = pc.get("x")
1433
+ if "pc_y" not in payload and "y" in pc:
1434
+ payload["pc_y"] = pc.get("y")
1435
+ if "pc_w" not in payload and "cols" in pc:
1436
+ payload["pc_w"] = pc.get("cols")
1437
+ if "pc_h" not in payload and "rows" in pc:
1438
+ payload["pc_h"] = pc.get("rows")
1439
+ if isinstance(mobile, dict):
1440
+ if "mobile_x" not in payload and "x" in mobile:
1441
+ payload["mobile_x"] = mobile.get("x")
1442
+ if "mobile_y" not in payload and "y" in mobile:
1443
+ payload["mobile_y"] = mobile.get("y")
1444
+ if "mobile_w" not in payload and "cols" in mobile:
1445
+ payload["mobile_w"] = mobile.get("cols")
1446
+ if "mobile_h" not in payload and "rows" in mobile:
1447
+ payload["mobile_h"] = mobile.get("rows")
1448
+ return payload
1449
+
1450
+
1451
+ class PortalChartRefPatch(StrictModel):
1452
+ app_key: str
1453
+ chart_id: str | None = None
1454
+ chart_name: str | None = None
1455
+
1456
+ @model_validator(mode="after")
1457
+ def validate_target(self) -> "PortalChartRefPatch":
1458
+ if not (self.chart_id or self.chart_name):
1459
+ raise ValueError("chart_ref requires chart_id or chart_name")
1460
+ return self
1461
+
1462
+
1463
+ class PortalViewRefPatch(StrictModel):
1464
+ app_key: str
1465
+ view_key: str | None = None
1466
+ view_name: str | None = None
1467
+
1468
+ @model_validator(mode="after")
1469
+ def validate_target(self) -> "PortalViewRefPatch":
1470
+ if not (self.view_key or self.view_name):
1471
+ raise ValueError("view_ref requires view_key or view_name")
1472
+ return self
1473
+
1474
+
1475
+ class PortalSectionPatch(StrictModel):
1476
+ title: str
1477
+ source_type: str = Field(validation_alias=AliasChoices("source_type", "sourceType"))
1478
+ position: PortalComponentPositionPatch | None = None
1479
+ dash_style_config: dict[str, Any] | None = Field(default=None, validation_alias=AliasChoices("dash_style_config", "dashStyleConfigBO"))
1480
+ config: dict[str, Any] = Field(default_factory=dict)
1481
+ chart_ref: PortalChartRefPatch | None = None
1482
+ view_ref: PortalViewRefPatch | None = None
1483
+ text: str | None = None
1484
+ url: str | None = None
1485
+
1486
+ @model_validator(mode="before")
1487
+ @classmethod
1488
+ def normalize_aliases(cls, value: Any) -> Any:
1489
+ if not isinstance(value, dict):
1490
+ return value
1491
+ payload = dict(value)
1492
+ raw_type = payload.get("source_type", payload.get("sourceType"))
1493
+ if isinstance(raw_type, str):
1494
+ payload["source_type"] = raw_type.strip().lower()
1495
+ if "chartRef" in payload and "chart_ref" not in payload:
1496
+ payload["chart_ref"] = payload.pop("chartRef")
1497
+ if "viewRef" in payload and "view_ref" not in payload:
1498
+ payload["view_ref"] = payload.pop("viewRef")
1499
+ if "dashStyleConfigBO" in payload and "dash_style_config" not in payload:
1500
+ payload["dash_style_config"] = payload.pop("dashStyleConfigBO")
1501
+ return payload
1502
+
1503
+ @model_validator(mode="after")
1504
+ def validate_shape(self) -> "PortalSectionPatch":
1505
+ supported = {"chart", "view", "grid", "filter", "text", "link"}
1506
+ if self.source_type not in supported:
1507
+ raise ValueError(f"unsupported portal source_type '{self.source_type}'")
1508
+ if self.source_type == "chart" and self.chart_ref is None:
1509
+ raise ValueError("chart section requires chart_ref")
1510
+ if self.source_type == "view" and self.view_ref is None:
1511
+ raise ValueError("view section requires view_ref")
1512
+ if self.source_type == "text" and self.text is None:
1513
+ raise ValueError("text section requires text")
1514
+ if self.source_type == "link" and self.url is None:
1515
+ raise ValueError("link section requires url")
1516
+ return self
1517
+
1518
+
1519
+ class PortalApplyRequest(StrictModel):
1520
+ dash_key: str | None = None
1521
+ dash_name: str | None = None
1522
+ package_tag_id: int | None = None
1523
+ publish: bool = True
1524
+ sections: list[PortalSectionPatch] = Field(default_factory=list)
1525
+ visibility: VisibilityPatch | None = None
1526
+ auth: dict[str, Any] | None = None
1527
+ icon: str | None = None
1528
+ color: str | None = None
1529
+ hide_copyright: bool | None = Field(default=None, validation_alias=AliasChoices("hide_copyright", "hideCopyright"))
1530
+ dash_global_config: dict[str, Any] | None = Field(default=None, validation_alias=AliasChoices("dash_global_config", "dashGlobalConfig"))
1531
+ config: dict[str, Any] = Field(default_factory=dict)
1532
+
1533
+ @model_validator(mode="after")
1534
+ def validate_shape(self) -> "PortalApplyRequest":
1535
+ if not self.dash_key and not self.package_tag_id:
1536
+ raise ValueError("package_tag_id is required when dash_key is empty")
1537
+ if not self.dash_key and not self.dash_name:
1538
+ raise ValueError("dash_name is required when creating a portal")
1539
+ if not self.dash_key and not self.sections:
1540
+ raise ValueError("portal apply requires a non-empty sections list when creating a portal")
1541
+ if self.visibility is not None and self.auth is not None:
1542
+ raise ValueError("visibility and auth cannot be provided together")
1543
+ return self
1544
+
1545
+
1546
+ FieldPatch.model_rebuild()
1547
+ FieldMutation.model_rebuild()
1548
+ FieldUpdatePatch.model_rebuild()
1549
+
1550
+
1551
+ class AppGetResponse(StrictModel):
1552
+ app_key: str
1553
+ title: str | None = None
1554
+ app_icon: str | None = None
1555
+ visibility: dict[str, Any] = Field(default_factory=dict)
1556
+ tag_ids: list[int] = Field(default_factory=list)
1557
+ publish_status: int | None = None
1558
+ field_count: int = 0
1559
+ layout_section_count: int = 0
1560
+ view_count: int = 0
1561
+ workflow_enabled: bool = False
1562
+ verification_hints: list[str] = Field(default_factory=list)
1563
+ editability: dict[str, bool | None] = Field(default_factory=dict)
1564
+
1565
+
1566
+ class AppGetFieldsResponse(StrictModel):
1567
+ app_key: str
1568
+ fields: list[dict[str, Any]] = Field(default_factory=list)
1569
+ field_count: int = 0
1570
+
1571
+
1572
+ class AppGetLayoutResponse(StrictModel):
1573
+ app_key: str
1574
+ sections: list[dict[str, Any]] = Field(default_factory=list)
1575
+ unplaced_fields: list[str] = Field(default_factory=list)
1576
+ layout_mode_detected: str = "empty"
1577
+
1578
+
1579
+ class AppGetViewsResponse(StrictModel):
1580
+ app_key: str
1581
+ views: list[dict[str, Any]] = Field(default_factory=list)
1582
+
1583
+
1584
+ class AppGetFlowResponse(StrictModel):
1585
+ app_key: str
1586
+ enabled: bool = False
1587
+ nodes: list[dict[str, Any]] = Field(default_factory=list)
1588
+ transitions: list[dict[str, Any]] = Field(default_factory=list)
1589
+
1590
+
1591
+ class AppGetChartsResponse(StrictModel):
1592
+ app_key: str
1593
+ charts: list[dict[str, Any]] = Field(default_factory=list)
1594
+ chart_count: int = 0
1595
+
1596
+
1597
+ AppReadSummaryResponse = AppGetResponse
1598
+ AppFieldsReadResponse = AppGetFieldsResponse
1599
+ AppLayoutReadResponse = AppGetLayoutResponse
1600
+ AppViewsReadResponse = AppGetViewsResponse
1601
+ AppFlowReadResponse = AppGetFlowResponse
1602
+ AppChartsReadResponse = AppGetChartsResponse
1603
+
1604
+
1605
+ class PortalListResponse(StrictModel):
1606
+ items: list[dict[str, Any]] = Field(default_factory=list)
1607
+ total: int = 0
1608
+
1609
+
1610
+ class PortalReadSummaryResponse(StrictModel):
1611
+ dash_key: str
1612
+ being_draft: bool = True
1613
+ dash_name: str | None = None
1614
+ package_tag_ids: list[int] = Field(default_factory=list)
1615
+ dash_icon: str | None = None
1616
+ hide_copyright: bool | None = None
1617
+ config_keys: list[str] = Field(default_factory=list)
1618
+ dash_global_config_keys: list[str] = Field(default_factory=list)
1619
+ section_count: int = 0
1620
+ sections: list[dict[str, Any]] = Field(default_factory=list)
1621
+
1622
+
1623
+ class PortalGetResponse(StrictModel):
1624
+ dash_key: str
1625
+ being_draft: bool = True
1626
+ dash_name: str | None = None
1627
+ package_tag_ids: list[int] = Field(default_factory=list)
1628
+ dash_icon: str | None = None
1629
+ hide_copyright: bool | None = None
1630
+ visibility: dict[str, Any] = Field(default_factory=dict)
1631
+ auth: dict[str, Any] = Field(default_factory=dict)
1632
+ config: dict[str, Any] = Field(default_factory=dict)
1633
+ dash_global_config: dict[str, Any] = Field(default_factory=dict)
1634
+ component_count: int = 0
1635
+ components: list[dict[str, Any]] = Field(default_factory=list)
1636
+
1637
+
1638
+ class ViewGetResponse(StrictModel):
1639
+ view_key: str
1640
+ base_info: dict[str, Any] = Field(default_factory=dict)
1641
+ visibility: dict[str, Any] = Field(default_factory=dict)
1642
+ config: dict[str, Any] = Field(default_factory=dict)
1643
+ questions: list[dict[str, Any]] = Field(default_factory=list)
1644
+ associations: list[dict[str, Any]] = Field(default_factory=list)
1645
+
1646
+
1647
+ class ChartGetResponse(StrictModel):
1648
+ chart_id: str
1649
+ base: dict[str, Any] = Field(default_factory=dict)
1650
+ visibility: dict[str, Any] = Field(default_factory=dict)
1651
+ config: dict[str, Any] = Field(default_factory=dict)
1652
+
1653
+
1654
+ class SchemaPlanRequest(StrictModel):
1655
+ app_key: str = ""
1656
+ package_tag_id: int | None = None
1657
+ app_name: str = Field(default="", validation_alias=AliasChoices("app_name", "app_title", "title"))
1658
+ icon: str | None = None
1659
+ color: str | None = None
1660
+ visibility: VisibilityPatch | None = None
1661
+ create_if_missing: bool = False
1662
+ add_fields: list[FieldPatch] = Field(default_factory=list)
1663
+ update_fields: list[FieldUpdatePatch] = Field(default_factory=list)
1664
+ remove_fields: list[FieldRemovePatch] = Field(default_factory=list)
1665
+
1666
+
1667
+ class LayoutPlanRequest(StrictModel):
1668
+ app_key: str
1669
+ mode: LayoutApplyMode = LayoutApplyMode.merge
1670
+ sections: list[LayoutSectionPatch] = Field(default_factory=list)
1671
+ preset: LayoutPreset | None = None
1672
+
1673
+ @model_validator(mode="before")
1674
+ @classmethod
1675
+ def normalize_mode_alias(cls, value: Any) -> Any:
1676
+ if not isinstance(value, dict):
1677
+ return value
1678
+ payload = dict(value)
1679
+ if str(payload.get("mode") or "").strip().lower() == "overwrite":
1680
+ payload["mode"] = "replace"
1681
+ return payload
1682
+
1683
+
1684
+ class FlowPlanRequest(StrictModel):
1685
+ app_key: str
1686
+ mode: str = "replace"
1687
+ nodes: list[FlowNodePatch] = Field(default_factory=list)
1688
+ transitions: list[FlowTransitionPatch] = Field(default_factory=list)
1689
+ preset: FlowPreset | None = None
1690
+
1691
+ @model_validator(mode="before")
1692
+ @classmethod
1693
+ def normalize_mode_alias(cls, value: Any) -> Any:
1694
+ if not isinstance(value, dict):
1695
+ return value
1696
+ payload = dict(value)
1697
+ if str(payload.get("mode") or "").strip().lower() == "overwrite":
1698
+ payload["mode"] = "replace"
1699
+ raw_preset = payload.get("preset")
1700
+ if raw_preset is None and isinstance(payload.get("base_preset"), str):
1701
+ raw_preset = payload["base_preset"]
1702
+ payload["preset"] = raw_preset
1703
+ if isinstance(raw_preset, str):
1704
+ normalized_preset = raw_preset.strip().lower()
1705
+ preset_aliases = {
1706
+ "default_approval": FlowPreset.basic_approval.value,
1707
+ "approval": FlowPreset.basic_approval.value,
1708
+ "basic approval": FlowPreset.basic_approval.value,
1709
+ "default_fill_then_approve": FlowPreset.basic_fill_then_approve.value,
1710
+ "default-fill-then-approve": FlowPreset.basic_fill_then_approve.value,
1711
+ "fill_then_approve": FlowPreset.basic_fill_then_approve.value,
1712
+ }
1713
+ if normalized_preset in preset_aliases:
1714
+ payload["preset"] = preset_aliases[normalized_preset]
1715
+ return payload
1716
+
1717
+
1718
+ class ViewsPlanRequest(StrictModel):
1719
+ app_key: str
1720
+ upsert_views: list[ViewUpsertPatch] = Field(default_factory=list)
1721
+ remove_views: list[str] = Field(default_factory=list)
1722
+ preset: ViewsPreset | None = None
1723
+
1724
+
1725
+ class OperationResultEnvelope(StrictModel):
1726
+ status: str
1727
+ error_code: str | None = None
1728
+ recoverable: bool = False
1729
+ message: str
1730
+ normalized_args: dict[str, Any] = Field(default_factory=dict)
1731
+ missing_fields: list[str] = Field(default_factory=list)
1732
+ allowed_values: dict[str, Any] = Field(default_factory=dict)
1733
+ details: dict[str, Any] = Field(default_factory=dict)
1734
+ request_id: str | None = None
1735
+ suggested_next_call: dict[str, Any] | None = None
1736
+ noop: bool = False
1737
+ verification: dict[str, Any] = Field(default_factory=dict)
1738
+
1739
+
1740
+ def _normalize_field_payload(value: Any) -> Any:
1741
+ if not isinstance(value, dict):
1742
+ return value
1743
+ payload = dict(value)
1744
+ if "fields" in payload and "subfields" not in payload:
1745
+ payload["subfields"] = payload.pop("fields")
1746
+ raw_type = payload.get("type")
1747
+ if isinstance(raw_type, int):
1748
+ normalized_from_id = FIELD_TYPE_ID_ALIASES.get(raw_type)
1749
+ if normalized_from_id is not None:
1750
+ payload["type"] = normalized_from_id.value
1751
+ if isinstance(raw_type, str):
1752
+ normalized = FIELD_TYPE_ALIASES.get(raw_type.strip().lower())
1753
+ if normalized is not None:
1754
+ payload["type"] = normalized.value
1755
+ normalized_relation_mode = _normalize_public_relation_mode(
1756
+ payload.get("relation_mode", payload.get("relationMode", payload.get("selection_mode", payload.get("selectionMode"))))
1757
+ )
1758
+ if normalized_relation_mode is None:
1759
+ for alias_key in ("optional_data_num", "optionalDataNum", "multiple", "allow_multiple"):
1760
+ if alias_key in payload:
1761
+ normalized_relation_mode = _normalize_public_relation_mode(payload.get(alias_key))
1762
+ break
1763
+ if normalized_relation_mode is not None:
1764
+ payload["relation_mode"] = normalized_relation_mode
1765
+ for alias_key in (
1766
+ "relationMode",
1767
+ "selection_mode",
1768
+ "selectionMode",
1769
+ "optional_data_num",
1770
+ "optionalDataNum",
1771
+ "multiple",
1772
+ "allow_multiple",
1773
+ ):
1774
+ payload.pop(alias_key, None)
1775
+ return payload
1776
+
1777
+
1778
+ def _slugify_title(title: str) -> str:
1779
+ normalized = "".join(ch.lower() if ch.isalnum() else "_" for ch in str(title or ""))
1780
+ collapsed = "_".join(part for part in normalized.split("_") if part)
1781
+ return collapsed or "section"
1782
+
1783
+
1784
+ def _normalize_public_relation_mode(value: Any) -> str | None:
1785
+ if value is None:
1786
+ return None
1787
+ if isinstance(value, bool):
1788
+ return PublicRelationMode.multiple.value if value else PublicRelationMode.single.value
1789
+ if isinstance(value, int):
1790
+ if value == 0:
1791
+ return PublicRelationMode.multiple.value
1792
+ if value == 1:
1793
+ return PublicRelationMode.single.value
1794
+ return None
1795
+ if isinstance(value, str):
1796
+ normalized = value.strip().lower()
1797
+ aliases = {
1798
+ "single": PublicRelationMode.single.value,
1799
+ "single_select": PublicRelationMode.single.value,
1800
+ "single-select": PublicRelationMode.single.value,
1801
+ "one": PublicRelationMode.single.value,
1802
+ "1": PublicRelationMode.single.value,
1803
+ "multiple": PublicRelationMode.multiple.value,
1804
+ "multi": PublicRelationMode.multiple.value,
1805
+ "multi_select": PublicRelationMode.multiple.value,
1806
+ "multi-select": PublicRelationMode.multiple.value,
1807
+ "many": PublicRelationMode.multiple.value,
1808
+ "0": PublicRelationMode.multiple.value,
1809
+ }
1810
+ return aliases.get(normalized, normalized or None)
1811
+ return None
1812
+
1813
+
1814
+ def _normalize_public_department_scope_mode(value: Any) -> str | None:
1815
+ if value is None:
1816
+ return None
1817
+ if isinstance(value, int):
1818
+ if value == 1:
1819
+ return PublicDepartmentScopeMode.all.value
1820
+ if value == 2:
1821
+ return PublicDepartmentScopeMode.custom.value
1822
+ return None
1823
+ if isinstance(value, str):
1824
+ normalized = value.strip().lower()
1825
+ aliases = {
1826
+ "all": PublicDepartmentScopeMode.all.value,
1827
+ "workspace_all": PublicDepartmentScopeMode.all.value,
1828
+ "workspace-all": PublicDepartmentScopeMode.all.value,
1829
+ "default": PublicDepartmentScopeMode.all.value,
1830
+ "default_all": PublicDepartmentScopeMode.all.value,
1831
+ "default-all": PublicDepartmentScopeMode.all.value,
1832
+ "1": PublicDepartmentScopeMode.all.value,
1833
+ "custom": PublicDepartmentScopeMode.custom.value,
1834
+ "explicit": PublicDepartmentScopeMode.custom.value,
1835
+ "selected": PublicDepartmentScopeMode.custom.value,
1836
+ "2": PublicDepartmentScopeMode.custom.value,
1837
+ }
1838
+ return aliases.get(normalized, normalized or None)
1839
+ return None
1840
+
1841
+
1842
+ CustomButtonMatchRulePatch.model_rebuild()
1843
+ CustomButtonAddDataConfigPatch.model_rebuild()
1844
+ CodeBlockAliasPathPatch.model_rebuild()
1845
+ ViewButtonBindingPatch.model_rebuild()
1846
+ ViewUpsertPatch.model_rebuild()