@qingflow-tech/qingflow-app-user-mcp 1.0.0

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 (109) hide show
  1. package/README.md +37 -0
  2. package/docs/local-agent-install.md +332 -0
  3. package/entry_point.py +13 -0
  4. package/npm/bin/qingflow-app-user-mcp.mjs +7 -0
  5. package/npm/lib/runtime.mjs +339 -0
  6. package/npm/scripts/postinstall.mjs +16 -0
  7. package/package.json +34 -0
  8. package/pyproject.toml +67 -0
  9. package/qingflow-app-user-mcp +15 -0
  10. package/skills/qingflow-app-user/SKILL.md +79 -0
  11. package/skills/qingflow-app-user/agents/openai.yaml +4 -0
  12. package/skills/qingflow-app-user/references/data-gotchas.md +29 -0
  13. package/skills/qingflow-app-user/references/environments.md +63 -0
  14. package/skills/qingflow-app-user/references/record-patterns.md +48 -0
  15. package/skills/qingflow-app-user/references/workflow-usage.md +26 -0
  16. package/skills/qingflow-record-analysis/SKILL.md +158 -0
  17. package/skills/qingflow-record-analysis/agents/openai.yaml +4 -0
  18. package/skills/qingflow-record-analysis/references/analysis-gotchas.md +145 -0
  19. package/skills/qingflow-record-analysis/references/analysis-patterns.md +125 -0
  20. package/skills/qingflow-record-analysis/references/confidence-reporting.md +92 -0
  21. package/skills/qingflow-record-analysis/references/dsl-templates.md +93 -0
  22. package/skills/qingflow-record-delete/SKILL.md +29 -0
  23. package/skills/qingflow-record-import/SKILL.md +31 -0
  24. package/skills/qingflow-record-insert/SKILL.md +58 -0
  25. package/skills/qingflow-record-update/SKILL.md +42 -0
  26. package/skills/qingflow-task-ops/SKILL.md +123 -0
  27. package/skills/qingflow-task-ops/agents/openai.yaml +4 -0
  28. package/skills/qingflow-task-ops/references/environments.md +44 -0
  29. package/skills/qingflow-task-ops/references/workflow-usage.md +27 -0
  30. package/src/qingflow_mcp/__init__.py +5 -0
  31. package/src/qingflow_mcp/__main__.py +5 -0
  32. package/src/qingflow_mcp/backend_client.py +649 -0
  33. package/src/qingflow_mcp/builder_facade/__init__.py +3 -0
  34. package/src/qingflow_mcp/builder_facade/models.py +1836 -0
  35. package/src/qingflow_mcp/builder_facade/service.py +15044 -0
  36. package/src/qingflow_mcp/cli/__init__.py +1 -0
  37. package/src/qingflow_mcp/cli/commands/__init__.py +18 -0
  38. package/src/qingflow_mcp/cli/commands/app.py +40 -0
  39. package/src/qingflow_mcp/cli/commands/auth.py +44 -0
  40. package/src/qingflow_mcp/cli/commands/builder.py +538 -0
  41. package/src/qingflow_mcp/cli/commands/chart.py +18 -0
  42. package/src/qingflow_mcp/cli/commands/common.py +62 -0
  43. package/src/qingflow_mcp/cli/commands/imports.py +96 -0
  44. package/src/qingflow_mcp/cli/commands/portal.py +25 -0
  45. package/src/qingflow_mcp/cli/commands/record.py +331 -0
  46. package/src/qingflow_mcp/cli/commands/repo.py +80 -0
  47. package/src/qingflow_mcp/cli/commands/task.py +89 -0
  48. package/src/qingflow_mcp/cli/commands/view.py +18 -0
  49. package/src/qingflow_mcp/cli/commands/workspace.py +25 -0
  50. package/src/qingflow_mcp/cli/context.py +60 -0
  51. package/src/qingflow_mcp/cli/formatters.py +334 -0
  52. package/src/qingflow_mcp/cli/json_io.py +50 -0
  53. package/src/qingflow_mcp/cli/main.py +178 -0
  54. package/src/qingflow_mcp/config.py +513 -0
  55. package/src/qingflow_mcp/errors.py +66 -0
  56. package/src/qingflow_mcp/import_store.py +121 -0
  57. package/src/qingflow_mcp/json_types.py +18 -0
  58. package/src/qingflow_mcp/list_type_labels.py +76 -0
  59. package/src/qingflow_mcp/public_surface.py +233 -0
  60. package/src/qingflow_mcp/repository_store.py +71 -0
  61. package/src/qingflow_mcp/response_trim.py +470 -0
  62. package/src/qingflow_mcp/server.py +212 -0
  63. package/src/qingflow_mcp/server_app_builder.py +533 -0
  64. package/src/qingflow_mcp/server_app_user.py +362 -0
  65. package/src/qingflow_mcp/session_store.py +302 -0
  66. package/src/qingflow_mcp/solution/__init__.py +6 -0
  67. package/src/qingflow_mcp/solution/build_assembly_store.py +181 -0
  68. package/src/qingflow_mcp/solution/compiler/__init__.py +282 -0
  69. package/src/qingflow_mcp/solution/compiler/chart_compiler.py +96 -0
  70. package/src/qingflow_mcp/solution/compiler/form_compiler.py +495 -0
  71. package/src/qingflow_mcp/solution/compiler/icon_utils.py +187 -0
  72. package/src/qingflow_mcp/solution/compiler/navigation_compiler.py +57 -0
  73. package/src/qingflow_mcp/solution/compiler/package_compiler.py +19 -0
  74. package/src/qingflow_mcp/solution/compiler/portal_compiler.py +60 -0
  75. package/src/qingflow_mcp/solution/compiler/view_compiler.py +51 -0
  76. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
  77. package/src/qingflow_mcp/solution/design_session.py +222 -0
  78. package/src/qingflow_mcp/solution/design_store.py +100 -0
  79. package/src/qingflow_mcp/solution/executor.py +2398 -0
  80. package/src/qingflow_mcp/solution/normalizer.py +23 -0
  81. package/src/qingflow_mcp/solution/requirements_builder.py +536 -0
  82. package/src/qingflow_mcp/solution/run_store.py +244 -0
  83. package/src/qingflow_mcp/solution/spec_models.py +855 -0
  84. package/src/qingflow_mcp/tools/__init__.py +1 -0
  85. package/src/qingflow_mcp/tools/ai_builder_tools.py +3419 -0
  86. package/src/qingflow_mcp/tools/app_tools.py +925 -0
  87. package/src/qingflow_mcp/tools/approval_tools.py +1062 -0
  88. package/src/qingflow_mcp/tools/auth_tools.py +875 -0
  89. package/src/qingflow_mcp/tools/base.py +388 -0
  90. package/src/qingflow_mcp/tools/code_block_tools.py +777 -0
  91. package/src/qingflow_mcp/tools/custom_button_tools.py +202 -0
  92. package/src/qingflow_mcp/tools/directory_tools.py +675 -0
  93. package/src/qingflow_mcp/tools/feedback_tools.py +238 -0
  94. package/src/qingflow_mcp/tools/file_tools.py +409 -0
  95. package/src/qingflow_mcp/tools/import_tools.py +2189 -0
  96. package/src/qingflow_mcp/tools/navigation_tools.py +210 -0
  97. package/src/qingflow_mcp/tools/package_tools.py +326 -0
  98. package/src/qingflow_mcp/tools/portal_tools.py +158 -0
  99. package/src/qingflow_mcp/tools/qingbi_report_tools.py +374 -0
  100. package/src/qingflow_mcp/tools/record_tools.py +14037 -0
  101. package/src/qingflow_mcp/tools/repository_dev_tools.py +552 -0
  102. package/src/qingflow_mcp/tools/resource_read_tools.py +421 -0
  103. package/src/qingflow_mcp/tools/role_tools.py +112 -0
  104. package/src/qingflow_mcp/tools/solution_tools.py +4054 -0
  105. package/src/qingflow_mcp/tools/task_context_tools.py +2228 -0
  106. package/src/qingflow_mcp/tools/task_tools.py +890 -0
  107. package/src/qingflow_mcp/tools/view_tools.py +335 -0
  108. package/src/qingflow_mcp/tools/workflow_tools.py +376 -0
  109. package/src/qingflow_mcp/tools/workspace_tools.py +125 -0
@@ -0,0 +1,1836 @@
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
+
823
+ @model_validator(mode="after")
824
+ def validate_shape(self) -> "FieldMutation":
825
+ if self.type == PublicFieldType.relation and not self.target_app_key:
826
+ raise ValueError("relation field requires target_app_key")
827
+ relation_patch = (
828
+ self.type == PublicFieldType.relation
829
+ or self.target_app_key is not None
830
+ or self.display_field is not None
831
+ or self.visible_fields is not None
832
+ or self.relation_mode is not None
833
+ )
834
+ 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):
835
+ raise ValueError("display_field, visible_fields, and relation_mode are only allowed for relation fields")
836
+ if self.type is not None and self.type != PublicFieldType.department and self.department_scope is not None:
837
+ raise ValueError("department_scope is only allowed for department fields")
838
+ if self.type is not None and self.type != PublicFieldType.q_linker and (
839
+ self.remote_lookup_config is not None
840
+ or self.q_linker_binding is not None
841
+ ):
842
+ raise ValueError("remote_lookup_config and q_linker_binding are only allowed for q_linker fields")
843
+ if self.type is not None and self.type != PublicFieldType.code_block and (
844
+ self.code_block_config is not None
845
+ or self.code_block_binding is not None
846
+ or self.auto_trigger is not None
847
+ or self.custom_button_text_enabled is not None
848
+ or self.custom_button_text is not None
849
+ ):
850
+ 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")
851
+ if self.type == PublicFieldType.subtable and not self.subfields:
852
+ raise ValueError("subtable field requires subfields")
853
+ return self
854
+
855
+ @model_validator(mode="before")
856
+ @classmethod
857
+ def normalize_aliases(cls, value: Any) -> Any:
858
+ return _normalize_field_payload(value)
859
+
860
+
861
+ class FieldUpdatePatch(StrictModel):
862
+ selector: FieldSelector
863
+ set: FieldMutation
864
+
865
+
866
+ class FieldRemovePatch(StrictModel):
867
+ field_id: str | None = None
868
+ que_id: int | None = None
869
+ name: str | None = Field(default=None, validation_alias=AliasChoices("name", "title", "label"))
870
+
871
+ @model_validator(mode="after")
872
+ def validate_shape(self) -> "FieldRemovePatch":
873
+ if not any((self.field_id, self.que_id, self.name)):
874
+ raise ValueError("remove patch must include field_id, que_id, or name")
875
+ return self
876
+
877
+
878
+ def _coerce_layout_columns(value: Any) -> int | None:
879
+ if isinstance(value, bool):
880
+ return None
881
+ if isinstance(value, int):
882
+ return value if value > 0 else None
883
+ if isinstance(value, str):
884
+ stripped = value.strip()
885
+ if stripped.isdigit():
886
+ parsed = int(stripped)
887
+ return parsed if parsed > 0 else None
888
+ return None
889
+
890
+
891
+ def _normalize_layout_rows(value: Any, *, columns: int | None = None) -> Any:
892
+ if not isinstance(value, list):
893
+ return value
894
+ if value and all(isinstance(item, list) for item in value):
895
+ return value
896
+ if not value:
897
+ return []
898
+ width = columns if columns and columns > 0 else None
899
+ if width is None:
900
+ return [list(value)]
901
+ return [list(value[index : index + width]) for index in range(0, len(value), width) if value[index : index + width]]
902
+
903
+
904
+ class LayoutSectionPatch(StrictModel):
905
+ type: str | None = Field(default=None, validation_alias=AliasChoices("type", "kind", "block_type", "blockType"))
906
+ section_id: str | None = Field(default=None, validation_alias=AliasChoices("section_id", "sectionId", "paragraph_id", "paragraphId"))
907
+ title: str = Field(validation_alias=AliasChoices("title", "name", "paragraph_title", "paragraphTitle"))
908
+ rows: list[list[Any]] = Field(default_factory=list)
909
+
910
+ @model_validator(mode="before")
911
+ @classmethod
912
+ def normalize_aliases(cls, value: Any) -> Any:
913
+ if not isinstance(value, dict):
914
+ return value
915
+ payload = dict(value)
916
+ if "name" in payload and "title" not in payload:
917
+ payload["title"] = payload.pop("name")
918
+ if "paragraph_title" in payload and "title" not in payload:
919
+ payload["title"] = payload.pop("paragraph_title")
920
+ if "paragraphTitle" in payload and "title" not in payload:
921
+ payload["title"] = payload.pop("paragraphTitle")
922
+ shorthand: Any | None = None
923
+ if "rows" not in payload:
924
+ if "fields" in payload:
925
+ shorthand = payload.pop("fields")
926
+ elif "field_ids" in payload:
927
+ shorthand = payload.pop("field_ids")
928
+ if shorthand is not None:
929
+ payload["rows"] = _normalize_layout_rows(
930
+ shorthand,
931
+ columns=_coerce_layout_columns(payload.pop("columns", None)),
932
+ )
933
+ return payload
934
+
935
+ @model_validator(mode="after")
936
+ def validate_rows(self) -> "LayoutSectionPatch":
937
+ if self.type is not None and str(self.type).strip().lower() != "paragraph":
938
+ raise ValueError("layout section type must be 'paragraph'")
939
+ if not self.rows:
940
+ raise ValueError("section rows must be a non-empty list")
941
+ for row in self.rows:
942
+ if not isinstance(row, list) or not row:
943
+ raise ValueError("section rows must be a non-empty list")
944
+ if not self.section_id:
945
+ self.section_id = _slugify_title(self.title)
946
+ return self
947
+
948
+
949
+ class FlowNodePatch(StrictModel):
950
+ id: str
951
+ type: PublicFlowNodeType
952
+ name: str
953
+ assignees: FlowAssigneePatch = Field(default_factory=FlowAssigneePatch)
954
+ permissions: FlowNodePermissionsPatch = Field(default_factory=FlowNodePermissionsPatch)
955
+ conditions: list[FlowConditionRulePatch] = Field(default_factory=list)
956
+ condition_groups: list[list[FlowConditionRulePatch]] = Field(default_factory=list)
957
+ config: dict[str, Any] = Field(default_factory=dict)
958
+
959
+ @model_validator(mode="before")
960
+ @classmethod
961
+ def normalize_aliases(cls, value: Any) -> Any:
962
+ if not isinstance(value, dict):
963
+ return value
964
+ payload = dict(value)
965
+ assignees = dict(payload.get("assignees") or {})
966
+ permissions = dict(payload.get("permissions") or {})
967
+
968
+ for key in ("role_ids", "role_names", "member_uids", "member_emails", "member_names", "include_sub_departs"):
969
+ if key in payload and key not in assignees:
970
+ assignees[key] = payload.pop(key)
971
+ for key in ("editable_fields",):
972
+ if key in payload and key not in permissions:
973
+ permissions[key] = payload.pop(key)
974
+ if "filters" in payload and "conditions" not in payload:
975
+ payload["conditions"] = payload.pop("filters")
976
+ if "rules" in payload and "conditions" not in payload:
977
+ payload["conditions"] = payload.pop("rules")
978
+ if "conditionRules" in payload and "condition_groups" not in payload:
979
+ payload["condition_groups"] = payload.pop("conditionRules")
980
+ if "conditionGroups" in payload and "condition_groups" not in payload:
981
+ payload["condition_groups"] = payload.pop("conditionGroups")
982
+ if "owners" in payload and "member_names" not in assignees:
983
+ assignees["member_names"] = payload.pop("owners")
984
+ if "approvers" in payload and "role_names" not in assignees:
985
+ assignees["role_names"] = payload.pop("approvers")
986
+ if assignees:
987
+ payload["assignees"] = assignees
988
+ if permissions:
989
+ payload["permissions"] = permissions
990
+ return payload
991
+
992
+ @model_validator(mode="after")
993
+ def validate_branch_conditions(self) -> "FlowNodePatch":
994
+ if self.conditions:
995
+ self.condition_groups = [list(self.conditions), *self.condition_groups]
996
+ self.conditions = []
997
+ if self.type != PublicFlowNodeType.condition and self.condition_groups:
998
+ raise ValueError("condition_groups are only allowed on condition nodes")
999
+ return self
1000
+
1001
+
1002
+ class FlowTransitionPatch(StrictModel):
1003
+ source: str = Field(alias="from")
1004
+ target: str = Field(alias="to")
1005
+
1006
+
1007
+ class ViewUpsertPatch(StrictModel):
1008
+ name: str
1009
+ view_key: str | None = Field(default=None, validation_alias=AliasChoices("view_key", "viewKey"))
1010
+ type: PublicViewType
1011
+ columns: list[str] = Field(default_factory=list)
1012
+ group_by: str | None = None
1013
+ filters: list[ViewFilterRulePatch] = Field(default_factory=list)
1014
+ start_field: str | None = Field(default=None, validation_alias=AliasChoices("start_field", "startField"))
1015
+ end_field: str | None = Field(default=None, validation_alias=AliasChoices("end_field", "endField"))
1016
+ title_field: str | None = Field(default=None, validation_alias=AliasChoices("title_field", "titleField"))
1017
+ buttons: list["ViewButtonBindingPatch"] | None = None
1018
+ visibility: VisibilityPatch | None = None
1019
+
1020
+ @model_validator(mode="before")
1021
+ @classmethod
1022
+ def normalize_aliases(cls, value: Any) -> Any:
1023
+ if not isinstance(value, dict):
1024
+ return value
1025
+ payload = dict(value)
1026
+ if "fields" in payload and "columns" not in payload:
1027
+ payload["columns"] = payload.pop("fields")
1028
+ if "column_names" in payload and "columns" not in payload:
1029
+ payload["columns"] = payload.pop("column_names")
1030
+ if "columnNames" in payload and "columns" not in payload:
1031
+ payload["columns"] = payload.pop("columnNames")
1032
+ if "filter_rules" in payload and "filters" not in payload:
1033
+ payload["filters"] = payload.pop("filter_rules")
1034
+ if "filterRules" in payload and "filters" not in payload:
1035
+ payload["filters"] = payload.pop("filterRules")
1036
+ raw_type = payload.get("type")
1037
+ if isinstance(raw_type, str):
1038
+ normalized = raw_type.strip().lower()
1039
+ if normalized == "tableview":
1040
+ payload["type"] = "table"
1041
+ elif normalized == "cardview":
1042
+ payload["type"] = "card"
1043
+ elif normalized == "kanban":
1044
+ payload["type"] = "board"
1045
+ elif normalized == "ganttview":
1046
+ payload["type"] = "gantt"
1047
+ return payload
1048
+
1049
+ @model_validator(mode="after")
1050
+ def validate_shape(self) -> "ViewUpsertPatch":
1051
+ if self.type in {PublicViewType.table, PublicViewType.card} and not self.columns:
1052
+ raise ValueError("table/card views require columns")
1053
+ if self.type == PublicViewType.board and not self.group_by:
1054
+ raise ValueError("board view requires group_by")
1055
+ if self.type == PublicViewType.gantt and not (self.start_field and self.end_field):
1056
+ raise ValueError("gantt view requires start_field and end_field")
1057
+ return self
1058
+
1059
+
1060
+ class CustomButtonJudgeValuePatch(StrictModel):
1061
+ id: int | str | None = None
1062
+ value: Any | None = None
1063
+
1064
+ @model_validator(mode="before")
1065
+ @classmethod
1066
+ def normalize_aliases(cls, value: Any) -> Any:
1067
+ if not isinstance(value, dict):
1068
+ return value
1069
+ payload = dict(value)
1070
+ if "opt_id" in payload and "id" not in payload:
1071
+ payload["id"] = payload.pop("opt_id")
1072
+ if "member_id" in payload and "id" not in payload:
1073
+ payload["id"] = payload.pop("member_id")
1074
+ return payload
1075
+
1076
+
1077
+ class CustomButtonQuestionRefPatch(StrictModel):
1078
+ que_id: int = Field(validation_alias=AliasChoices("que_id", "queId"))
1079
+ que_title: str | None = Field(default=None, validation_alias=AliasChoices("que_title", "queTitle"))
1080
+ que_type: int | None = Field(default=None, validation_alias=AliasChoices("que_type", "queType"))
1081
+
1082
+
1083
+ class CustomButtonMatchRulePatch(StrictModel):
1084
+ que_id: int = Field(validation_alias=AliasChoices("que_id", "queId"))
1085
+ que_title: str | None = Field(default=None, validation_alias=AliasChoices("que_title", "queTitle"))
1086
+ que_type: int | None = Field(default=None, validation_alias=AliasChoices("que_type", "queType"))
1087
+ date_type: int | None = Field(default=None, validation_alias=AliasChoices("date_type", "dateType"))
1088
+ judge_type: int | None = Field(default=None, validation_alias=AliasChoices("judge_type", "judgeType"))
1089
+ match_type: int | None = Field(default=None, validation_alias=AliasChoices("match_type", "matchType"))
1090
+ judge_values: list[str] = Field(default_factory=list, validation_alias=AliasChoices("judge_values", "judgeValues"))
1091
+ judge_que_type: int | None = Field(default=None, validation_alias=AliasChoices("judge_que_type", "judgeQueType"))
1092
+ judge_que_id: int | None = Field(default=None, validation_alias=AliasChoices("judge_que_id", "judgeQueId"))
1093
+ judge_que_detail: CustomButtonQuestionRefPatch | None = Field(
1094
+ default=None,
1095
+ validation_alias=AliasChoices("judge_que_detail", "judgeQueDetail"),
1096
+ )
1097
+ judge_value_details: list[CustomButtonJudgeValuePatch] = Field(
1098
+ default_factory=list,
1099
+ validation_alias=AliasChoices("judge_value_details", "judgeValueDetails"),
1100
+ )
1101
+ path_value: str | None = Field(default=None, validation_alias=AliasChoices("path_value", "pathValue"))
1102
+ table_update_type: int | None = Field(default=None, validation_alias=AliasChoices("table_update_type", "tableUpdateType"))
1103
+ multi_value: bool | None = Field(default=None, validation_alias=AliasChoices("multi_value", "multiValue"))
1104
+ add_rule: str | None = Field(default=None, validation_alias=AliasChoices("add_rule", "addRule"))
1105
+ filter_condition: list[list["CustomButtonMatchRulePatch"]] = Field(
1106
+ default_factory=list,
1107
+ validation_alias=AliasChoices("filter_condition", "filterCondition"),
1108
+ )
1109
+ field_id_prefix: str | None = Field(default=None, validation_alias=AliasChoices("field_id_prefix", "fieldIdPrefix"))
1110
+
1111
+ @model_validator(mode="before")
1112
+ @classmethod
1113
+ def normalize_aliases(cls, value: Any) -> Any:
1114
+ if not isinstance(value, dict):
1115
+ return value
1116
+ payload = dict(value)
1117
+ if "judgeValue" in payload and "judge_values" not in payload and "judgeValues" not in payload:
1118
+ payload["judge_values"] = [payload.pop("judgeValue")]
1119
+ return payload
1120
+
1121
+
1122
+ class CustomButtonAddDataConfigPatch(StrictModel):
1123
+ related_app_key: str | None = Field(default=None, validation_alias=AliasChoices("related_app_key", "relatedAppKey"))
1124
+ related_app_name: str | None = Field(default=None, validation_alias=AliasChoices("related_app_name", "relatedAppName"))
1125
+ que_relation: list[CustomButtonMatchRulePatch] = Field(
1126
+ default_factory=list,
1127
+ validation_alias=AliasChoices("que_relation", "queRelation"),
1128
+ )
1129
+
1130
+
1131
+ class CustomButtonExternalQRobotConfigPatch(StrictModel):
1132
+ external_qrobot_config_id: int | None = Field(
1133
+ default=None,
1134
+ validation_alias=AliasChoices("external_qrobot_config_id", "externalQRobotConfigId"),
1135
+ )
1136
+ triggered_text: str | None = Field(default=None, validation_alias=AliasChoices("triggered_text", "triggeredText"))
1137
+
1138
+
1139
+ class CustomButtonWingsConfigPatch(StrictModel):
1140
+ wings_agent_id: int | None = Field(default=None, validation_alias=AliasChoices("wings_agent_id", "wingsAgentId"))
1141
+ wings_agent_name: str | None = Field(default=None, validation_alias=AliasChoices("wings_agent_name", "wingsAgentName"))
1142
+ bind_que_id_list: list[int] = Field(default_factory=list, validation_alias=AliasChoices("bind_que_id_list", "bindQueIdList"))
1143
+ bind_file_que_id_list: list[int] = Field(
1144
+ default_factory=list,
1145
+ validation_alias=AliasChoices("bind_file_que_id_list", "bindFileQueIdList"),
1146
+ )
1147
+ default_prompt: str | None = Field(default=None, validation_alias=AliasChoices("default_prompt", "defaultPrompt"))
1148
+ being_auto_send: bool | None = Field(default=None, validation_alias=AliasChoices("being_auto_send", "beingAutoSend"))
1149
+
1150
+ @model_validator(mode="before")
1151
+ @classmethod
1152
+ def normalize_aliases(cls, value: Any) -> Any:
1153
+ if not isinstance(value, dict):
1154
+ return value
1155
+ payload = dict(value)
1156
+ raw_agent_id = payload.get("wings_agent_id", payload.get("wingsAgentId"))
1157
+ if isinstance(raw_agent_id, str) and raw_agent_id.strip().isdigit():
1158
+ payload["wings_agent_id"] = int(raw_agent_id.strip())
1159
+ return payload
1160
+
1161
+
1162
+ def _is_white_button_color(value: str | None) -> bool:
1163
+ normalized = str(value or "").strip().lower().replace(" ", "")
1164
+ return normalized in {
1165
+ "#fff",
1166
+ "#ffffff",
1167
+ "white",
1168
+ "rgb(255,255,255)",
1169
+ "rgba(255,255,255,1)",
1170
+ }
1171
+
1172
+
1173
+ class CustomButtonPatch(StrictModel):
1174
+ button_text: str = Field(validation_alias=AliasChoices("button_text", "buttonText"))
1175
+ background_color: str = Field(validation_alias=AliasChoices("background_color", "backgroundColor"))
1176
+ text_color: str = Field(validation_alias=AliasChoices("text_color", "textColor"))
1177
+ button_icon: str = Field(validation_alias=AliasChoices("button_icon", "buttonIcon"))
1178
+ icon_color: str | None = Field(default=None, validation_alias=AliasChoices("icon_color", "iconColor"))
1179
+ trigger_action: PublicButtonTriggerAction = Field(validation_alias=AliasChoices("trigger_action", "triggerAction"))
1180
+ trigger_link_url: str | None = Field(default=None, validation_alias=AliasChoices("trigger_link_url", "triggerLinkUrl"))
1181
+ trigger_add_data_config: CustomButtonAddDataConfigPatch | None = Field(
1182
+ default=None,
1183
+ validation_alias=AliasChoices("trigger_add_data_config", "triggerAddDataConfig"),
1184
+ )
1185
+ external_qrobot_config: CustomButtonExternalQRobotConfigPatch | None = Field(
1186
+ default=None,
1187
+ validation_alias=AliasChoices(
1188
+ "external_qrobot_config",
1189
+ "externalQrobotConfig",
1190
+ "custom_button_external_qrobot_relation_vo",
1191
+ "customButtonExternalQRobotRelationVO",
1192
+ ),
1193
+ )
1194
+ trigger_wings_config: CustomButtonWingsConfigPatch | None = Field(
1195
+ default=None,
1196
+ validation_alias=AliasChoices("trigger_wings_config", "triggerWingsConfig"),
1197
+ )
1198
+
1199
+ @model_validator(mode="after")
1200
+ def validate_shape(self) -> "CustomButtonPatch":
1201
+ if self.trigger_action == PublicButtonTriggerAction.link and not str(self.trigger_link_url or "").strip():
1202
+ raise ValueError("link buttons require trigger_link_url")
1203
+ if self.trigger_action == PublicButtonTriggerAction.add_data and self.trigger_add_data_config is None:
1204
+ raise ValueError("addData buttons require trigger_add_data_config")
1205
+ if self.trigger_action == PublicButtonTriggerAction.qrobot and self.external_qrobot_config is None:
1206
+ raise ValueError("qRobot buttons require external_qrobot_config")
1207
+ if self.trigger_action == PublicButtonTriggerAction.wings and self.trigger_wings_config is None:
1208
+ raise ValueError("wings buttons require trigger_wings_config")
1209
+ if _is_white_button_color(self.background_color) and _is_white_button_color(self.text_color):
1210
+ raise ValueError("background_color and text_color cannot both be white")
1211
+ return self
1212
+
1213
+
1214
+ class ViewButtonBindingPatch(StrictModel):
1215
+ button_type: PublicViewButtonType = Field(validation_alias=AliasChoices("button_type", "buttonType"))
1216
+ config_type: PublicViewButtonConfigType = Field(validation_alias=AliasChoices("config_type", "configType"))
1217
+ button_id: int = Field(validation_alias=AliasChoices("button_id", "buttonId", "id"))
1218
+ button_text: str | None = Field(default=None, validation_alias=AliasChoices("button_text", "buttonText"))
1219
+ button_icon: str | None = Field(default=None, validation_alias=AliasChoices("button_icon", "buttonIcon"))
1220
+ icon_color: str | None = Field(default=None, validation_alias=AliasChoices("icon_color", "iconColor"))
1221
+ background_color: str | None = Field(default=None, validation_alias=AliasChoices("background_color", "backgroundColor"))
1222
+ text_color: str | None = Field(default=None, validation_alias=AliasChoices("text_color", "textColor"))
1223
+ trigger_action: str | None = Field(default=None, validation_alias=AliasChoices("trigger_action", "triggerAction"))
1224
+ print_tpls: list[Any] = Field(default_factory=list, validation_alias=AliasChoices("print_tpls", "printTpls"))
1225
+ being_main: bool = Field(default=False, validation_alias=AliasChoices("being_main", "beingMain"))
1226
+ button_limit: list[list[ViewFilterRulePatch]] = Field(
1227
+ default_factory=list,
1228
+ validation_alias=AliasChoices("button_limit", "buttonLimit"),
1229
+ )
1230
+ button_formula: str | None = Field(default=None, validation_alias=AliasChoices("button_formula", "buttonFormula"))
1231
+ button_formula_type: int = Field(default=1, validation_alias=AliasChoices("button_formula_type", "buttonFormulaType"))
1232
+
1233
+ @model_validator(mode="before")
1234
+ @classmethod
1235
+ def normalize_aliases(cls, value: Any) -> Any:
1236
+ if not isinstance(value, dict):
1237
+ return value
1238
+ payload = dict(value)
1239
+ raw_button_type = payload.get("button_type", payload.get("buttonType"))
1240
+ if isinstance(raw_button_type, str):
1241
+ normalized_type = raw_button_type.strip().lower()
1242
+ if normalized_type == "system":
1243
+ payload["button_type"] = "SYSTEM"
1244
+ elif normalized_type == "custom":
1245
+ payload["button_type"] = "CUSTOM"
1246
+ raw_config_type = payload.get("config_type", payload.get("configType"))
1247
+ if isinstance(raw_config_type, str):
1248
+ normalized_config = raw_config_type.strip().lower()
1249
+ if normalized_config == "top":
1250
+ payload["config_type"] = "TOP"
1251
+ elif normalized_config == "detail":
1252
+ payload["config_type"] = "DETAIL"
1253
+ raw_limits = payload.get("button_limit", payload.get("buttonLimit"))
1254
+ if isinstance(raw_limits, list) and raw_limits and all(isinstance(item, dict) for item in raw_limits):
1255
+ payload["button_limit"] = [raw_limits]
1256
+ return payload
1257
+
1258
+ @model_validator(mode="after")
1259
+ def validate_shape(self) -> "ViewButtonBindingPatch":
1260
+ if self.button_type == PublicViewButtonType.system:
1261
+ missing = [
1262
+ field_name
1263
+ for field_name, value in (
1264
+ ("button_icon", self.button_icon),
1265
+ ("background_color", self.background_color),
1266
+ ("text_color", self.text_color),
1267
+ ("trigger_action", self.trigger_action),
1268
+ )
1269
+ if not str(value or "").strip()
1270
+ ]
1271
+ if missing:
1272
+ raise ValueError(f"system button bindings require {', '.join(missing)}")
1273
+ if _is_white_button_color(self.background_color) and _is_white_button_color(self.text_color):
1274
+ raise ValueError("background_color and text_color cannot both be white")
1275
+ return self
1276
+
1277
+
1278
+ class ChartFilterRulePatch(StrictModel):
1279
+ field_name: str = Field(validation_alias=AliasChoices("field_name", "fieldName", "field", "name"))
1280
+ operator: ViewFilterOperator = Field(validation_alias=AliasChoices("operator", "op"))
1281
+ values: list[Any] = Field(default_factory=list)
1282
+
1283
+ @model_validator(mode="before")
1284
+ @classmethod
1285
+ def normalize_aliases(cls, value: Any) -> Any:
1286
+ if not isinstance(value, dict):
1287
+ return value
1288
+ payload = dict(value)
1289
+ if "value" in payload and "values" not in payload:
1290
+ raw_value = payload.pop("value")
1291
+ payload["values"] = list(raw_value) if isinstance(raw_value, list) else [raw_value]
1292
+ raw_operator = payload.get("operator", payload.get("op"))
1293
+ if isinstance(raw_operator, str):
1294
+ normalized = raw_operator.strip().lower()
1295
+ operator_aliases = {
1296
+ "equals": ViewFilterOperator.eq.value,
1297
+ "equal": ViewFilterOperator.eq.value,
1298
+ "=": ViewFilterOperator.eq.value,
1299
+ "not_equals": ViewFilterOperator.neq.value,
1300
+ "not_equal": ViewFilterOperator.neq.value,
1301
+ "!=": ViewFilterOperator.neq.value,
1302
+ ">=": ViewFilterOperator.gte.value,
1303
+ "<=": ViewFilterOperator.lte.value,
1304
+ "any_of": ViewFilterOperator.in_.value,
1305
+ "one_of": ViewFilterOperator.in_.value,
1306
+ "between_any": ViewFilterOperator.in_.value,
1307
+ "empty": ViewFilterOperator.is_empty.value,
1308
+ "is blank": ViewFilterOperator.is_empty.value,
1309
+ "blank": ViewFilterOperator.is_empty.value,
1310
+ "not_empty": ViewFilterOperator.not_empty.value,
1311
+ "not blank": ViewFilterOperator.not_empty.value,
1312
+ }
1313
+ if normalized in operator_aliases:
1314
+ payload["operator"] = operator_aliases[normalized]
1315
+ elif "operator" not in payload:
1316
+ payload["operator"] = normalized
1317
+ payload.pop("op", None)
1318
+ return payload
1319
+
1320
+ @model_validator(mode="after")
1321
+ def validate_shape(self) -> "ChartFilterRulePatch":
1322
+ if self.operator in {ViewFilterOperator.is_empty, ViewFilterOperator.not_empty}:
1323
+ self.values = []
1324
+ return self
1325
+ if not self.values:
1326
+ raise ValueError("chart filter rule requires values")
1327
+ return self
1328
+
1329
+
1330
+ class ChartUpsertPatch(StrictModel):
1331
+ chart_id: str | None = None
1332
+ name: str
1333
+ chart_type: PublicChartType
1334
+ dimension_field_ids: list[str] = Field(default_factory=list)
1335
+ indicator_field_ids: list[str] = Field(default_factory=list)
1336
+ filters: list[ChartFilterRulePatch] = Field(default_factory=list)
1337
+ question_config: list[dict[str, Any]] = Field(default_factory=list)
1338
+ user_config: list[dict[str, Any]] = Field(default_factory=list)
1339
+ config: dict[str, Any] = Field(default_factory=dict)
1340
+ visibility: VisibilityPatch | None = None
1341
+
1342
+ @model_validator(mode="before")
1343
+ @classmethod
1344
+ def normalize_aliases(cls, value: Any) -> Any:
1345
+ if not isinstance(value, dict):
1346
+ return value
1347
+ payload = dict(value)
1348
+ if "id" in payload and "chart_id" not in payload:
1349
+ payload["chart_id"] = payload.pop("id")
1350
+ if "type" in payload and "chart_type" not in payload:
1351
+ payload["chart_type"] = payload.pop("type")
1352
+ if "dimension_fields" in payload and "dimension_field_ids" not in payload:
1353
+ payload["dimension_field_ids"] = payload.pop("dimension_fields")
1354
+ if "indicator_fields" in payload and "indicator_field_ids" not in payload:
1355
+ payload["indicator_field_ids"] = payload.pop("indicator_fields")
1356
+ if "metric_field_ids" in payload and "indicator_field_ids" not in payload:
1357
+ payload["indicator_field_ids"] = payload.pop("metric_field_ids")
1358
+ raw_type = payload.get("chart_type")
1359
+ if isinstance(raw_type, str):
1360
+ normalized = raw_type.strip().lower()
1361
+ aliases = {
1362
+ "targetchart": PublicChartType.target.value,
1363
+ "piechart": PublicChartType.pie.value,
1364
+ "barchart": PublicChartType.bar.value,
1365
+ "linechart": PublicChartType.line.value,
1366
+ "tablechart": PublicChartType.table.value,
1367
+ }
1368
+ if normalized in aliases:
1369
+ payload["chart_type"] = aliases[normalized]
1370
+ if isinstance(payload.get("chart_id"), int):
1371
+ payload["chart_id"] = str(payload["chart_id"])
1372
+ if isinstance(payload.get("dimension_field_ids"), list):
1373
+ payload["dimension_field_ids"] = [str(item) for item in payload["dimension_field_ids"] if item is not None and str(item).strip()]
1374
+ if isinstance(payload.get("indicator_field_ids"), list):
1375
+ payload["indicator_field_ids"] = [str(item) for item in payload["indicator_field_ids"] if item is not None and str(item).strip()]
1376
+ return payload
1377
+
1378
+
1379
+ class ChartApplyRequest(StrictModel):
1380
+ app_key: str
1381
+ upsert_charts: list[ChartUpsertPatch] = Field(default_factory=list)
1382
+ remove_chart_ids: list[str] = Field(default_factory=list)
1383
+ reorder_chart_ids: list[str] = Field(default_factory=list)
1384
+
1385
+ @model_validator(mode="before")
1386
+ @classmethod
1387
+ def normalize_ids(cls, value: Any) -> Any:
1388
+ if not isinstance(value, dict):
1389
+ return value
1390
+ payload = dict(value)
1391
+ for key in ("remove_chart_ids", "reorder_chart_ids"):
1392
+ raw = payload.get(key)
1393
+ if isinstance(raw, list):
1394
+ payload[key] = [str(item) for item in raw if item is not None and str(item).strip()]
1395
+ return payload
1396
+
1397
+ @model_validator(mode="after")
1398
+ def validate_shape(self) -> "ChartApplyRequest":
1399
+ if not self.upsert_charts and not self.remove_chart_ids and not self.reorder_chart_ids:
1400
+ raise ValueError("chart apply requires at least one upsert, remove, or reorder operation")
1401
+ return self
1402
+
1403
+
1404
+ class PortalComponentPositionPatch(StrictModel):
1405
+ pc_x: int = Field(default=0, validation_alias=AliasChoices("pc_x", "pcX", "x"))
1406
+ pc_y: int = Field(default=0, validation_alias=AliasChoices("pc_y", "pcY", "y"))
1407
+ pc_w: int = Field(default=12, validation_alias=AliasChoices("pc_w", "pcW", "w"))
1408
+ pc_h: int = Field(default=8, validation_alias=AliasChoices("pc_h", "pcH", "h"))
1409
+ mobile_x: int = Field(default=0, validation_alias=AliasChoices("mobile_x", "mobileX"))
1410
+ mobile_y: int = Field(default=0, validation_alias=AliasChoices("mobile_y", "mobileY"))
1411
+ mobile_w: int = Field(default=12, validation_alias=AliasChoices("mobile_w", "mobileW"))
1412
+ mobile_h: int = Field(default=8, validation_alias=AliasChoices("mobile_h", "mobileH"))
1413
+
1414
+ @model_validator(mode="before")
1415
+ @classmethod
1416
+ def normalize_nested_layout(cls, value: Any) -> Any:
1417
+ if not isinstance(value, dict):
1418
+ return value
1419
+ payload = dict(value)
1420
+ pc = payload.pop("pc", None)
1421
+ mobile = payload.pop("mobile", None)
1422
+ if isinstance(pc, dict):
1423
+ if "pc_x" not in payload and "x" in pc:
1424
+ payload["pc_x"] = pc.get("x")
1425
+ if "pc_y" not in payload and "y" in pc:
1426
+ payload["pc_y"] = pc.get("y")
1427
+ if "pc_w" not in payload and "cols" in pc:
1428
+ payload["pc_w"] = pc.get("cols")
1429
+ if "pc_h" not in payload and "rows" in pc:
1430
+ payload["pc_h"] = pc.get("rows")
1431
+ if isinstance(mobile, dict):
1432
+ if "mobile_x" not in payload and "x" in mobile:
1433
+ payload["mobile_x"] = mobile.get("x")
1434
+ if "mobile_y" not in payload and "y" in mobile:
1435
+ payload["mobile_y"] = mobile.get("y")
1436
+ if "mobile_w" not in payload and "cols" in mobile:
1437
+ payload["mobile_w"] = mobile.get("cols")
1438
+ if "mobile_h" not in payload and "rows" in mobile:
1439
+ payload["mobile_h"] = mobile.get("rows")
1440
+ return payload
1441
+
1442
+
1443
+ class PortalChartRefPatch(StrictModel):
1444
+ app_key: str
1445
+ chart_id: str | None = None
1446
+ chart_name: str | None = None
1447
+
1448
+ @model_validator(mode="after")
1449
+ def validate_target(self) -> "PortalChartRefPatch":
1450
+ if not (self.chart_id or self.chart_name):
1451
+ raise ValueError("chart_ref requires chart_id or chart_name")
1452
+ return self
1453
+
1454
+
1455
+ class PortalViewRefPatch(StrictModel):
1456
+ app_key: str
1457
+ view_key: str | None = None
1458
+ view_name: str | None = None
1459
+
1460
+ @model_validator(mode="after")
1461
+ def validate_target(self) -> "PortalViewRefPatch":
1462
+ if not (self.view_key or self.view_name):
1463
+ raise ValueError("view_ref requires view_key or view_name")
1464
+ return self
1465
+
1466
+
1467
+ class PortalSectionPatch(StrictModel):
1468
+ title: str
1469
+ source_type: str = Field(validation_alias=AliasChoices("source_type", "sourceType"))
1470
+ position: PortalComponentPositionPatch | None = None
1471
+ dash_style_config: dict[str, Any] | None = Field(default=None, validation_alias=AliasChoices("dash_style_config", "dashStyleConfigBO"))
1472
+ config: dict[str, Any] = Field(default_factory=dict)
1473
+ chart_ref: PortalChartRefPatch | None = None
1474
+ view_ref: PortalViewRefPatch | None = None
1475
+ text: str | None = None
1476
+ url: str | None = None
1477
+
1478
+ @model_validator(mode="before")
1479
+ @classmethod
1480
+ def normalize_aliases(cls, value: Any) -> Any:
1481
+ if not isinstance(value, dict):
1482
+ return value
1483
+ payload = dict(value)
1484
+ raw_type = payload.get("source_type", payload.get("sourceType"))
1485
+ if isinstance(raw_type, str):
1486
+ payload["source_type"] = raw_type.strip().lower()
1487
+ if "chartRef" in payload and "chart_ref" not in payload:
1488
+ payload["chart_ref"] = payload.pop("chartRef")
1489
+ if "viewRef" in payload and "view_ref" not in payload:
1490
+ payload["view_ref"] = payload.pop("viewRef")
1491
+ if "dashStyleConfigBO" in payload and "dash_style_config" not in payload:
1492
+ payload["dash_style_config"] = payload.pop("dashStyleConfigBO")
1493
+ return payload
1494
+
1495
+ @model_validator(mode="after")
1496
+ def validate_shape(self) -> "PortalSectionPatch":
1497
+ supported = {"chart", "view", "grid", "filter", "text", "link"}
1498
+ if self.source_type not in supported:
1499
+ raise ValueError(f"unsupported portal source_type '{self.source_type}'")
1500
+ if self.source_type == "chart" and self.chart_ref is None:
1501
+ raise ValueError("chart section requires chart_ref")
1502
+ if self.source_type == "view" and self.view_ref is None:
1503
+ raise ValueError("view section requires view_ref")
1504
+ if self.source_type == "text" and self.text is None:
1505
+ raise ValueError("text section requires text")
1506
+ if self.source_type == "link" and self.url is None:
1507
+ raise ValueError("link section requires url")
1508
+ return self
1509
+
1510
+
1511
+ class PortalApplyRequest(StrictModel):
1512
+ dash_key: str | None = None
1513
+ dash_name: str | None = None
1514
+ package_tag_id: int | None = None
1515
+ publish: bool = True
1516
+ sections: list[PortalSectionPatch] = Field(default_factory=list)
1517
+ visibility: VisibilityPatch | None = None
1518
+ auth: dict[str, Any] | None = None
1519
+ icon: str | None = None
1520
+ color: str | None = None
1521
+ hide_copyright: bool | None = Field(default=None, validation_alias=AliasChoices("hide_copyright", "hideCopyright"))
1522
+ dash_global_config: dict[str, Any] | None = Field(default=None, validation_alias=AliasChoices("dash_global_config", "dashGlobalConfig"))
1523
+ config: dict[str, Any] = Field(default_factory=dict)
1524
+
1525
+ @model_validator(mode="after")
1526
+ def validate_shape(self) -> "PortalApplyRequest":
1527
+ if not self.dash_key and not self.package_tag_id:
1528
+ raise ValueError("package_tag_id is required when dash_key is empty")
1529
+ if not self.dash_key and not self.dash_name:
1530
+ raise ValueError("dash_name is required when creating a portal")
1531
+ if not self.sections:
1532
+ raise ValueError("portal apply requires a non-empty sections list")
1533
+ if self.visibility is not None and self.auth is not None:
1534
+ raise ValueError("visibility and auth cannot be provided together")
1535
+ return self
1536
+
1537
+
1538
+ FieldPatch.model_rebuild()
1539
+
1540
+
1541
+ class AppGetResponse(StrictModel):
1542
+ app_key: str
1543
+ title: str | None = None
1544
+ app_icon: str | None = None
1545
+ visibility: dict[str, Any] = Field(default_factory=dict)
1546
+ tag_ids: list[int] = Field(default_factory=list)
1547
+ publish_status: int | None = None
1548
+ field_count: int = 0
1549
+ layout_section_count: int = 0
1550
+ view_count: int = 0
1551
+ workflow_enabled: bool = False
1552
+ verification_hints: list[str] = Field(default_factory=list)
1553
+ editability: dict[str, bool | None] = Field(default_factory=dict)
1554
+
1555
+
1556
+ class AppGetFieldsResponse(StrictModel):
1557
+ app_key: str
1558
+ fields: list[dict[str, Any]] = Field(default_factory=list)
1559
+ field_count: int = 0
1560
+
1561
+
1562
+ class AppGetLayoutResponse(StrictModel):
1563
+ app_key: str
1564
+ sections: list[dict[str, Any]] = Field(default_factory=list)
1565
+ unplaced_fields: list[str] = Field(default_factory=list)
1566
+ layout_mode_detected: str = "empty"
1567
+
1568
+
1569
+ class AppGetViewsResponse(StrictModel):
1570
+ app_key: str
1571
+ views: list[dict[str, Any]] = Field(default_factory=list)
1572
+
1573
+
1574
+ class AppGetFlowResponse(StrictModel):
1575
+ app_key: str
1576
+ enabled: bool = False
1577
+ nodes: list[dict[str, Any]] = Field(default_factory=list)
1578
+ transitions: list[dict[str, Any]] = Field(default_factory=list)
1579
+
1580
+
1581
+ class AppGetChartsResponse(StrictModel):
1582
+ app_key: str
1583
+ charts: list[dict[str, Any]] = Field(default_factory=list)
1584
+ chart_count: int = 0
1585
+
1586
+
1587
+ AppReadSummaryResponse = AppGetResponse
1588
+ AppFieldsReadResponse = AppGetFieldsResponse
1589
+ AppLayoutReadResponse = AppGetLayoutResponse
1590
+ AppViewsReadResponse = AppGetViewsResponse
1591
+ AppFlowReadResponse = AppGetFlowResponse
1592
+ AppChartsReadResponse = AppGetChartsResponse
1593
+
1594
+
1595
+ class PortalListResponse(StrictModel):
1596
+ items: list[dict[str, Any]] = Field(default_factory=list)
1597
+ total: int = 0
1598
+
1599
+
1600
+ class PortalReadSummaryResponse(StrictModel):
1601
+ dash_key: str
1602
+ being_draft: bool = True
1603
+ dash_name: str | None = None
1604
+ package_tag_ids: list[int] = Field(default_factory=list)
1605
+ dash_icon: str | None = None
1606
+ hide_copyright: bool | None = None
1607
+ config_keys: list[str] = Field(default_factory=list)
1608
+ dash_global_config_keys: list[str] = Field(default_factory=list)
1609
+ section_count: int = 0
1610
+ sections: list[dict[str, Any]] = Field(default_factory=list)
1611
+
1612
+
1613
+ class PortalGetResponse(StrictModel):
1614
+ dash_key: str
1615
+ being_draft: bool = True
1616
+ dash_name: str | None = None
1617
+ package_tag_ids: list[int] = Field(default_factory=list)
1618
+ dash_icon: str | None = None
1619
+ hide_copyright: bool | None = None
1620
+ visibility: dict[str, Any] = Field(default_factory=dict)
1621
+ auth: dict[str, Any] = Field(default_factory=dict)
1622
+ config: dict[str, Any] = Field(default_factory=dict)
1623
+ dash_global_config: dict[str, Any] = Field(default_factory=dict)
1624
+ component_count: int = 0
1625
+ components: list[dict[str, Any]] = Field(default_factory=list)
1626
+
1627
+
1628
+ class ViewGetResponse(StrictModel):
1629
+ view_key: str
1630
+ base_info: dict[str, Any] = Field(default_factory=dict)
1631
+ visibility: dict[str, Any] = Field(default_factory=dict)
1632
+ config: dict[str, Any] = Field(default_factory=dict)
1633
+ questions: list[dict[str, Any]] = Field(default_factory=list)
1634
+ associations: list[dict[str, Any]] = Field(default_factory=list)
1635
+
1636
+
1637
+ class ChartGetResponse(StrictModel):
1638
+ chart_id: str
1639
+ base: dict[str, Any] = Field(default_factory=dict)
1640
+ visibility: dict[str, Any] = Field(default_factory=dict)
1641
+ config: dict[str, Any] = Field(default_factory=dict)
1642
+
1643
+
1644
+ class SchemaPlanRequest(StrictModel):
1645
+ app_key: str = ""
1646
+ package_tag_id: int | None = None
1647
+ app_name: str = Field(default="", validation_alias=AliasChoices("app_name", "app_title", "title"))
1648
+ icon: str | None = None
1649
+ color: str | None = None
1650
+ visibility: VisibilityPatch | None = None
1651
+ create_if_missing: bool = False
1652
+ add_fields: list[FieldPatch] = Field(default_factory=list)
1653
+ update_fields: list[FieldUpdatePatch] = Field(default_factory=list)
1654
+ remove_fields: list[FieldRemovePatch] = Field(default_factory=list)
1655
+
1656
+
1657
+ class LayoutPlanRequest(StrictModel):
1658
+ app_key: str
1659
+ mode: LayoutApplyMode = LayoutApplyMode.merge
1660
+ sections: list[LayoutSectionPatch] = Field(default_factory=list)
1661
+ preset: LayoutPreset | None = None
1662
+
1663
+ @model_validator(mode="before")
1664
+ @classmethod
1665
+ def normalize_mode_alias(cls, value: Any) -> Any:
1666
+ if not isinstance(value, dict):
1667
+ return value
1668
+ payload = dict(value)
1669
+ if str(payload.get("mode") or "").strip().lower() == "overwrite":
1670
+ payload["mode"] = "replace"
1671
+ return payload
1672
+
1673
+
1674
+ class FlowPlanRequest(StrictModel):
1675
+ app_key: str
1676
+ mode: str = "replace"
1677
+ nodes: list[FlowNodePatch] = Field(default_factory=list)
1678
+ transitions: list[FlowTransitionPatch] = Field(default_factory=list)
1679
+ preset: FlowPreset | None = None
1680
+
1681
+ @model_validator(mode="before")
1682
+ @classmethod
1683
+ def normalize_mode_alias(cls, value: Any) -> Any:
1684
+ if not isinstance(value, dict):
1685
+ return value
1686
+ payload = dict(value)
1687
+ if str(payload.get("mode") or "").strip().lower() == "overwrite":
1688
+ payload["mode"] = "replace"
1689
+ raw_preset = payload.get("preset")
1690
+ if raw_preset is None and isinstance(payload.get("base_preset"), str):
1691
+ raw_preset = payload["base_preset"]
1692
+ payload["preset"] = raw_preset
1693
+ if isinstance(raw_preset, str):
1694
+ normalized_preset = raw_preset.strip().lower()
1695
+ preset_aliases = {
1696
+ "default_approval": FlowPreset.basic_approval.value,
1697
+ "approval": FlowPreset.basic_approval.value,
1698
+ "basic approval": FlowPreset.basic_approval.value,
1699
+ "default_fill_then_approve": FlowPreset.basic_fill_then_approve.value,
1700
+ "default-fill-then-approve": FlowPreset.basic_fill_then_approve.value,
1701
+ "fill_then_approve": FlowPreset.basic_fill_then_approve.value,
1702
+ }
1703
+ if normalized_preset in preset_aliases:
1704
+ payload["preset"] = preset_aliases[normalized_preset]
1705
+ return payload
1706
+
1707
+
1708
+ class ViewsPlanRequest(StrictModel):
1709
+ app_key: str
1710
+ upsert_views: list[ViewUpsertPatch] = Field(default_factory=list)
1711
+ remove_views: list[str] = Field(default_factory=list)
1712
+ preset: ViewsPreset | None = None
1713
+
1714
+
1715
+ class OperationResultEnvelope(StrictModel):
1716
+ status: str
1717
+ error_code: str | None = None
1718
+ recoverable: bool = False
1719
+ message: str
1720
+ normalized_args: dict[str, Any] = Field(default_factory=dict)
1721
+ missing_fields: list[str] = Field(default_factory=list)
1722
+ allowed_values: dict[str, Any] = Field(default_factory=dict)
1723
+ details: dict[str, Any] = Field(default_factory=dict)
1724
+ request_id: str | None = None
1725
+ suggested_next_call: dict[str, Any] | None = None
1726
+ noop: bool = False
1727
+ verification: dict[str, Any] = Field(default_factory=dict)
1728
+
1729
+
1730
+ def _normalize_field_payload(value: Any) -> Any:
1731
+ if not isinstance(value, dict):
1732
+ return value
1733
+ payload = dict(value)
1734
+ if "fields" in payload and "subfields" not in payload:
1735
+ payload["subfields"] = payload.pop("fields")
1736
+ raw_type = payload.get("type")
1737
+ if isinstance(raw_type, int):
1738
+ normalized_from_id = FIELD_TYPE_ID_ALIASES.get(raw_type)
1739
+ if normalized_from_id is not None:
1740
+ payload["type"] = normalized_from_id.value
1741
+ if isinstance(raw_type, str):
1742
+ normalized = FIELD_TYPE_ALIASES.get(raw_type.strip().lower())
1743
+ if normalized is not None:
1744
+ payload["type"] = normalized.value
1745
+ normalized_relation_mode = _normalize_public_relation_mode(
1746
+ payload.get("relation_mode", payload.get("relationMode", payload.get("selection_mode", payload.get("selectionMode"))))
1747
+ )
1748
+ if normalized_relation_mode is None:
1749
+ for alias_key in ("optional_data_num", "optionalDataNum", "multiple", "allow_multiple"):
1750
+ if alias_key in payload:
1751
+ normalized_relation_mode = _normalize_public_relation_mode(payload.get(alias_key))
1752
+ break
1753
+ if normalized_relation_mode is not None:
1754
+ payload["relation_mode"] = normalized_relation_mode
1755
+ for alias_key in (
1756
+ "relationMode",
1757
+ "selection_mode",
1758
+ "selectionMode",
1759
+ "optional_data_num",
1760
+ "optionalDataNum",
1761
+ "multiple",
1762
+ "allow_multiple",
1763
+ ):
1764
+ payload.pop(alias_key, None)
1765
+ return payload
1766
+
1767
+
1768
+ def _slugify_title(title: str) -> str:
1769
+ normalized = "".join(ch.lower() if ch.isalnum() else "_" for ch in str(title or ""))
1770
+ collapsed = "_".join(part for part in normalized.split("_") if part)
1771
+ return collapsed or "section"
1772
+
1773
+
1774
+ def _normalize_public_relation_mode(value: Any) -> str | None:
1775
+ if value is None:
1776
+ return None
1777
+ if isinstance(value, bool):
1778
+ return PublicRelationMode.multiple.value if value else PublicRelationMode.single.value
1779
+ if isinstance(value, int):
1780
+ if value == 0:
1781
+ return PublicRelationMode.multiple.value
1782
+ if value == 1:
1783
+ return PublicRelationMode.single.value
1784
+ return None
1785
+ if isinstance(value, str):
1786
+ normalized = value.strip().lower()
1787
+ aliases = {
1788
+ "single": PublicRelationMode.single.value,
1789
+ "single_select": PublicRelationMode.single.value,
1790
+ "single-select": PublicRelationMode.single.value,
1791
+ "one": PublicRelationMode.single.value,
1792
+ "1": PublicRelationMode.single.value,
1793
+ "multiple": PublicRelationMode.multiple.value,
1794
+ "multi": PublicRelationMode.multiple.value,
1795
+ "multi_select": PublicRelationMode.multiple.value,
1796
+ "multi-select": PublicRelationMode.multiple.value,
1797
+ "many": PublicRelationMode.multiple.value,
1798
+ "0": PublicRelationMode.multiple.value,
1799
+ }
1800
+ return aliases.get(normalized, normalized or None)
1801
+ return None
1802
+
1803
+
1804
+ def _normalize_public_department_scope_mode(value: Any) -> str | None:
1805
+ if value is None:
1806
+ return None
1807
+ if isinstance(value, int):
1808
+ if value == 1:
1809
+ return PublicDepartmentScopeMode.all.value
1810
+ if value == 2:
1811
+ return PublicDepartmentScopeMode.custom.value
1812
+ return None
1813
+ if isinstance(value, str):
1814
+ normalized = value.strip().lower()
1815
+ aliases = {
1816
+ "all": PublicDepartmentScopeMode.all.value,
1817
+ "workspace_all": PublicDepartmentScopeMode.all.value,
1818
+ "workspace-all": PublicDepartmentScopeMode.all.value,
1819
+ "default": PublicDepartmentScopeMode.all.value,
1820
+ "default_all": PublicDepartmentScopeMode.all.value,
1821
+ "default-all": PublicDepartmentScopeMode.all.value,
1822
+ "1": PublicDepartmentScopeMode.all.value,
1823
+ "custom": PublicDepartmentScopeMode.custom.value,
1824
+ "explicit": PublicDepartmentScopeMode.custom.value,
1825
+ "selected": PublicDepartmentScopeMode.custom.value,
1826
+ "2": PublicDepartmentScopeMode.custom.value,
1827
+ }
1828
+ return aliases.get(normalized, normalized or None)
1829
+ return None
1830
+
1831
+
1832
+ CustomButtonMatchRulePatch.model_rebuild()
1833
+ CustomButtonAddDataConfigPatch.model_rebuild()
1834
+ CodeBlockAliasPathPatch.model_rebuild()
1835
+ ViewButtonBindingPatch.model_rebuild()
1836
+ ViewUpsertPatch.model_rebuild()