@josephyan/qingflow-cli 0.2.0-beta.68 → 0.2.0-beta.70

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.
@@ -320,7 +320,7 @@ class AuthTools(ToolBase):
320
320
  )
321
321
  if refreshed_profile is not None:
322
322
  session_profile = refreshed_profile
323
- return {
323
+ response = {
324
324
  "profile": session_profile.profile,
325
325
  "base_url": session_profile.base_url,
326
326
  "qf_version": session_profile.qf_version,
@@ -333,6 +333,14 @@ class AuthTools(ToolBase):
333
333
  "persisted": session_profile.persisted,
334
334
  "request_route": self._request_route_payload(context),
335
335
  }
336
+ member_info, member_warnings = self._workspace_member_info(
337
+ session_profile=session_profile,
338
+ backend_session=backend_session,
339
+ )
340
+ response.update(member_info)
341
+ if member_warnings:
342
+ response["warnings"] = member_warnings
343
+ return response
336
344
 
337
345
  def auth_logout(self, *, profile: str = DEFAULT_PROFILE, forget_persisted: bool = False) -> dict[str, Any]:
338
346
  if not self.sessions.has_profile(profile):
@@ -589,6 +597,256 @@ class AuthTools(ToolBase):
589
597
  )
590
598
  return refreshed
591
599
 
600
+ def _workspace_member_info(
601
+ self,
602
+ *,
603
+ session_profile, # type: ignore[no-untyped-def]
604
+ backend_session, # type: ignore[no-untyped-def]
605
+ ) -> tuple[dict[str, Any], list[dict[str, Any]]]:
606
+ default_payload = {
607
+ "departments": [],
608
+ "roles": [],
609
+ "permission_level": None,
610
+ }
611
+ ws_id = session_profile.selected_ws_id
612
+ if ws_id is None:
613
+ return default_payload, []
614
+
615
+ context = BackendRequestContext(
616
+ base_url=backend_session.base_url,
617
+ token=backend_session.token,
618
+ ws_id=ws_id,
619
+ qf_version=backend_session.qf_version,
620
+ qf_version_source=backend_session.qf_version_source,
621
+ )
622
+ permission_level = self._resolve_permission_level(
623
+ self._workspace_auth(context, ws_id=ws_id)
624
+ )
625
+ payload = dict(default_payload)
626
+ payload["permission_level"] = permission_level
627
+
628
+ member = self._lookup_current_member(
629
+ context=context,
630
+ uid=session_profile.uid,
631
+ email=session_profile.email,
632
+ nick_name=session_profile.nick_name,
633
+ )
634
+ if member is None:
635
+ return payload, [
636
+ {
637
+ "code": "CURRENT_MEMBER_PROFILE_UNAVAILABLE",
638
+ "message": (
639
+ "auth_whoami could not resolve current member departments and roles "
640
+ f"in workspace {ws_id}."
641
+ ),
642
+ }
643
+ ]
644
+
645
+ payload["departments"] = self._compact_departments(member)
646
+ payload["roles"] = self._compact_roles(member)
647
+ return payload, []
648
+
649
+ def _workspace_auth(self, context: BackendRequestContext, *, ws_id: int) -> int | None:
650
+ workspace = self._fetch_workspace_auth_from_detail(context, ws_id=ws_id)
651
+ if workspace is not None:
652
+ return workspace
653
+ return self._fetch_workspace_auth_from_list(context, ws_id=ws_id)
654
+
655
+ def _fetch_workspace_auth_from_detail(self, context: BackendRequestContext, *, ws_id: int) -> int | None:
656
+ try:
657
+ workspace = self.backend.request("GET", context, f"/user/workspace/{ws_id}")
658
+ except QingflowApiError:
659
+ return None
660
+ if not isinstance(workspace, dict):
661
+ return None
662
+ return self._coerce_auth_value(workspace.get("auth"))
663
+
664
+ def _fetch_workspace_auth_from_list(self, context: BackendRequestContext, *, ws_id: int) -> int | None:
665
+ try:
666
+ payload = self.backend.request(
667
+ "POST",
668
+ context,
669
+ "/user/workspaceList/pageQuery",
670
+ json_body={"pageNum": 1, "pageSize": 100, "authList": [0, 1, 2, 3]},
671
+ )
672
+ except QingflowApiError:
673
+ return None
674
+ workspaces = payload.get("list") if isinstance(payload, dict) else []
675
+ if not isinstance(workspaces, list):
676
+ return None
677
+ for item in workspaces:
678
+ if not isinstance(item, dict) or item.get("wsId") != ws_id:
679
+ continue
680
+ return self._coerce_auth_value(item.get("auth"))
681
+ return None
682
+
683
+ def _lookup_current_member(
684
+ self,
685
+ *,
686
+ context: BackendRequestContext,
687
+ uid: int | None,
688
+ email: str | None,
689
+ nick_name: str | None,
690
+ ) -> dict[str, Any] | None:
691
+ candidates: list[dict[str, Any]] = []
692
+ for keyword in (email, nick_name):
693
+ member = self._search_member_once(context, uid=uid, keyword=keyword)
694
+ if member is not None:
695
+ return member
696
+ if keyword:
697
+ candidates.extend(self._search_member_items(context, keyword=keyword))
698
+ if uid is not None and uid > 0:
699
+ for item in candidates:
700
+ if self._same_member(item, uid=uid):
701
+ return item
702
+ return self._search_member_once(context, uid=uid, keyword=None)
703
+ return None
704
+
705
+ def _search_member_once(
706
+ self,
707
+ context: BackendRequestContext,
708
+ *,
709
+ uid: int | None,
710
+ keyword: str | None,
711
+ ) -> dict[str, Any] | None:
712
+ for item in self._search_member_items(context, keyword=keyword):
713
+ if self._same_member(item, uid=uid):
714
+ return item
715
+ return None
716
+
717
+ def _search_member_items(self, context: BackendRequestContext, *, keyword: str | None) -> list[dict[str, Any]]:
718
+ params: dict[str, Any] = {"pageNum": 1, "pageSize": 100, "containDisable": True}
719
+ normalized_keyword = str(keyword or "").strip()
720
+ if normalized_keyword:
721
+ params["keyword"] = normalized_keyword
722
+ try:
723
+ payload = self.backend.request("GET", context, "/contact", params=params)
724
+ except QingflowApiError:
725
+ return []
726
+ items = self._extract_items(payload)
727
+ return [item for item in items if isinstance(item, dict)]
728
+
729
+ def _same_member(self, item: dict[str, Any], *, uid: int | None) -> bool:
730
+ if uid is None or uid <= 0:
731
+ return False
732
+ for key in ("uid", "id", "userId"):
733
+ value = item.get(key)
734
+ if value is None:
735
+ continue
736
+ coerced = self._coerce_int(value)
737
+ if coerced is not None and coerced == uid:
738
+ return True
739
+ if str(value).strip() == str(uid):
740
+ return True
741
+ return False
742
+
743
+ def _compact_departments(self, member: dict[str, Any]) -> list[dict[str, Any]]:
744
+ items: list[dict[str, Any]] = []
745
+ seen: set[tuple[int | None, str | None]] = set()
746
+ for depart in self._walk_nested_items(member.get("departs")):
747
+ if not isinstance(depart, dict):
748
+ continue
749
+ dept_id = self._coerce_int(
750
+ depart.get("deptId", depart.get("departId", depart.get("id")))
751
+ )
752
+ dept_name = self._normalize_text(
753
+ depart.get("deptName", depart.get("departName", depart.get("name")))
754
+ )
755
+ key = (dept_id, dept_name)
756
+ if key in seen or (dept_id is None and dept_name is None):
757
+ continue
758
+ seen.add(key)
759
+ item = {"dept_id": dept_id, "dept_name": dept_name}
760
+ items.append({k: v for k, v in item.items() if v is not None})
761
+ return items
762
+
763
+ def _compact_roles(self, member: dict[str, Any]) -> list[dict[str, Any]]:
764
+ items: list[dict[str, Any]] = []
765
+ seen: set[tuple[int | None, str | None]] = set()
766
+ for role in self._walk_nested_items(member.get("roles")):
767
+ if not isinstance(role, dict):
768
+ continue
769
+ role_id = self._coerce_int(role.get("roleId", role.get("id")))
770
+ role_name = self._normalize_text(role.get("roleName", role.get("name")))
771
+ key = (role_id, role_name)
772
+ if key in seen or (role_id is None and role_name is None):
773
+ continue
774
+ seen.add(key)
775
+ item = {"role_id": role_id, "role_name": role_name}
776
+ items.append({k: v for k, v in item.items() if v is not None})
777
+ return items
778
+
779
+ def _resolve_permission_level(self, auth_code: int | None) -> str | None:
780
+ mapping = {
781
+ 2: "超级管理",
782
+ 1: "系统管理员",
783
+ 3: "子管理员",
784
+ 0: "基本成员",
785
+ }
786
+ return mapping.get(auth_code)
787
+
788
+ def _coerce_auth_value(self, value: Any) -> int | None:
789
+ coerced = self._coerce_int(value)
790
+ if coerced is not None:
791
+ return coerced
792
+ normalized = self._normalize_text(value)
793
+ if normalized is None:
794
+ return None
795
+ lowered = normalized.lower()
796
+ if lowered in {"creator", "workspaccreator", "workspacecreator"}:
797
+ return 2
798
+ if lowered in {"admin", "administrator"}:
799
+ return 1
800
+ if lowered in {"subadmin", "dataadmin"}:
801
+ return 3
802
+ if lowered in {"member", "visitor", "normal"}:
803
+ return 0
804
+ return None
805
+
806
+ def _extract_items(self, payload: Any) -> list[Any]:
807
+ if isinstance(payload, list):
808
+ return payload
809
+ if not isinstance(payload, dict):
810
+ return []
811
+ for key in ("list", "items", "rows", "result"):
812
+ value = payload.get(key)
813
+ if isinstance(value, list):
814
+ return value
815
+ for key in ("data", "page"):
816
+ nested = payload.get(key)
817
+ if isinstance(nested, list):
818
+ return nested
819
+ if isinstance(nested, dict):
820
+ for nested_key in ("list", "items", "rows", "result"):
821
+ value = nested.get(nested_key)
822
+ if isinstance(value, list):
823
+ return value
824
+ return []
825
+
826
+ def _walk_nested_items(self, value: Any) -> list[Any]:
827
+ if isinstance(value, list):
828
+ items: list[Any] = []
829
+ for item in value:
830
+ items.extend(self._walk_nested_items(item))
831
+ return items
832
+ return [value]
833
+
834
+ def _coerce_int(self, value: Any) -> int | None:
835
+ if isinstance(value, bool) or value is None:
836
+ return None
837
+ if isinstance(value, int):
838
+ return value
839
+ try:
840
+ return int(str(value).strip())
841
+ except (TypeError, ValueError):
842
+ return None
843
+
844
+ def _normalize_text(self, value: Any) -> str | None:
845
+ if value is None:
846
+ return None
847
+ text = str(value).strip()
848
+ return text or None
849
+
592
850
  def _fetch_workspace_with_name_fallback(
593
851
  self,
594
852
  base_url: str,
@@ -129,6 +129,25 @@ class ImportTools(ToolBase):
129
129
  import_id: str | None = None,
130
130
  process_id_str: str | None = None,
131
131
  ) -> dict[str, Any]:
132
+ selector_count = sum(
133
+ 1
134
+ for item in (
135
+ bool(_normalize_optional_text(process_id_str)),
136
+ bool(_normalize_optional_text(import_id)),
137
+ bool(str(app_key or "").strip()),
138
+ )
139
+ if item
140
+ )
141
+ if selector_count != 1:
142
+ return self._failed_status_result(
143
+ error_code="CONFIG_ERROR",
144
+ message="record_import_status_get accepts exactly one selector: process_id_str, import_id, or app_key",
145
+ extra={
146
+ "details": {
147
+ "fix_hint": "Use `process_id_str` or `import_id` for a known import, or use only `app_key` to inspect the latest import in that app.",
148
+ }
149
+ },
150
+ )
132
151
  return self.record_import_status_get(
133
152
  profile=profile,
134
153
  app_key=app_key,
@@ -729,30 +748,62 @@ class ImportTools(ToolBase):
729
748
  self,
730
749
  *,
731
750
  profile: str,
732
- app_key: str,
751
+ app_key: str = "",
733
752
  import_id: str | None = None,
734
753
  process_id_str: str | None = None,
735
754
  ) -> dict[str, Any]:
736
- if not app_key.strip():
737
- return self._failed_status_result(error_code="IMPORT_STATUS_AMBIGUOUS", message="app_key is required")
755
+ normalized_app_key = (app_key or "").strip()
756
+ normalized_import_id = _normalize_optional_text(import_id)
757
+ normalized_process_id = _normalize_optional_text(process_id_str)
758
+ if normalized_import_id and normalized_process_id:
759
+ return self._failed_status_result(
760
+ error_code="CONFIG_ERROR",
761
+ message="record_import_status_get accepts import_id or process_id_str, but not both at the same time",
762
+ extra={
763
+ "details": {
764
+ "fix_hint": "Use only one of `import_id` or `process_id_str`. You may pass `app_key` as an optional routing hint for direct method compatibility.",
765
+ }
766
+ },
767
+ )
768
+ if not normalized_process_id and not normalized_import_id and not normalized_app_key:
769
+ return self._failed_status_result(
770
+ error_code="CONFIG_ERROR",
771
+ message="record_import_status_get requires at least one selector: process_id_str, import_id, or app_key",
772
+ extra={
773
+ "details": {
774
+ "fix_hint": "Use `process_id_str` or `import_id` for a known import, or use only `app_key` to inspect the latest import in that app.",
775
+ }
776
+ },
777
+ )
738
778
 
739
779
  def runner(_session_profile, context):
740
780
  local_job = None
741
- normalized_import_id = _normalize_optional_text(import_id)
742
- normalized_process_id = _normalize_optional_text(process_id_str)
743
781
  if normalized_import_id:
744
782
  local_job = self._job_store.get(normalized_import_id)
745
783
  if local_job is None and normalized_process_id:
746
784
  matches = [item for item in self._job_store.list() if _normalize_optional_text(item.get("process_id_str")) == normalized_process_id]
747
785
  local_job = matches[0] if len(matches) == 1 else None
786
+ resolved_app_key = normalized_app_key
787
+ if not resolved_app_key and isinstance(local_job, dict):
788
+ resolved_app_key = str(local_job.get("app_key") or "").strip()
789
+ if not resolved_app_key:
790
+ return self._failed_status_result(
791
+ error_code="CONFIG_ERROR",
792
+ message="record_import_status_get could not determine app_key from the provided selector",
793
+ extra={
794
+ "details": {
795
+ "fix_hint": "Use the original `app_key`, or call import status with the latest-import mode: only `app_key`.",
796
+ }
797
+ },
798
+ )
748
799
  if local_job is None and not normalized_import_id and not normalized_process_id:
749
- recent = [item for item in self._job_store.list() if str(item.get("app_key")) == app_key]
800
+ recent = [item for item in self._job_store.list() if str(item.get("app_key")) == resolved_app_key]
750
801
  local_job = recent[0] if recent else None
751
802
  page = self.backend.request(
752
803
  "GET",
753
804
  context,
754
805
  "/app/apply/dataImport/record",
755
- params={"appKey": app_key, "pageNum": 1, "pageSize": 100},
806
+ params={"appKey": resolved_app_key, "pageNum": 1, "pageSize": 100},
756
807
  )
757
808
  records = _extract_import_records(page)
758
809
  matched_record, matched_by = _match_import_record(
@@ -785,6 +836,7 @@ class ImportTools(ToolBase):
785
836
  return {
786
837
  "ok": True,
787
838
  "status": _normalize_optional_text(matched_record.get("processStatus")) or "unknown",
839
+ "app_key": resolved_app_key,
788
840
  "import_id": normalized_import_id or (local_job.get("import_id") if isinstance(local_job, dict) else None),
789
841
  "process_id_str": normalized_process,
790
842
  "matched_by": matched_by,
@@ -155,16 +155,33 @@ class QingbiReportTools(ToolBase):
155
155
  page_size_y: int | None = None,
156
156
  ) -> JSONObject:
157
157
  self._require_chart_id(chart_id)
158
- params = {"qfUUID": uuid4().hex}
159
- if page_num is not None:
160
- params["pageNum"] = page_num
161
- if page_size is not None:
162
- params["pageSize"] = page_size
158
+ params = {
159
+ "qfUUID": uuid4().hex,
160
+ # Match Qingflow's real BI/qflow read path defaults so a caller can
161
+ # fetch chart data without having to synthesize pagination first.
162
+ "pageNum": page_num if page_num is not None else 1,
163
+ "pageSize": page_size if page_size is not None else 20,
164
+ }
163
165
  if page_num_y is not None:
164
166
  params["pageNumY"] = page_num_y
165
167
  if page_size_y is not None:
166
168
  params["pageSizeY"] = page_size_y
167
- return self._request(profile, "POST", f"/qingbi/charts/data/{chart_id}", chart_id=chart_id, params=params, json_body=payload)
169
+ if payload:
170
+ return self._request(
171
+ profile,
172
+ "POST",
173
+ f"/qingbi/charts/data/qflow/{chart_id}/detail",
174
+ chart_id=chart_id,
175
+ params=params,
176
+ json_body=payload,
177
+ )
178
+ return self._request(
179
+ profile,
180
+ "GET",
181
+ f"/qingbi/charts/data/qflow/{chart_id}",
182
+ chart_id=chart_id,
183
+ params=params,
184
+ )
168
185
 
169
186
  def qingbi_report_delete(self, *, profile: str, chart_id: str) -> JSONObject:
170
187
  self._require_chart_id(chart_id)
@@ -317,9 +317,6 @@ class RecordTools(ToolBase):
317
317
  limit: int = 50,
318
318
  strict_full: bool = True,
319
319
  view_id: str | None = None,
320
- list_type: int | None = None,
321
- view_key: str | None = None,
322
- view_name: str | None = None,
323
320
  output_profile: str = "normal",
324
321
  ) -> JSONObject:
325
322
  return self.record_analyze(
@@ -332,9 +329,9 @@ class RecordTools(ToolBase):
332
329
  limit=limit,
333
330
  strict_full=strict_full,
334
331
  view_id=view_id,
335
- list_type=list_type,
336
- view_key=view_key,
337
- view_name=view_name,
332
+ list_type=None,
333
+ view_key=None,
334
+ view_name=None,
338
335
  output_profile=output_profile,
339
336
  )
340
337
 
@@ -354,9 +351,6 @@ class RecordTools(ToolBase):
354
351
  limit: int = 50,
355
352
  page: int = 1,
356
353
  view_id: str | None = None,
357
- list_type: int | None = None,
358
- view_key: str | None = None,
359
- view_name: str | None = None,
360
354
  output_profile: str = "normal",
361
355
  ) -> JSONObject:
362
356
  return self.record_list(
@@ -368,9 +362,9 @@ class RecordTools(ToolBase):
368
362
  limit=limit,
369
363
  page=page,
370
364
  view_id=view_id,
371
- list_type=list_type,
372
- view_key=view_key,
373
- view_name=view_name,
365
+ list_type=None,
366
+ view_key=None,
367
+ view_name=None,
374
368
  output_profile=output_profile,
375
369
  )
376
370
 
@@ -139,21 +139,28 @@ class WorkspaceTools(ToolBase):
139
139
  qf_version=workspace_qf_version,
140
140
  qf_version_source="workspace_system_version",
141
141
  )
142
+ active_context = BackendRequestContext(
143
+ base_url=session_profile.base_url,
144
+ token=context.token,
145
+ ws_id=session_profile.selected_ws_id,
146
+ qf_version=session_profile.qf_version,
147
+ qf_version_source=session_profile.qf_version_source,
148
+ )
149
+ workspace_version = self._workspace_version_summary(
150
+ self._fetch_workspace_account_info(active_context),
151
+ workspace_base_info=self._fetch_workspace_base_info(active_context),
152
+ workspace_detail=result,
153
+ )
142
154
  return {
143
155
  "profile": profile,
144
156
  "selected_ws_id": session_profile.selected_ws_id,
145
157
  "selected_ws_name": session_profile.selected_ws_name,
146
158
  "workspace": result,
159
+ "workspace_version": workspace_version,
147
160
  "qf_version": session_profile.qf_version,
148
161
  "qf_version_source": session_profile.qf_version_source,
149
162
  "request_route": self.backend.describe_route(
150
- BackendRequestContext(
151
- base_url=session_profile.base_url,
152
- token=context.token,
153
- ws_id=session_profile.selected_ws_id,
154
- qf_version=session_profile.qf_version,
155
- qf_version_source=session_profile.qf_version_source,
156
- )
163
+ active_context
157
164
  ),
158
165
  }
159
166
 
@@ -189,6 +196,116 @@ class WorkspaceTools(ToolBase):
189
196
  normalized = str(value).strip()
190
197
  return normalized or None
191
198
 
199
+ def _fetch_workspace_base_info(self, context: BackendRequestContext) -> dict[str, Any] | None:
200
+ try:
201
+ payload = self.backend.request("GET", context, "/ws/baseInfo")
202
+ except QingflowApiError:
203
+ return None
204
+ return payload if isinstance(payload, dict) else None
205
+
206
+ def _fetch_workspace_account_info(self, context: BackendRequestContext) -> dict[str, Any] | None:
207
+ try:
208
+ payload = self.backend.request("GET", context, "/ws/account")
209
+ except QingflowApiError:
210
+ return None
211
+ return payload if isinstance(payload, dict) else None
212
+
213
+ def _workspace_version_summary(
214
+ self,
215
+ payload: Any,
216
+ *,
217
+ workspace_base_info: Any,
218
+ workspace_detail: Any,
219
+ ) -> dict[str, Any]:
220
+ account_info = payload if isinstance(payload, dict) else {}
221
+ base_info = workspace_base_info if isinstance(workspace_base_info, dict) else {}
222
+ detail_info = workspace_detail if isinstance(workspace_detail, dict) else {}
223
+ level_code = self._first_present_int(
224
+ account_info.get("accountLevel"),
225
+ base_info.get("accountLevel"),
226
+ detail_info.get("accountLevel"),
227
+ )
228
+ level_name = self._account_level_name(level_code)
229
+ return {
230
+ "level_code": level_code,
231
+ "level_name": level_name,
232
+ "display_name": self._account_level_display_name(level_name),
233
+ "being_trial": self._first_present_bool(
234
+ account_info.get("trial"),
235
+ base_info.get("trial"),
236
+ detail_info.get("trial"),
237
+ ),
238
+ "expire_date": self._first_present_value(
239
+ account_info.get("expireDate"),
240
+ base_info.get("expireDate"),
241
+ detail_info.get("expireDate"),
242
+ ),
243
+ }
244
+
245
+ def _account_level_name(self, level_code: int | None) -> str | None:
246
+ mapping = {
247
+ 0: "FREE",
248
+ 10: "AIR",
249
+ 20: "BASIC",
250
+ 30: "TEAM",
251
+ 35: "PROFESSIONAL",
252
+ 40: "ENTERPRISE",
253
+ }
254
+ return mapping.get(level_code)
255
+
256
+ def _account_level_display_name(self, level_name: str | None) -> str | None:
257
+ mapping = {
258
+ "FREE": "免费版",
259
+ "AIR": "Air版",
260
+ "BASIC": "Pro版",
261
+ "TEAM": "团队版",
262
+ "PROFESSIONAL": "专业版",
263
+ "ENTERPRISE": "企业版",
264
+ }
265
+ return mapping.get(level_name)
266
+
267
+ def _coerce_int(self, value: Any) -> int | None:
268
+ if isinstance(value, bool) or value is None:
269
+ return None
270
+ if isinstance(value, int):
271
+ return value
272
+ try:
273
+ return int(str(value).strip())
274
+ except (TypeError, ValueError):
275
+ return None
276
+
277
+ def _coerce_bool(self, value: Any) -> bool | None:
278
+ if isinstance(value, bool):
279
+ return value
280
+ if value is None:
281
+ return None
282
+ normalized = str(value).strip().lower()
283
+ if normalized in {"true", "1"}:
284
+ return True
285
+ if normalized in {"false", "0"}:
286
+ return False
287
+ return None
288
+
289
+ def _first_present_int(self, *values: Any) -> int | None:
290
+ for value in values:
291
+ coerced = self._coerce_int(value)
292
+ if coerced is not None:
293
+ return coerced
294
+ return None
295
+
296
+ def _first_present_bool(self, *values: Any) -> bool | None:
297
+ for value in values:
298
+ coerced = self._coerce_bool(value)
299
+ if coerced is not None:
300
+ return coerced
301
+ return None
302
+
303
+ def _first_present_value(self, *values: Any) -> Any:
304
+ for value in values:
305
+ if value is not None:
306
+ return value
307
+ return None
308
+
192
309
  def workspace_set_plugin_status(
193
310
  self,
194
311
  *,