rossum-mcp 1.1.0__py3-none-any.whl → 1.1.1__py3-none-any.whl
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.
- rossum_mcp/__init__.py +1 -1
- rossum_mcp/tools/schemas/__init__.py +182 -0
- rossum_mcp/tools/schemas/models.py +151 -0
- rossum_mcp/tools/schemas/operations.py +183 -0
- rossum_mcp/tools/schemas/patching.py +202 -0
- rossum_mcp/tools/schemas/pruning.py +133 -0
- rossum_mcp/tools/schemas/validation.py +128 -0
- {rossum_mcp-1.1.0.dist-info → rossum_mcp-1.1.1.dist-info}/METADATA +1 -1
- {rossum_mcp-1.1.0.dist-info → rossum_mcp-1.1.1.dist-info}/RECORD +13 -8
- rossum_mcp/tools/schemas.py +0 -800
- {rossum_mcp-1.1.0.dist-info → rossum_mcp-1.1.1.dist-info}/WHEEL +0 -0
- {rossum_mcp-1.1.0.dist-info → rossum_mcp-1.1.1.dist-info}/entry_points.txt +0 -0
- {rossum_mcp-1.1.0.dist-info → rossum_mcp-1.1.1.dist-info}/licenses/LICENSE +0 -0
- {rossum_mcp-1.1.0.dist-info → rossum_mcp-1.1.1.dist-info}/top_level.txt +0 -0
rossum_mcp/__init__.py
CHANGED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Schema tools for Rossum MCP Server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from rossum_api.models.schema import Schema
|
|
8
|
+
|
|
9
|
+
from rossum_mcp.tools.schemas.models import (
|
|
10
|
+
DatapointType,
|
|
11
|
+
NodeCategory,
|
|
12
|
+
SchemaDatapoint,
|
|
13
|
+
SchemaMultivalue,
|
|
14
|
+
SchemaNode,
|
|
15
|
+
SchemaNodeUpdate,
|
|
16
|
+
SchemaTreeNode,
|
|
17
|
+
SchemaTuple,
|
|
18
|
+
)
|
|
19
|
+
from rossum_mcp.tools.schemas.operations import (
|
|
20
|
+
create_schema,
|
|
21
|
+
delete_schema,
|
|
22
|
+
get_schema,
|
|
23
|
+
get_schema_tree_structure,
|
|
24
|
+
list_schemas,
|
|
25
|
+
patch_schema,
|
|
26
|
+
prune_schema_fields,
|
|
27
|
+
update_schema,
|
|
28
|
+
)
|
|
29
|
+
from rossum_mcp.tools.schemas.patching import (
|
|
30
|
+
PatchOperation,
|
|
31
|
+
_apply_add_operation,
|
|
32
|
+
_apply_remove_operation,
|
|
33
|
+
_apply_update_operation,
|
|
34
|
+
_find_node_anywhere,
|
|
35
|
+
_find_node_in_children,
|
|
36
|
+
_find_parent_children_list,
|
|
37
|
+
_get_section_children_as_list,
|
|
38
|
+
apply_schema_patch,
|
|
39
|
+
)
|
|
40
|
+
from rossum_mcp.tools.schemas.pruning import (
|
|
41
|
+
_collect_all_field_ids,
|
|
42
|
+
_collect_ancestor_ids,
|
|
43
|
+
_extract_schema_tree,
|
|
44
|
+
_remove_fields_from_content,
|
|
45
|
+
)
|
|
46
|
+
from rossum_mcp.tools.schemas.validation import (
|
|
47
|
+
MAX_ID_LENGTH,
|
|
48
|
+
VALID_DATAPOINT_TYPES,
|
|
49
|
+
VALID_UI_CONFIGURATION_EDIT,
|
|
50
|
+
VALID_UI_CONFIGURATION_TYPES,
|
|
51
|
+
SchemaValidationError,
|
|
52
|
+
_validate_datapoint,
|
|
53
|
+
_validate_id,
|
|
54
|
+
_validate_multivalue,
|
|
55
|
+
_validate_node,
|
|
56
|
+
_validate_section,
|
|
57
|
+
_validate_tuple,
|
|
58
|
+
sanitize_schema_content,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if TYPE_CHECKING:
|
|
62
|
+
from fastmcp import FastMCP
|
|
63
|
+
from rossum_api import AsyncRossumAPIClient
|
|
64
|
+
|
|
65
|
+
__all__ = [
|
|
66
|
+
"MAX_ID_LENGTH",
|
|
67
|
+
"VALID_DATAPOINT_TYPES",
|
|
68
|
+
"VALID_UI_CONFIGURATION_EDIT",
|
|
69
|
+
"VALID_UI_CONFIGURATION_TYPES",
|
|
70
|
+
"DatapointType",
|
|
71
|
+
"NodeCategory",
|
|
72
|
+
"PatchOperation",
|
|
73
|
+
"SchemaDatapoint",
|
|
74
|
+
"SchemaMultivalue",
|
|
75
|
+
"SchemaNode",
|
|
76
|
+
"SchemaNodeUpdate",
|
|
77
|
+
"SchemaTreeNode",
|
|
78
|
+
"SchemaTuple",
|
|
79
|
+
"SchemaValidationError",
|
|
80
|
+
"_apply_add_operation",
|
|
81
|
+
"_apply_remove_operation",
|
|
82
|
+
"_apply_update_operation",
|
|
83
|
+
"_collect_all_field_ids",
|
|
84
|
+
"_collect_ancestor_ids",
|
|
85
|
+
"_extract_schema_tree",
|
|
86
|
+
"_find_node_anywhere",
|
|
87
|
+
"_find_node_in_children",
|
|
88
|
+
"_find_parent_children_list",
|
|
89
|
+
"_get_section_children_as_list",
|
|
90
|
+
"_remove_fields_from_content",
|
|
91
|
+
"_validate_datapoint",
|
|
92
|
+
"_validate_id",
|
|
93
|
+
"_validate_multivalue",
|
|
94
|
+
"_validate_node",
|
|
95
|
+
"_validate_section",
|
|
96
|
+
"_validate_tuple",
|
|
97
|
+
"apply_schema_patch",
|
|
98
|
+
"create_schema",
|
|
99
|
+
"delete_schema",
|
|
100
|
+
"get_schema",
|
|
101
|
+
"get_schema_tree_structure",
|
|
102
|
+
"list_schemas",
|
|
103
|
+
"patch_schema",
|
|
104
|
+
"prune_schema_fields",
|
|
105
|
+
"register_schema_tools",
|
|
106
|
+
"sanitize_schema_content",
|
|
107
|
+
"update_schema",
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def register_schema_tools(mcp: FastMCP, client: AsyncRossumAPIClient) -> None:
|
|
112
|
+
"""Register schema-related tools with the FastMCP server."""
|
|
113
|
+
from rossum_mcp.tools.schemas import operations as ops # noqa: PLC0415 - avoid circular import
|
|
114
|
+
|
|
115
|
+
@mcp.tool(description="Retrieve schema details.")
|
|
116
|
+
async def get_schema(schema_id: int) -> Schema | dict:
|
|
117
|
+
return await ops.get_schema(client, schema_id)
|
|
118
|
+
|
|
119
|
+
@mcp.tool(description="List all schemas with optional filters.")
|
|
120
|
+
async def list_schemas(name: str | None = None, queue_id: int | None = None) -> list[Schema]:
|
|
121
|
+
return await ops.list_schemas(client, name, queue_id)
|
|
122
|
+
|
|
123
|
+
@mcp.tool(description="Update schema, typically for field-level thresholds.")
|
|
124
|
+
async def update_schema(schema_id: int, schema_data: dict) -> Schema | dict:
|
|
125
|
+
return await ops.update_schema(client, schema_id, schema_data)
|
|
126
|
+
|
|
127
|
+
@mcp.tool(description="Create a schema. Must have ≥1 section with children (datapoints).")
|
|
128
|
+
async def create_schema(name: str, content: list[dict]) -> Schema | dict:
|
|
129
|
+
return await ops.create_schema(client, name, content)
|
|
130
|
+
|
|
131
|
+
@mcp.tool(
|
|
132
|
+
description="""Patch schema nodes (add/update/remove fields in a schema).
|
|
133
|
+
|
|
134
|
+
You MUST load `schema-patching` skill first to avoid errors.
|
|
135
|
+
|
|
136
|
+
Operations:
|
|
137
|
+
- add: Create new field. Requires parent_id (section or tuple id) and node_data.
|
|
138
|
+
- update: Modify existing field. Requires node_data with fields to change.
|
|
139
|
+
- remove: Delete field. Only requires node_id.
|
|
140
|
+
|
|
141
|
+
Node types for add:
|
|
142
|
+
- Datapoint (simple field): {"label": "Field Name", "category": "datapoint", "type": "string|number|date|enum"}
|
|
143
|
+
- Enum field: Include "options": [{"value": "v1", "label": "Label 1"}, ...]
|
|
144
|
+
- Multivalue (table): {"label": "Table", "category": "multivalue", "children": <tuple>}
|
|
145
|
+
- Tuple (table row): {"id": "row_id", "label": "Row", "category": "tuple", "children": [<datapoints with id>]}
|
|
146
|
+
|
|
147
|
+
Important: Datapoints inside a tuple MUST have an "id" field. Section-level datapoints get id from node_id parameter.
|
|
148
|
+
"""
|
|
149
|
+
)
|
|
150
|
+
async def patch_schema(
|
|
151
|
+
schema_id: int,
|
|
152
|
+
operation: PatchOperation,
|
|
153
|
+
node_id: str,
|
|
154
|
+
node_data: SchemaNode | SchemaNodeUpdate | None = None,
|
|
155
|
+
parent_id: str | None = None,
|
|
156
|
+
position: int | None = None,
|
|
157
|
+
) -> Schema | dict:
|
|
158
|
+
return await ops.patch_schema(client, schema_id, operation, node_id, node_data, parent_id, position)
|
|
159
|
+
|
|
160
|
+
@mcp.tool(description="Get lightweight tree structure of schema with only ids, labels, categories, and types.")
|
|
161
|
+
async def get_schema_tree_structure(schema_id: int) -> list[dict] | dict:
|
|
162
|
+
return await ops.get_schema_tree_structure(client, schema_id)
|
|
163
|
+
|
|
164
|
+
@mcp.tool(
|
|
165
|
+
description="""Remove multiple fields from schema at once. Efficient for pruning unwanted fields during setup.
|
|
166
|
+
|
|
167
|
+
Use fields_to_keep OR fields_to_remove (not both):
|
|
168
|
+
- fields_to_keep: Keep only these field IDs (plus sections). All others removed.
|
|
169
|
+
- fields_to_remove: Remove these specific field IDs.
|
|
170
|
+
|
|
171
|
+
Returns dict with removed_fields and remaining_fields lists. Sections cannot be removed."""
|
|
172
|
+
)
|
|
173
|
+
async def prune_schema_fields(
|
|
174
|
+
schema_id: int,
|
|
175
|
+
fields_to_keep: list[str] | None = None,
|
|
176
|
+
fields_to_remove: list[str] | None = None,
|
|
177
|
+
) -> dict:
|
|
178
|
+
return await ops.prune_schema_fields(client, schema_id, fields_to_keep, fields_to_remove)
|
|
179
|
+
|
|
180
|
+
@mcp.tool(description="Delete a schema. Fails if schema is linked to a queue or annotation (HTTP 409).")
|
|
181
|
+
async def delete_schema(schema_id: int) -> dict:
|
|
182
|
+
return await ops.delete_schema(client, schema_id)
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Schema dataclass models for Rossum MCP Server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import asdict, dataclass
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
DatapointType = Literal["string", "number", "date", "enum", "button"]
|
|
9
|
+
NodeCategory = Literal["datapoint", "multivalue", "tuple"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class SchemaDatapoint:
|
|
14
|
+
"""A datapoint node for schema patch operations.
|
|
15
|
+
|
|
16
|
+
Use for adding/updating fields that capture or display values.
|
|
17
|
+
When used inside a tuple (table), id is required.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
label: str
|
|
21
|
+
id: str | None = None
|
|
22
|
+
category: Literal["datapoint"] = "datapoint"
|
|
23
|
+
type: DatapointType | None = None
|
|
24
|
+
rir_field_names: list[str] | None = None
|
|
25
|
+
default_value: str | None = None
|
|
26
|
+
score_threshold: float | None = None
|
|
27
|
+
hidden: bool = False
|
|
28
|
+
disable_prediction: bool = False
|
|
29
|
+
can_export: bool = True
|
|
30
|
+
constraints: dict | None = None
|
|
31
|
+
options: list[dict] | None = None
|
|
32
|
+
ui_configuration: dict | None = None
|
|
33
|
+
formula: str | None = None
|
|
34
|
+
prompt: str | None = None
|
|
35
|
+
context: list[str] | None = None
|
|
36
|
+
width: int | None = None
|
|
37
|
+
stretch: bool | None = None
|
|
38
|
+
|
|
39
|
+
def to_dict(self) -> dict:
|
|
40
|
+
"""Convert to dict, excluding None values."""
|
|
41
|
+
return {k: v for k, v in asdict(self).items() if v is not None}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class SchemaTuple:
|
|
46
|
+
"""A tuple node for schema patch operations.
|
|
47
|
+
|
|
48
|
+
Use within multivalue to define table row structure with multiple columns.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
id: str
|
|
52
|
+
label: str
|
|
53
|
+
children: list[SchemaDatapoint]
|
|
54
|
+
category: Literal["tuple"] = "tuple"
|
|
55
|
+
hidden: bool = False
|
|
56
|
+
|
|
57
|
+
def to_dict(self) -> dict:
|
|
58
|
+
"""Convert to dict, excluding None values."""
|
|
59
|
+
result: dict = {"id": self.id, "category": self.category, "label": self.label}
|
|
60
|
+
if self.hidden:
|
|
61
|
+
result["hidden"] = self.hidden
|
|
62
|
+
result["children"] = [child.to_dict() for child in self.children]
|
|
63
|
+
return result
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class SchemaMultivalue:
|
|
68
|
+
"""A multivalue node for schema patch operations.
|
|
69
|
+
|
|
70
|
+
Use for repeating fields or tables. Children is a single Tuple or Datapoint (NOT a list).
|
|
71
|
+
The id is optional here since it gets set from node_id in patch_schema.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
label: str
|
|
75
|
+
children: SchemaTuple | SchemaDatapoint
|
|
76
|
+
id: str | None = None
|
|
77
|
+
category: Literal["multivalue"] = "multivalue"
|
|
78
|
+
rir_field_names: list[str] | None = None
|
|
79
|
+
min_occurrences: int | None = None
|
|
80
|
+
max_occurrences: int | None = None
|
|
81
|
+
hidden: bool = False
|
|
82
|
+
|
|
83
|
+
def to_dict(self) -> dict:
|
|
84
|
+
"""Convert to dict, excluding None values."""
|
|
85
|
+
result: dict = {"label": self.label, "category": self.category}
|
|
86
|
+
if self.id:
|
|
87
|
+
result["id"] = self.id
|
|
88
|
+
if self.rir_field_names:
|
|
89
|
+
result["rir_field_names"] = self.rir_field_names
|
|
90
|
+
if self.min_occurrences is not None:
|
|
91
|
+
result["min_occurrences"] = self.min_occurrences
|
|
92
|
+
if self.max_occurrences is not None:
|
|
93
|
+
result["max_occurrences"] = self.max_occurrences
|
|
94
|
+
if self.hidden:
|
|
95
|
+
result["hidden"] = self.hidden
|
|
96
|
+
result["children"] = self.children.to_dict()
|
|
97
|
+
return result
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass
|
|
101
|
+
class SchemaNodeUpdate:
|
|
102
|
+
"""Partial update for an existing schema node.
|
|
103
|
+
|
|
104
|
+
Only include fields you want to update - all fields are optional.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
label: str | None = None
|
|
108
|
+
type: DatapointType | None = None
|
|
109
|
+
score_threshold: float | None = None
|
|
110
|
+
hidden: bool | None = None
|
|
111
|
+
disable_prediction: bool | None = None
|
|
112
|
+
can_export: bool | None = None
|
|
113
|
+
default_value: str | None = None
|
|
114
|
+
rir_field_names: list[str] | None = None
|
|
115
|
+
constraints: dict | None = None
|
|
116
|
+
options: list[dict] | None = None
|
|
117
|
+
ui_configuration: dict | None = None
|
|
118
|
+
formula: str | None = None
|
|
119
|
+
prompt: str | None = None
|
|
120
|
+
context: list[str] | None = None
|
|
121
|
+
width: int | None = None
|
|
122
|
+
stretch: bool | None = None
|
|
123
|
+
min_occurrences: int | None = None
|
|
124
|
+
max_occurrences: int | None = None
|
|
125
|
+
|
|
126
|
+
def to_dict(self) -> dict:
|
|
127
|
+
"""Convert to dict, excluding None values."""
|
|
128
|
+
return {k: v for k, v in asdict(self).items() if v is not None}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@dataclass
|
|
132
|
+
class SchemaTreeNode:
|
|
133
|
+
"""Lightweight schema node for tree structure display."""
|
|
134
|
+
|
|
135
|
+
id: str
|
|
136
|
+
label: str
|
|
137
|
+
category: str
|
|
138
|
+
type: str | None = None
|
|
139
|
+
children: list[SchemaTreeNode] | None = None
|
|
140
|
+
|
|
141
|
+
def to_dict(self) -> dict:
|
|
142
|
+
"""Convert to dict, excluding None values."""
|
|
143
|
+
result: dict = {"id": self.id, "label": self.label, "category": self.category}
|
|
144
|
+
if self.type:
|
|
145
|
+
result["type"] = self.type
|
|
146
|
+
if self.children:
|
|
147
|
+
result["children"] = [child.to_dict() for child in self.children]
|
|
148
|
+
return result
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
SchemaNode = SchemaDatapoint | SchemaMultivalue | SchemaTuple
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Schema CRUD operations for Rossum MCP Server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from dataclasses import asdict, is_dataclass
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
from rossum_api import APIClientError
|
|
10
|
+
from rossum_api.domain_logic.resources import Resource
|
|
11
|
+
from rossum_api.models.schema import Schema
|
|
12
|
+
|
|
13
|
+
from rossum_mcp.tools.base import TRUNCATED_MARKER, delete_resource, is_read_write_mode
|
|
14
|
+
from rossum_mcp.tools.schemas.models import SchemaNode, SchemaNodeUpdate # noqa: TC001 - needed at runtime for FastMCP
|
|
15
|
+
from rossum_mcp.tools.schemas.patching import PatchOperation, apply_schema_patch
|
|
16
|
+
from rossum_mcp.tools.schemas.pruning import (
|
|
17
|
+
_collect_all_field_ids,
|
|
18
|
+
_collect_ancestor_ids,
|
|
19
|
+
_extract_schema_tree,
|
|
20
|
+
_remove_fields_from_content,
|
|
21
|
+
)
|
|
22
|
+
from rossum_mcp.tools.schemas.validation import sanitize_schema_content
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from rossum_api import AsyncRossumAPIClient
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _truncate_schema_for_list(schema: Schema) -> Schema:
|
|
31
|
+
"""Truncate content field in schema to save context in list responses."""
|
|
32
|
+
from dataclasses import replace # noqa: PLC0415 - avoid circular import with models
|
|
33
|
+
|
|
34
|
+
return replace(schema, content=TRUNCATED_MARKER)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
async def get_schema(client: AsyncRossumAPIClient, schema_id: int) -> Schema | dict:
|
|
38
|
+
try:
|
|
39
|
+
schema: Schema = await client.retrieve_schema(schema_id)
|
|
40
|
+
return schema
|
|
41
|
+
except APIClientError as e:
|
|
42
|
+
if e.status_code == 404:
|
|
43
|
+
return {"error": f"Schema {schema_id} not found"}
|
|
44
|
+
raise
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async def list_schemas(
|
|
48
|
+
client: AsyncRossumAPIClient, name: str | None = None, queue_id: int | None = None
|
|
49
|
+
) -> list[Schema]:
|
|
50
|
+
logger.debug(f"Listing schemas: name={name}, queue_id={queue_id}")
|
|
51
|
+
filters: dict[str, int | str] = {}
|
|
52
|
+
if name is not None:
|
|
53
|
+
filters["name"] = name
|
|
54
|
+
if queue_id is not None:
|
|
55
|
+
filters["queue"] = queue_id
|
|
56
|
+
|
|
57
|
+
schemas = [schema async for schema in client.list_schemas(**filters)] # type: ignore[arg-type]
|
|
58
|
+
return [_truncate_schema_for_list(schema) for schema in schemas]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
async def update_schema(client: AsyncRossumAPIClient, schema_id: int, schema_data: dict) -> Schema | dict:
|
|
62
|
+
if not is_read_write_mode():
|
|
63
|
+
return {"error": "update_schema is not available in read-only mode"}
|
|
64
|
+
|
|
65
|
+
logger.debug(f"Updating schema: schema_id={schema_id}")
|
|
66
|
+
await client._http_client.update(Resource.Schema, schema_id, schema_data)
|
|
67
|
+
updated_schema: Schema = await client.retrieve_schema(schema_id)
|
|
68
|
+
return updated_schema
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async def create_schema(client: AsyncRossumAPIClient, name: str, content: list[dict]) -> Schema | dict:
|
|
72
|
+
if not is_read_write_mode():
|
|
73
|
+
return {"error": "create_schema is not available in read-only mode"}
|
|
74
|
+
|
|
75
|
+
logger.debug(f"Creating schema: name={name}")
|
|
76
|
+
sanitized_content = sanitize_schema_content(content)
|
|
77
|
+
schema_data = {"name": name, "content": sanitized_content}
|
|
78
|
+
schema: Schema = await client.create_new_schema(schema_data)
|
|
79
|
+
return schema
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
async def patch_schema(
|
|
83
|
+
client: AsyncRossumAPIClient,
|
|
84
|
+
schema_id: int,
|
|
85
|
+
operation: PatchOperation,
|
|
86
|
+
node_id: str,
|
|
87
|
+
node_data: SchemaNode | SchemaNodeUpdate | None = None,
|
|
88
|
+
parent_id: str | None = None,
|
|
89
|
+
position: int | None = None,
|
|
90
|
+
) -> Schema | dict:
|
|
91
|
+
if not is_read_write_mode():
|
|
92
|
+
return {"error": "patch_schema is not available in read-only mode"}
|
|
93
|
+
|
|
94
|
+
if operation not in ("add", "update", "remove"):
|
|
95
|
+
return {"error": f"Invalid operation '{operation}'. Must be 'add', 'update', or 'remove'."}
|
|
96
|
+
|
|
97
|
+
logger.debug(f"Patching schema: schema_id={schema_id}, operation={operation}, node_id={node_id}")
|
|
98
|
+
|
|
99
|
+
node_data_dict: dict | None = None
|
|
100
|
+
if node_data is not None:
|
|
101
|
+
if isinstance(node_data, dict):
|
|
102
|
+
node_data_dict = node_data
|
|
103
|
+
elif hasattr(node_data, "to_dict"):
|
|
104
|
+
node_data_dict = node_data.to_dict()
|
|
105
|
+
else:
|
|
106
|
+
node_data_dict = asdict(node_data)
|
|
107
|
+
|
|
108
|
+
current_schema: dict = await client._http_client.request_json("GET", f"schemas/{schema_id}")
|
|
109
|
+
content_list = current_schema.get("content", [])
|
|
110
|
+
if not isinstance(content_list, list):
|
|
111
|
+
return {"error": "Unexpected schema content format"}
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
patched_content = apply_schema_patch(
|
|
115
|
+
content=content_list,
|
|
116
|
+
operation=operation,
|
|
117
|
+
node_id=node_id,
|
|
118
|
+
node_data=node_data_dict,
|
|
119
|
+
parent_id=parent_id,
|
|
120
|
+
position=position,
|
|
121
|
+
)
|
|
122
|
+
except ValueError as e:
|
|
123
|
+
return {"error": str(e)}
|
|
124
|
+
|
|
125
|
+
sanitized_content = sanitize_schema_content(patched_content)
|
|
126
|
+
await client._http_client.update(Resource.Schema, schema_id, {"content": sanitized_content})
|
|
127
|
+
updated_schema: Schema = await client.retrieve_schema(schema_id)
|
|
128
|
+
return updated_schema
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
async def get_schema_tree_structure(client: AsyncRossumAPIClient, schema_id: int) -> list[dict] | dict:
|
|
132
|
+
schema = await get_schema(client, schema_id)
|
|
133
|
+
if isinstance(schema, dict):
|
|
134
|
+
return schema
|
|
135
|
+
content_dicts: list[dict[str, Any]] = [
|
|
136
|
+
asdict(section) if is_dataclass(section) else dict(section) # type: ignore[arg-type]
|
|
137
|
+
for section in schema.content
|
|
138
|
+
]
|
|
139
|
+
return _extract_schema_tree(content_dicts)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
async def prune_schema_fields(
|
|
143
|
+
client: AsyncRossumAPIClient,
|
|
144
|
+
schema_id: int,
|
|
145
|
+
fields_to_keep: list[str] | None = None,
|
|
146
|
+
fields_to_remove: list[str] | None = None,
|
|
147
|
+
) -> dict:
|
|
148
|
+
if not is_read_write_mode():
|
|
149
|
+
return {"error": "prune_schema_fields is not available in read-only mode"}
|
|
150
|
+
|
|
151
|
+
if fields_to_keep and fields_to_remove:
|
|
152
|
+
return {"error": "Specify fields_to_keep OR fields_to_remove, not both"}
|
|
153
|
+
if not fields_to_keep and not fields_to_remove:
|
|
154
|
+
return {"error": "Must specify fields_to_keep or fields_to_remove"}
|
|
155
|
+
|
|
156
|
+
current_schema: dict = await client._http_client.request_json("GET", f"schemas/{schema_id}")
|
|
157
|
+
content = current_schema.get("content", [])
|
|
158
|
+
if not isinstance(content, list):
|
|
159
|
+
return {"error": "Unexpected schema content format"}
|
|
160
|
+
all_ids = _collect_all_field_ids(content)
|
|
161
|
+
|
|
162
|
+
section_ids = {s.get("id") for s in content if s.get("category") == "section"}
|
|
163
|
+
|
|
164
|
+
if fields_to_keep:
|
|
165
|
+
fields_to_keep_set = set(fields_to_keep) | section_ids
|
|
166
|
+
ancestor_ids = _collect_ancestor_ids(content, fields_to_keep_set)
|
|
167
|
+
fields_to_keep_set |= ancestor_ids
|
|
168
|
+
remove_set = all_ids - fields_to_keep_set
|
|
169
|
+
else:
|
|
170
|
+
remove_set = set(fields_to_remove) - section_ids # type: ignore[arg-type]
|
|
171
|
+
|
|
172
|
+
if not remove_set:
|
|
173
|
+
return {"removed_fields": [], "remaining_fields": sorted(all_ids)}
|
|
174
|
+
|
|
175
|
+
pruned_content, removed = _remove_fields_from_content(content, remove_set)
|
|
176
|
+
await client._http_client.update(Resource.Schema, schema_id, {"content": pruned_content})
|
|
177
|
+
|
|
178
|
+
remaining_ids = _collect_all_field_ids(pruned_content)
|
|
179
|
+
return {"removed_fields": sorted(removed), "remaining_fields": sorted(remaining_ids)}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
async def delete_schema(client: AsyncRossumAPIClient, schema_id: int) -> dict:
|
|
183
|
+
return await delete_resource("schema", schema_id, client.delete_schema)
|