mcp-eregistrations-bpa 0.8.5__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.
Potentially problematic release.
This version of mcp-eregistrations-bpa might be problematic. Click here for more details.
- mcp_eregistrations_bpa/__init__.py +121 -0
- mcp_eregistrations_bpa/__main__.py +6 -0
- mcp_eregistrations_bpa/arazzo/__init__.py +21 -0
- mcp_eregistrations_bpa/arazzo/expression.py +379 -0
- mcp_eregistrations_bpa/audit/__init__.py +56 -0
- mcp_eregistrations_bpa/audit/context.py +66 -0
- mcp_eregistrations_bpa/audit/logger.py +236 -0
- mcp_eregistrations_bpa/audit/models.py +131 -0
- mcp_eregistrations_bpa/auth/__init__.py +64 -0
- mcp_eregistrations_bpa/auth/callback.py +391 -0
- mcp_eregistrations_bpa/auth/cas.py +409 -0
- mcp_eregistrations_bpa/auth/oidc.py +252 -0
- mcp_eregistrations_bpa/auth/permissions.py +162 -0
- mcp_eregistrations_bpa/auth/token_manager.py +348 -0
- mcp_eregistrations_bpa/bpa_client/__init__.py +84 -0
- mcp_eregistrations_bpa/bpa_client/client.py +740 -0
- mcp_eregistrations_bpa/bpa_client/endpoints.py +193 -0
- mcp_eregistrations_bpa/bpa_client/errors.py +276 -0
- mcp_eregistrations_bpa/bpa_client/models.py +203 -0
- mcp_eregistrations_bpa/config.py +349 -0
- mcp_eregistrations_bpa/db/__init__.py +21 -0
- mcp_eregistrations_bpa/db/connection.py +64 -0
- mcp_eregistrations_bpa/db/migrations.py +168 -0
- mcp_eregistrations_bpa/exceptions.py +39 -0
- mcp_eregistrations_bpa/py.typed +0 -0
- mcp_eregistrations_bpa/rollback/__init__.py +19 -0
- mcp_eregistrations_bpa/rollback/manager.py +616 -0
- mcp_eregistrations_bpa/server.py +152 -0
- mcp_eregistrations_bpa/tools/__init__.py +372 -0
- mcp_eregistrations_bpa/tools/actions.py +155 -0
- mcp_eregistrations_bpa/tools/analysis.py +352 -0
- mcp_eregistrations_bpa/tools/audit.py +399 -0
- mcp_eregistrations_bpa/tools/behaviours.py +1042 -0
- mcp_eregistrations_bpa/tools/bots.py +627 -0
- mcp_eregistrations_bpa/tools/classifications.py +575 -0
- mcp_eregistrations_bpa/tools/costs.py +765 -0
- mcp_eregistrations_bpa/tools/debug_strategies.py +351 -0
- mcp_eregistrations_bpa/tools/debugger.py +1230 -0
- mcp_eregistrations_bpa/tools/determinants.py +2235 -0
- mcp_eregistrations_bpa/tools/document_requirements.py +670 -0
- mcp_eregistrations_bpa/tools/export.py +899 -0
- mcp_eregistrations_bpa/tools/fields.py +162 -0
- mcp_eregistrations_bpa/tools/form_errors.py +36 -0
- mcp_eregistrations_bpa/tools/formio_helpers.py +971 -0
- mcp_eregistrations_bpa/tools/forms.py +1269 -0
- mcp_eregistrations_bpa/tools/jsonlogic_builder.py +466 -0
- mcp_eregistrations_bpa/tools/large_response.py +163 -0
- mcp_eregistrations_bpa/tools/messages.py +523 -0
- mcp_eregistrations_bpa/tools/notifications.py +241 -0
- mcp_eregistrations_bpa/tools/registration_institutions.py +680 -0
- mcp_eregistrations_bpa/tools/registrations.py +897 -0
- mcp_eregistrations_bpa/tools/role_status.py +447 -0
- mcp_eregistrations_bpa/tools/role_units.py +400 -0
- mcp_eregistrations_bpa/tools/roles.py +1236 -0
- mcp_eregistrations_bpa/tools/rollback.py +335 -0
- mcp_eregistrations_bpa/tools/services.py +674 -0
- mcp_eregistrations_bpa/tools/workflows.py +2487 -0
- mcp_eregistrations_bpa/tools/yaml_transformer.py +991 -0
- mcp_eregistrations_bpa/workflows/__init__.py +28 -0
- mcp_eregistrations_bpa/workflows/loader.py +440 -0
- mcp_eregistrations_bpa/workflows/models.py +336 -0
- mcp_eregistrations_bpa-0.8.5.dist-info/METADATA +965 -0
- mcp_eregistrations_bpa-0.8.5.dist-info/RECORD +66 -0
- mcp_eregistrations_bpa-0.8.5.dist-info/WHEEL +4 -0
- mcp_eregistrations_bpa-0.8.5.dist-info/entry_points.txt +2 -0
- mcp_eregistrations_bpa-0.8.5.dist-info/licenses/LICENSE +86 -0
|
@@ -0,0 +1,991 @@
|
|
|
1
|
+
"""YAML transformation engine for BPA service exports.
|
|
2
|
+
|
|
3
|
+
Transforms raw JSON exports from BPA into clean, human-readable YAML
|
|
4
|
+
suitable for AI consumption, version control, and documentation.
|
|
5
|
+
|
|
6
|
+
Key transformations:
|
|
7
|
+
- Parse stringified JSON (formSchema, jsonDeterminants)
|
|
8
|
+
- Convert camelCase to snake_case
|
|
9
|
+
- Extract essential Form.io properties
|
|
10
|
+
- Preserve metadata for re-import
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import re
|
|
17
|
+
from datetime import UTC, datetime
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
import yaml
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"YAMLTransformer",
|
|
24
|
+
"TransformationError",
|
|
25
|
+
"normalize_export_data",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
# Schema version for the YAML output
|
|
29
|
+
SCHEMA_VERSION = "1.0"
|
|
30
|
+
|
|
31
|
+
# Form.io properties to extract (essential for understanding the form)
|
|
32
|
+
ESSENTIAL_FORMIO_PROPS = {
|
|
33
|
+
"key",
|
|
34
|
+
"type",
|
|
35
|
+
"label",
|
|
36
|
+
"validate",
|
|
37
|
+
"data",
|
|
38
|
+
"determinantIds",
|
|
39
|
+
"hidden",
|
|
40
|
+
"disabled",
|
|
41
|
+
"defaultValue",
|
|
42
|
+
"multiple",
|
|
43
|
+
"conditional",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Form.io properties to skip (internal/noise)
|
|
47
|
+
SKIP_FORMIO_PROPS = {
|
|
48
|
+
"tableView",
|
|
49
|
+
"input",
|
|
50
|
+
"version",
|
|
51
|
+
"persistent",
|
|
52
|
+
"customDefaultValue",
|
|
53
|
+
"calculateValue",
|
|
54
|
+
"calculateServer",
|
|
55
|
+
"widget",
|
|
56
|
+
"attributes",
|
|
57
|
+
"overlay",
|
|
58
|
+
"allowCalculateOverride",
|
|
59
|
+
"encrypted",
|
|
60
|
+
"showCharCount",
|
|
61
|
+
"showWordCount",
|
|
62
|
+
"spellcheck",
|
|
63
|
+
"redrawOn",
|
|
64
|
+
"clearOnHide",
|
|
65
|
+
"modalEdit",
|
|
66
|
+
"refreshOn",
|
|
67
|
+
"dataGridLabel",
|
|
68
|
+
"allowMultipleMasks",
|
|
69
|
+
"addons",
|
|
70
|
+
"mask",
|
|
71
|
+
"inputType",
|
|
72
|
+
"inputFormat",
|
|
73
|
+
"inputMask",
|
|
74
|
+
"displayMask",
|
|
75
|
+
"tabindex",
|
|
76
|
+
"autocomplete",
|
|
77
|
+
"dbIndex",
|
|
78
|
+
"customClass",
|
|
79
|
+
"id",
|
|
80
|
+
"placeholder",
|
|
81
|
+
"prefix",
|
|
82
|
+
"suffix",
|
|
83
|
+
"tooltip",
|
|
84
|
+
"description",
|
|
85
|
+
"errorLabel",
|
|
86
|
+
"hideLabel",
|
|
87
|
+
"autofocus",
|
|
88
|
+
"kickbox",
|
|
89
|
+
"minLength",
|
|
90
|
+
"maxLength",
|
|
91
|
+
"delimiter",
|
|
92
|
+
"requireDecimal",
|
|
93
|
+
"case",
|
|
94
|
+
"truncateMultipleSpaces",
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class TransformationError(Exception):
|
|
99
|
+
"""Raised when YAML transformation fails."""
|
|
100
|
+
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def camel_to_snake(name: str) -> str:
|
|
105
|
+
"""Convert camelCase to snake_case.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
name: The camelCase string to convert.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
The snake_case equivalent.
|
|
112
|
+
|
|
113
|
+
Examples:
|
|
114
|
+
>>> camel_to_snake("shortName")
|
|
115
|
+
'short_name'
|
|
116
|
+
>>> camel_to_snake("applicantFormSelected")
|
|
117
|
+
'applicant_form_selected'
|
|
118
|
+
"""
|
|
119
|
+
# Insert underscore before uppercase letters and convert to lowercase
|
|
120
|
+
s1 = re.sub(r"(.)([A-Z][a-z]+)", r"\1_\2", name)
|
|
121
|
+
return re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def transform_keys(obj: Any, skip_keys: set[str] | None = None) -> Any:
|
|
125
|
+
"""Recursively transform dictionary keys from camelCase to snake_case.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
obj: The object to transform (dict, list, or primitive).
|
|
129
|
+
skip_keys: Set of keys to skip transformation for.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
The transformed object with snake_case keys.
|
|
133
|
+
"""
|
|
134
|
+
skip_keys = skip_keys or set()
|
|
135
|
+
|
|
136
|
+
if isinstance(obj, dict):
|
|
137
|
+
return {
|
|
138
|
+
(camel_to_snake(k) if k not in skip_keys else k): transform_keys(
|
|
139
|
+
v, skip_keys
|
|
140
|
+
)
|
|
141
|
+
for k, v in obj.items()
|
|
142
|
+
}
|
|
143
|
+
elif isinstance(obj, list):
|
|
144
|
+
return [transform_keys(item, skip_keys) for item in obj]
|
|
145
|
+
else:
|
|
146
|
+
return obj
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def normalize_export_data(data: dict[str, Any]) -> dict[str, Any]:
|
|
150
|
+
"""Normalize BPA export data to a consistent flat structure.
|
|
151
|
+
|
|
152
|
+
The live BPA API returns data with different structure than test mocks:
|
|
153
|
+
- Live API: {"appVersion": ..., "service": {"name": ..., ...}, "catalogs": [...]}
|
|
154
|
+
- Test mocks: {"name": ..., "registrations": [...], ...}
|
|
155
|
+
|
|
156
|
+
This function unwraps nested structures and remaps field names.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
data: Raw export data from BPA API or test mock.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Normalized data with consistent flat structure.
|
|
163
|
+
"""
|
|
164
|
+
# If data is already flat (has "name" at root), return as-is
|
|
165
|
+
if "name" in data and "service" not in data:
|
|
166
|
+
return data
|
|
167
|
+
|
|
168
|
+
# If data has nested "service" key, extract and merge
|
|
169
|
+
if "service" in data and isinstance(data["service"], dict):
|
|
170
|
+
service_data = data["service"].copy()
|
|
171
|
+
|
|
172
|
+
# Remap live API field names to expected names
|
|
173
|
+
field_mappings = {
|
|
174
|
+
"serviceDeterminants": "determinants",
|
|
175
|
+
"applicantFormPage": "applicantForm",
|
|
176
|
+
"guideFormPage": "guideForm",
|
|
177
|
+
"sendFileFormPage": "sendFileForm",
|
|
178
|
+
"paymentFormPage": "paymentForm",
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
for old_key, new_key in field_mappings.items():
|
|
182
|
+
if old_key in service_data:
|
|
183
|
+
service_data[new_key] = service_data.pop(old_key)
|
|
184
|
+
|
|
185
|
+
# Parse stringified formSchema in forms if needed
|
|
186
|
+
for form_key in ["applicantForm", "guideForm", "sendFileForm", "paymentForm"]:
|
|
187
|
+
form = service_data.get(form_key)
|
|
188
|
+
if form and isinstance(form, dict):
|
|
189
|
+
form_schema = form.get("formSchema")
|
|
190
|
+
if isinstance(form_schema, str):
|
|
191
|
+
try:
|
|
192
|
+
form["formSchema"] = json.loads(form_schema)
|
|
193
|
+
except json.JSONDecodeError:
|
|
194
|
+
pass # Keep as string if parsing fails
|
|
195
|
+
|
|
196
|
+
# Merge catalogs from root level if present
|
|
197
|
+
if "catalogs" in data:
|
|
198
|
+
service_data["catalogs"] = data["catalogs"]
|
|
199
|
+
|
|
200
|
+
# Preserve app version at root level
|
|
201
|
+
if "appVersion" in data:
|
|
202
|
+
service_data["appVersion"] = data["appVersion"]
|
|
203
|
+
|
|
204
|
+
return service_data
|
|
205
|
+
|
|
206
|
+
return data
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class YAMLTransformer:
|
|
210
|
+
"""Transforms BPA JSON exports to clean YAML format."""
|
|
211
|
+
|
|
212
|
+
def __init__(self, include_metadata: bool = True) -> None:
|
|
213
|
+
"""Initialize the transformer.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
include_metadata: Whether to include _metadata section with IDs
|
|
217
|
+
for re-import capability. Default True.
|
|
218
|
+
"""
|
|
219
|
+
self._include_metadata = include_metadata
|
|
220
|
+
self._metadata: dict[str, Any] = {}
|
|
221
|
+
|
|
222
|
+
def transform_to_dict(
|
|
223
|
+
self, export_data: dict[str, Any]
|
|
224
|
+
) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
225
|
+
"""Transform raw JSON export to optimized dictionary structure.
|
|
226
|
+
|
|
227
|
+
This method builds the clean, optimized data structure without
|
|
228
|
+
serializing it. Use this when you need the data as a Python dict
|
|
229
|
+
(e.g., for JSON serialization).
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
export_data: Raw JSON export from service_export_raw.
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
Tuple of (data_dict, summary).
|
|
236
|
+
|
|
237
|
+
Raises:
|
|
238
|
+
TransformationError: If transformation fails.
|
|
239
|
+
"""
|
|
240
|
+
try:
|
|
241
|
+
# Build the optimized structure
|
|
242
|
+
data_dict = self._build_yaml_structure(export_data)
|
|
243
|
+
|
|
244
|
+
# Generate summary (extract from data since it's now in the structure)
|
|
245
|
+
summary = data_dict.get("summary", {}).copy()
|
|
246
|
+
if not summary:
|
|
247
|
+
summary = self._generate_summary(data_dict)
|
|
248
|
+
|
|
249
|
+
return data_dict, summary
|
|
250
|
+
|
|
251
|
+
except Exception as e:
|
|
252
|
+
raise TransformationError(f"Failed to transform export: {e}") from e
|
|
253
|
+
|
|
254
|
+
def transform(self, export_data: dict[str, Any]) -> tuple[str, dict[str, Any]]:
|
|
255
|
+
"""Transform raw JSON export to YAML.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
export_data: Raw JSON export from service_export_raw.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
Tuple of (yaml_content, summary).
|
|
262
|
+
|
|
263
|
+
Raises:
|
|
264
|
+
TransformationError: If transformation fails.
|
|
265
|
+
"""
|
|
266
|
+
try:
|
|
267
|
+
# Build the optimized structure
|
|
268
|
+
yaml_data, summary = self.transform_to_dict(export_data)
|
|
269
|
+
|
|
270
|
+
# Convert to YAML string
|
|
271
|
+
yaml_content = yaml.dump(
|
|
272
|
+
yaml_data,
|
|
273
|
+
default_flow_style=False,
|
|
274
|
+
allow_unicode=True,
|
|
275
|
+
sort_keys=False,
|
|
276
|
+
width=120,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
return yaml_content, summary
|
|
280
|
+
|
|
281
|
+
except Exception as e:
|
|
282
|
+
raise TransformationError(f"Failed to transform export: {e}") from e
|
|
283
|
+
|
|
284
|
+
def _build_yaml_structure(self, data: dict[str, Any]) -> dict[str, Any]:
|
|
285
|
+
"""Build the complete YAML structure.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
data: Raw export data.
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
Structured dictionary for YAML output.
|
|
292
|
+
"""
|
|
293
|
+
# Normalize export data to handle both live API and test mock structures
|
|
294
|
+
data = normalize_export_data(data)
|
|
295
|
+
|
|
296
|
+
# Reset metadata for this transformation
|
|
297
|
+
self._metadata = {
|
|
298
|
+
"service_id": data.get("id"),
|
|
299
|
+
"service_old_id": data.get("oldId"),
|
|
300
|
+
"registration_ids": {},
|
|
301
|
+
"determinant_ids": {},
|
|
302
|
+
"role_ids": {},
|
|
303
|
+
"bot_ids": {},
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
result: dict[str, Any] = {
|
|
307
|
+
"version": SCHEMA_VERSION,
|
|
308
|
+
"exported_at": datetime.now(UTC).isoformat(),
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
# Add BPA version if available
|
|
312
|
+
if "bpaVersion" in data:
|
|
313
|
+
result["bpa_version"] = data["bpaVersion"]
|
|
314
|
+
|
|
315
|
+
# Build all sections first to calculate summary
|
|
316
|
+
service_section = self._build_service_section(data)
|
|
317
|
+
registrations = data.get("registrations", [])
|
|
318
|
+
registrations_section = (
|
|
319
|
+
self._build_registrations_section(registrations) if registrations else []
|
|
320
|
+
)
|
|
321
|
+
determinants = data.get("determinants", [])
|
|
322
|
+
determinants_section = (
|
|
323
|
+
self._build_determinants_section(determinants) if determinants else {}
|
|
324
|
+
)
|
|
325
|
+
forms_section = self._build_forms_section(data)
|
|
326
|
+
roles = data.get("roles", [])
|
|
327
|
+
workflow_section = self._build_workflow_section(roles) if roles else {}
|
|
328
|
+
bots = data.get("bots", [])
|
|
329
|
+
bots_section = self._build_bots_section(bots) if bots else {}
|
|
330
|
+
print_docs = data.get("printDocuments", [])
|
|
331
|
+
print_docs_section = (
|
|
332
|
+
self._build_print_documents_section(print_docs) if print_docs else []
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
# Add summary section at the top (after version/exported_at)
|
|
336
|
+
result["summary"] = self._build_summary_section(
|
|
337
|
+
service_section,
|
|
338
|
+
registrations_section,
|
|
339
|
+
determinants_section,
|
|
340
|
+
forms_section,
|
|
341
|
+
workflow_section,
|
|
342
|
+
bots_section,
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
# Build service section
|
|
346
|
+
result["service"] = service_section
|
|
347
|
+
|
|
348
|
+
# Add pre-built sections (only if they have content)
|
|
349
|
+
if registrations_section:
|
|
350
|
+
result["registrations"] = registrations_section
|
|
351
|
+
|
|
352
|
+
if determinants_section:
|
|
353
|
+
result["determinants"] = determinants_section
|
|
354
|
+
|
|
355
|
+
if forms_section:
|
|
356
|
+
result["forms"] = forms_section
|
|
357
|
+
|
|
358
|
+
if workflow_section and workflow_section.get("roles"):
|
|
359
|
+
result["workflow"] = workflow_section
|
|
360
|
+
|
|
361
|
+
if bots_section:
|
|
362
|
+
result["bots"] = bots_section
|
|
363
|
+
|
|
364
|
+
if print_docs_section:
|
|
365
|
+
result["print_documents"] = print_docs_section
|
|
366
|
+
|
|
367
|
+
# Add metadata section if requested
|
|
368
|
+
if self._include_metadata:
|
|
369
|
+
result["_metadata"] = self._metadata
|
|
370
|
+
|
|
371
|
+
return result
|
|
372
|
+
|
|
373
|
+
def _build_summary_section(
|
|
374
|
+
self,
|
|
375
|
+
service_section: dict[str, Any],
|
|
376
|
+
registrations_section: list[dict[str, Any]],
|
|
377
|
+
determinants_section: dict[str, Any],
|
|
378
|
+
forms_section: dict[str, Any],
|
|
379
|
+
workflow_section: dict[str, Any],
|
|
380
|
+
bots_section: dict[str, Any],
|
|
381
|
+
) -> dict[str, Any]:
|
|
382
|
+
"""Build the summary section for quick scanning.
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
service_section: Built service section.
|
|
386
|
+
registrations_section: Built registrations list.
|
|
387
|
+
determinants_section: Built determinants dictionary.
|
|
388
|
+
forms_section: Built forms dictionary.
|
|
389
|
+
workflow_section: Built workflow dictionary.
|
|
390
|
+
bots_section: Built bots dictionary.
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
Summary dictionary with counts.
|
|
394
|
+
"""
|
|
395
|
+
summary: dict[str, Any] = {
|
|
396
|
+
"service_name": service_section.get("name", "Unknown"),
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
# Count registrations
|
|
400
|
+
summary["registration_count"] = len(registrations_section)
|
|
401
|
+
|
|
402
|
+
# Count determinants
|
|
403
|
+
det_count = 0
|
|
404
|
+
for category in determinants_section.values():
|
|
405
|
+
if isinstance(category, list):
|
|
406
|
+
det_count += len(category)
|
|
407
|
+
summary["determinant_count"] = det_count
|
|
408
|
+
|
|
409
|
+
# Count form fields
|
|
410
|
+
field_count = 0
|
|
411
|
+
for form in forms_section.values():
|
|
412
|
+
if isinstance(form, dict):
|
|
413
|
+
components = form.get("components", [])
|
|
414
|
+
field_count += self._count_components(components)
|
|
415
|
+
summary["field_count"] = field_count
|
|
416
|
+
|
|
417
|
+
# Count roles
|
|
418
|
+
roles = workflow_section.get("roles", [])
|
|
419
|
+
summary["role_count"] = len(roles)
|
|
420
|
+
|
|
421
|
+
# Count bots
|
|
422
|
+
bot_count = 0
|
|
423
|
+
for category in bots_section.values():
|
|
424
|
+
if isinstance(category, list):
|
|
425
|
+
bot_count += len(category)
|
|
426
|
+
summary["bot_count"] = bot_count
|
|
427
|
+
|
|
428
|
+
return summary
|
|
429
|
+
|
|
430
|
+
def _build_service_section(self, data: dict[str, Any]) -> dict[str, Any]:
|
|
431
|
+
"""Build the service section.
|
|
432
|
+
|
|
433
|
+
Args:
|
|
434
|
+
data: Raw export data.
|
|
435
|
+
|
|
436
|
+
Returns:
|
|
437
|
+
Service section dictionary.
|
|
438
|
+
"""
|
|
439
|
+
service: dict[str, Any] = {
|
|
440
|
+
"name": data.get("name", "Unknown"),
|
|
441
|
+
"short_name": data.get("shortName", ""),
|
|
442
|
+
"active": data.get("active", True),
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
# Extract service properties from serviceJsonProperties
|
|
446
|
+
json_props = data.get("serviceJsonProperties")
|
|
447
|
+
if json_props:
|
|
448
|
+
if isinstance(json_props, str):
|
|
449
|
+
try:
|
|
450
|
+
json_props = json.loads(json_props)
|
|
451
|
+
except json.JSONDecodeError:
|
|
452
|
+
json_props = {}
|
|
453
|
+
|
|
454
|
+
if isinstance(json_props, dict):
|
|
455
|
+
service["properties"] = transform_keys(json_props)
|
|
456
|
+
|
|
457
|
+
return service
|
|
458
|
+
|
|
459
|
+
def _build_registrations_section(
|
|
460
|
+
self, registrations: list[dict[str, Any]]
|
|
461
|
+
) -> list[dict[str, Any]]:
|
|
462
|
+
"""Build the registrations section.
|
|
463
|
+
|
|
464
|
+
Args:
|
|
465
|
+
registrations: List of registration objects.
|
|
466
|
+
|
|
467
|
+
Returns:
|
|
468
|
+
Transformed registrations list.
|
|
469
|
+
"""
|
|
470
|
+
result = []
|
|
471
|
+
|
|
472
|
+
for reg in registrations:
|
|
473
|
+
name = reg.get("name", "Unknown")
|
|
474
|
+
|
|
475
|
+
# Store ID mapping
|
|
476
|
+
if reg.get("id"):
|
|
477
|
+
self._metadata["registration_ids"][name] = reg["id"]
|
|
478
|
+
|
|
479
|
+
transformed = {
|
|
480
|
+
"name": name,
|
|
481
|
+
"short_name": reg.get("shortName", ""),
|
|
482
|
+
"active": reg.get("active", True),
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
# Add costs if present
|
|
486
|
+
costs = reg.get("costs", [])
|
|
487
|
+
if costs:
|
|
488
|
+
transformed["costs"] = self._transform_costs(costs)
|
|
489
|
+
|
|
490
|
+
# Add document requirements if present
|
|
491
|
+
doc_reqs = reg.get("documentRequirements", [])
|
|
492
|
+
if doc_reqs:
|
|
493
|
+
transformed["document_requirements"] = [
|
|
494
|
+
{"name": dr.get("name", ""), "required": dr.get("required", True)}
|
|
495
|
+
for dr in doc_reqs
|
|
496
|
+
]
|
|
497
|
+
|
|
498
|
+
# Add document results if present
|
|
499
|
+
doc_results = reg.get("documentResults", [])
|
|
500
|
+
if doc_results:
|
|
501
|
+
transformed["document_results"] = [
|
|
502
|
+
{
|
|
503
|
+
"name": dr.get("name", ""),
|
|
504
|
+
"is_digital": dr.get("isDigital", False),
|
|
505
|
+
}
|
|
506
|
+
for dr in doc_results
|
|
507
|
+
]
|
|
508
|
+
|
|
509
|
+
result.append(transformed)
|
|
510
|
+
|
|
511
|
+
return result
|
|
512
|
+
|
|
513
|
+
def _transform_costs(self, costs: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
514
|
+
"""Transform cost objects.
|
|
515
|
+
|
|
516
|
+
Args:
|
|
517
|
+
costs: List of cost objects.
|
|
518
|
+
|
|
519
|
+
Returns:
|
|
520
|
+
Transformed costs list.
|
|
521
|
+
"""
|
|
522
|
+
result = []
|
|
523
|
+
|
|
524
|
+
for cost in costs:
|
|
525
|
+
transformed: dict[str, Any] = {
|
|
526
|
+
"name": cost.get("name", ""),
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
# Determine cost type
|
|
530
|
+
cost_type = cost.get("costType", "").lower()
|
|
531
|
+
if cost_type == "fix":
|
|
532
|
+
transformed["type"] = "fixed"
|
|
533
|
+
transformed["amount"] = cost.get("fixValue", 0)
|
|
534
|
+
elif cost_type == "formula":
|
|
535
|
+
transformed["type"] = "formula"
|
|
536
|
+
transformed["formula"] = cost.get("formula", "")
|
|
537
|
+
else:
|
|
538
|
+
transformed["type"] = cost_type or "unknown"
|
|
539
|
+
|
|
540
|
+
# Add currency if present
|
|
541
|
+
currency = cost.get("currency") or cost.get("currencyId")
|
|
542
|
+
if currency:
|
|
543
|
+
transformed["currency"] = currency
|
|
544
|
+
|
|
545
|
+
result.append(transformed)
|
|
546
|
+
|
|
547
|
+
return result
|
|
548
|
+
|
|
549
|
+
def _build_determinants_section(
|
|
550
|
+
self, determinants: list[dict[str, Any]]
|
|
551
|
+
) -> dict[str, Any]:
|
|
552
|
+
"""Build the determinants section.
|
|
553
|
+
|
|
554
|
+
Args:
|
|
555
|
+
determinants: List of determinant objects.
|
|
556
|
+
|
|
557
|
+
Returns:
|
|
558
|
+
Categorized determinants dictionary.
|
|
559
|
+
"""
|
|
560
|
+
form_field_based = []
|
|
561
|
+
registration_based = []
|
|
562
|
+
other = []
|
|
563
|
+
|
|
564
|
+
for det in determinants:
|
|
565
|
+
name = det.get("name", "Unknown")
|
|
566
|
+
|
|
567
|
+
# Store ID mapping
|
|
568
|
+
if det.get("id"):
|
|
569
|
+
self._metadata["determinant_ids"][name] = det["id"]
|
|
570
|
+
|
|
571
|
+
det_type = det.get("type", "").lower()
|
|
572
|
+
|
|
573
|
+
transformed: dict[str, Any] = {
|
|
574
|
+
"name": name,
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if det_type in ("textdeterminant", "text"):
|
|
578
|
+
transformed["field"] = det.get("targetFormFieldKey", "")
|
|
579
|
+
transformed["operator"] = det.get("operator", "equals")
|
|
580
|
+
# Parse condition value from jsonCondition if available
|
|
581
|
+
json_cond = det.get("jsonCondition")
|
|
582
|
+
if json_cond:
|
|
583
|
+
transformed["value"] = self._parse_condition_value(json_cond)
|
|
584
|
+
form_field_based.append(transformed)
|
|
585
|
+
|
|
586
|
+
elif det_type in ("selectdeterminant", "select"):
|
|
587
|
+
transformed["field"] = det.get("targetFormFieldKey", "")
|
|
588
|
+
transformed["operator"] = det.get("operator", "equals")
|
|
589
|
+
transformed["value"] = det.get("selectValue", "")
|
|
590
|
+
form_field_based.append(transformed)
|
|
591
|
+
|
|
592
|
+
elif det_type in ("registrationdeterminant", "registration"):
|
|
593
|
+
transformed["registration"] = det.get("registrationName", "")
|
|
594
|
+
transformed["selected"] = det.get("selected", True)
|
|
595
|
+
registration_based.append(transformed)
|
|
596
|
+
|
|
597
|
+
else:
|
|
598
|
+
transformed["type"] = det_type
|
|
599
|
+
other.append(transformed)
|
|
600
|
+
|
|
601
|
+
result: dict[str, Any] = {}
|
|
602
|
+
|
|
603
|
+
if form_field_based:
|
|
604
|
+
result["form_field_based"] = form_field_based
|
|
605
|
+
|
|
606
|
+
if registration_based:
|
|
607
|
+
result["registration_based"] = registration_based
|
|
608
|
+
|
|
609
|
+
if other:
|
|
610
|
+
result["other"] = other
|
|
611
|
+
|
|
612
|
+
return result
|
|
613
|
+
|
|
614
|
+
def _parse_condition_value(self, json_condition: str | dict[str, Any]) -> Any:
|
|
615
|
+
"""Parse condition value from jsonCondition.
|
|
616
|
+
|
|
617
|
+
Args:
|
|
618
|
+
json_condition: JSON condition string or dict.
|
|
619
|
+
|
|
620
|
+
Returns:
|
|
621
|
+
Extracted condition value.
|
|
622
|
+
"""
|
|
623
|
+
if isinstance(json_condition, str):
|
|
624
|
+
try:
|
|
625
|
+
json_condition = json.loads(json_condition)
|
|
626
|
+
except json.JSONDecodeError:
|
|
627
|
+
return json_condition
|
|
628
|
+
|
|
629
|
+
if isinstance(json_condition, dict):
|
|
630
|
+
# Extract value from common patterns
|
|
631
|
+
return json_condition.get("value", json_condition)
|
|
632
|
+
|
|
633
|
+
return json_condition
|
|
634
|
+
|
|
635
|
+
def _build_forms_section(self, data: dict[str, Any]) -> dict[str, Any]:
|
|
636
|
+
"""Build the forms section.
|
|
637
|
+
|
|
638
|
+
Args:
|
|
639
|
+
data: Raw export data.
|
|
640
|
+
|
|
641
|
+
Returns:
|
|
642
|
+
Forms section dictionary.
|
|
643
|
+
"""
|
|
644
|
+
forms: dict[str, Any] = {}
|
|
645
|
+
|
|
646
|
+
form_keys = [
|
|
647
|
+
("guideForm", "guide"),
|
|
648
|
+
("applicantForm", "applicant"),
|
|
649
|
+
("sendFileForm", "send_file"),
|
|
650
|
+
("paymentForm", "payment"),
|
|
651
|
+
]
|
|
652
|
+
|
|
653
|
+
for json_key, yaml_key in form_keys:
|
|
654
|
+
form_data = data.get(json_key)
|
|
655
|
+
if form_data:
|
|
656
|
+
parsed = self._parse_form(form_data)
|
|
657
|
+
if parsed:
|
|
658
|
+
forms[yaml_key] = parsed
|
|
659
|
+
|
|
660
|
+
return forms
|
|
661
|
+
|
|
662
|
+
def _parse_form(self, form_data: dict[str, Any]) -> dict[str, Any] | None:
|
|
663
|
+
"""Parse a form object.
|
|
664
|
+
|
|
665
|
+
Args:
|
|
666
|
+
form_data: Form data from export.
|
|
667
|
+
|
|
668
|
+
Returns:
|
|
669
|
+
Parsed form dictionary or None.
|
|
670
|
+
"""
|
|
671
|
+
if not form_data:
|
|
672
|
+
return None
|
|
673
|
+
|
|
674
|
+
result: dict[str, Any] = {
|
|
675
|
+
"active": form_data.get("active", True),
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
# Parse form schema
|
|
679
|
+
form_schema = form_data.get("formSchema")
|
|
680
|
+
if form_schema:
|
|
681
|
+
components = self._parse_form_schema(form_schema)
|
|
682
|
+
if components:
|
|
683
|
+
result["components"] = components
|
|
684
|
+
|
|
685
|
+
return result
|
|
686
|
+
|
|
687
|
+
def _parse_form_schema(
|
|
688
|
+
self, schema: str | dict[str, Any]
|
|
689
|
+
) -> list[dict[str, Any]] | None:
|
|
690
|
+
"""Parse Form.io schema (may be stringified JSON).
|
|
691
|
+
|
|
692
|
+
Args:
|
|
693
|
+
schema: Form schema (string or dict).
|
|
694
|
+
|
|
695
|
+
Returns:
|
|
696
|
+
List of parsed components.
|
|
697
|
+
"""
|
|
698
|
+
if isinstance(schema, str):
|
|
699
|
+
try:
|
|
700
|
+
schema = json.loads(schema)
|
|
701
|
+
except json.JSONDecodeError:
|
|
702
|
+
return None
|
|
703
|
+
|
|
704
|
+
if not isinstance(schema, dict):
|
|
705
|
+
return None
|
|
706
|
+
|
|
707
|
+
components = schema.get("components", [])
|
|
708
|
+
if not components:
|
|
709
|
+
return None
|
|
710
|
+
|
|
711
|
+
return self._extract_components(components)
|
|
712
|
+
|
|
713
|
+
def _extract_components(
|
|
714
|
+
self, components: list[dict[str, Any]]
|
|
715
|
+
) -> list[dict[str, Any]]:
|
|
716
|
+
"""Extract essential properties from Form.io components.
|
|
717
|
+
|
|
718
|
+
Args:
|
|
719
|
+
components: List of Form.io components.
|
|
720
|
+
|
|
721
|
+
Returns:
|
|
722
|
+
Simplified component list.
|
|
723
|
+
"""
|
|
724
|
+
result = []
|
|
725
|
+
|
|
726
|
+
for comp in components:
|
|
727
|
+
if not isinstance(comp, dict):
|
|
728
|
+
continue
|
|
729
|
+
|
|
730
|
+
# Skip components without a key
|
|
731
|
+
key = comp.get("key")
|
|
732
|
+
if not key:
|
|
733
|
+
continue
|
|
734
|
+
|
|
735
|
+
extracted: dict[str, Any] = {
|
|
736
|
+
"key": key,
|
|
737
|
+
"type": comp.get("type", "unknown"),
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
# Add label if present
|
|
741
|
+
label = comp.get("label")
|
|
742
|
+
if label:
|
|
743
|
+
extracted["label"] = label
|
|
744
|
+
|
|
745
|
+
# Add required if true
|
|
746
|
+
validate = comp.get("validate", {})
|
|
747
|
+
if isinstance(validate, dict) and validate.get("required"):
|
|
748
|
+
extracted["required"] = True
|
|
749
|
+
|
|
750
|
+
# Add data source info for selects
|
|
751
|
+
data = comp.get("data", {})
|
|
752
|
+
if isinstance(data, dict):
|
|
753
|
+
if data.get("dataSrc"):
|
|
754
|
+
extracted["data_source"] = data["dataSrc"]
|
|
755
|
+
if data.get("catalog"):
|
|
756
|
+
extracted["catalog"] = data["catalog"]
|
|
757
|
+
|
|
758
|
+
# Add determinant IDs if present
|
|
759
|
+
det_ids = comp.get("determinantIds")
|
|
760
|
+
if det_ids:
|
|
761
|
+
extracted["determinant_ids"] = det_ids
|
|
762
|
+
|
|
763
|
+
# Add hidden/disabled if true
|
|
764
|
+
if comp.get("hidden"):
|
|
765
|
+
extracted["hidden"] = True
|
|
766
|
+
if comp.get("disabled"):
|
|
767
|
+
extracted["disabled"] = True
|
|
768
|
+
|
|
769
|
+
# Handle nested components (panels, columns, etc.)
|
|
770
|
+
nested = comp.get("components", [])
|
|
771
|
+
if nested:
|
|
772
|
+
nested_extracted = self._extract_components(nested)
|
|
773
|
+
if nested_extracted:
|
|
774
|
+
extracted["components"] = nested_extracted
|
|
775
|
+
|
|
776
|
+
# Handle columns
|
|
777
|
+
columns = comp.get("columns", [])
|
|
778
|
+
if columns:
|
|
779
|
+
col_components = []
|
|
780
|
+
for col in columns:
|
|
781
|
+
if isinstance(col, dict):
|
|
782
|
+
col_nested = col.get("components", [])
|
|
783
|
+
if col_nested:
|
|
784
|
+
col_components.extend(self._extract_components(col_nested))
|
|
785
|
+
if col_components:
|
|
786
|
+
extracted["components"] = col_components
|
|
787
|
+
|
|
788
|
+
# Handle tabs
|
|
789
|
+
tabs = comp.get("tabs", [])
|
|
790
|
+
if tabs:
|
|
791
|
+
extracted["tabs"] = [
|
|
792
|
+
{"label": t.get("label", ""), "key": t.get("key", "")}
|
|
793
|
+
for t in tabs
|
|
794
|
+
if isinstance(t, dict)
|
|
795
|
+
]
|
|
796
|
+
|
|
797
|
+
result.append(extracted)
|
|
798
|
+
|
|
799
|
+
return result
|
|
800
|
+
|
|
801
|
+
def _build_workflow_section(self, roles: list[dict[str, Any]]) -> dict[str, Any]:
|
|
802
|
+
"""Build the workflow section from roles.
|
|
803
|
+
|
|
804
|
+
Args:
|
|
805
|
+
roles: List of role objects.
|
|
806
|
+
|
|
807
|
+
Returns:
|
|
808
|
+
Workflow section dictionary.
|
|
809
|
+
"""
|
|
810
|
+
result: dict[str, Any] = {"roles": []}
|
|
811
|
+
|
|
812
|
+
for role in roles:
|
|
813
|
+
name = role.get("name", "Unknown")
|
|
814
|
+
|
|
815
|
+
# Store ID mapping
|
|
816
|
+
if role.get("id"):
|
|
817
|
+
self._metadata["role_ids"][name] = role["id"]
|
|
818
|
+
|
|
819
|
+
transformed: dict[str, Any] = {
|
|
820
|
+
"name": name,
|
|
821
|
+
"short_name": role.get("shortName", ""),
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
# Add role type
|
|
825
|
+
role_type = role.get("type", "")
|
|
826
|
+
if role_type:
|
|
827
|
+
transformed["type"] = role_type
|
|
828
|
+
|
|
829
|
+
# Add start role flag
|
|
830
|
+
if role.get("startRole"):
|
|
831
|
+
transformed["start_role"] = True
|
|
832
|
+
|
|
833
|
+
# Add visible to applicant
|
|
834
|
+
if role.get("visibleToApplicant"):
|
|
835
|
+
transformed["visible_to_applicant"] = True
|
|
836
|
+
|
|
837
|
+
# Add statuses/destinations if present
|
|
838
|
+
statuses = role.get("statuses", [])
|
|
839
|
+
if statuses:
|
|
840
|
+
transformed["statuses"] = [
|
|
841
|
+
{
|
|
842
|
+
"name": s.get("name", ""),
|
|
843
|
+
"type": s.get("type", "status"),
|
|
844
|
+
}
|
|
845
|
+
for s in statuses
|
|
846
|
+
if isinstance(s, dict)
|
|
847
|
+
]
|
|
848
|
+
|
|
849
|
+
result["roles"].append(transformed)
|
|
850
|
+
|
|
851
|
+
return result
|
|
852
|
+
|
|
853
|
+
def _build_bots_section(self, bots: list[dict[str, Any]]) -> dict[str, Any]:
|
|
854
|
+
"""Build the bots section.
|
|
855
|
+
|
|
856
|
+
Args:
|
|
857
|
+
bots: List of bot objects.
|
|
858
|
+
|
|
859
|
+
Returns:
|
|
860
|
+
Categorized bots dictionary.
|
|
861
|
+
"""
|
|
862
|
+
documents = []
|
|
863
|
+
data_bots = []
|
|
864
|
+
other = []
|
|
865
|
+
|
|
866
|
+
for bot in bots:
|
|
867
|
+
name = bot.get("name", "Unknown")
|
|
868
|
+
|
|
869
|
+
# Store ID mapping
|
|
870
|
+
if bot.get("id"):
|
|
871
|
+
self._metadata["bot_ids"][name] = bot["id"]
|
|
872
|
+
|
|
873
|
+
transformed: dict[str, Any] = {
|
|
874
|
+
"name": name,
|
|
875
|
+
"short_name": bot.get("shortName", ""),
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
# Add enabled status
|
|
879
|
+
if not bot.get("enabled", True):
|
|
880
|
+
transformed["enabled"] = False
|
|
881
|
+
|
|
882
|
+
bot_type = bot.get("botType", "").lower()
|
|
883
|
+
|
|
884
|
+
if "document" in bot_type or "upload" in bot_type:
|
|
885
|
+
transformed["category"] = "document"
|
|
886
|
+
documents.append(transformed)
|
|
887
|
+
elif "data" in bot_type or "fetch" in bot_type:
|
|
888
|
+
transformed["category"] = "data"
|
|
889
|
+
data_bots.append(transformed)
|
|
890
|
+
else:
|
|
891
|
+
transformed["category"] = bot_type or "other"
|
|
892
|
+
other.append(transformed)
|
|
893
|
+
|
|
894
|
+
result: dict[str, Any] = {}
|
|
895
|
+
|
|
896
|
+
if documents:
|
|
897
|
+
result["documents"] = documents
|
|
898
|
+
if data_bots:
|
|
899
|
+
result["data"] = data_bots
|
|
900
|
+
if other:
|
|
901
|
+
result["other"] = other
|
|
902
|
+
|
|
903
|
+
return result
|
|
904
|
+
|
|
905
|
+
def _build_print_documents_section(
|
|
906
|
+
self, print_docs: list[dict[str, Any]]
|
|
907
|
+
) -> list[dict[str, Any]]:
|
|
908
|
+
"""Build the print documents section.
|
|
909
|
+
|
|
910
|
+
Args:
|
|
911
|
+
print_docs: List of print document objects.
|
|
912
|
+
|
|
913
|
+
Returns:
|
|
914
|
+
Transformed print documents list.
|
|
915
|
+
"""
|
|
916
|
+
return [
|
|
917
|
+
{
|
|
918
|
+
"name": doc.get("name", ""),
|
|
919
|
+
"short_name": doc.get("shortName", ""),
|
|
920
|
+
"active": doc.get("active", True),
|
|
921
|
+
}
|
|
922
|
+
for doc in print_docs
|
|
923
|
+
]
|
|
924
|
+
|
|
925
|
+
def _generate_summary(self, yaml_data: dict[str, Any]) -> dict[str, Any]:
|
|
926
|
+
"""Generate summary statistics for the transformation.
|
|
927
|
+
|
|
928
|
+
Args:
|
|
929
|
+
yaml_data: The transformed YAML data.
|
|
930
|
+
|
|
931
|
+
Returns:
|
|
932
|
+
Summary dictionary.
|
|
933
|
+
"""
|
|
934
|
+
summary: dict[str, Any] = {
|
|
935
|
+
"service_name": yaml_data.get("service", {}).get("name", "Unknown"),
|
|
936
|
+
"version": yaml_data.get("version", SCHEMA_VERSION),
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
# Count registrations
|
|
940
|
+
registrations = yaml_data.get("registrations", [])
|
|
941
|
+
summary["registration_count"] = len(registrations)
|
|
942
|
+
|
|
943
|
+
# Count determinants
|
|
944
|
+
determinants = yaml_data.get("determinants", {})
|
|
945
|
+
det_count = 0
|
|
946
|
+
for category in determinants.values():
|
|
947
|
+
if isinstance(category, list):
|
|
948
|
+
det_count += len(category)
|
|
949
|
+
summary["determinant_count"] = det_count
|
|
950
|
+
|
|
951
|
+
# Count form fields
|
|
952
|
+
forms = yaml_data.get("forms", {})
|
|
953
|
+
field_count = 0
|
|
954
|
+
for form in forms.values():
|
|
955
|
+
if isinstance(form, dict):
|
|
956
|
+
components = form.get("components", [])
|
|
957
|
+
field_count += self._count_components(components)
|
|
958
|
+
summary["field_count"] = field_count
|
|
959
|
+
|
|
960
|
+
# Count roles
|
|
961
|
+
workflow = yaml_data.get("workflow", {})
|
|
962
|
+
roles = workflow.get("roles", [])
|
|
963
|
+
summary["role_count"] = len(roles)
|
|
964
|
+
|
|
965
|
+
# Count bots
|
|
966
|
+
bots = yaml_data.get("bots", {})
|
|
967
|
+
bot_count = 0
|
|
968
|
+
for category in bots.values():
|
|
969
|
+
if isinstance(category, list):
|
|
970
|
+
bot_count += len(category)
|
|
971
|
+
summary["bot_count"] = bot_count
|
|
972
|
+
|
|
973
|
+
return summary
|
|
974
|
+
|
|
975
|
+
def _count_components(self, components: list[dict[str, Any]]) -> int:
|
|
976
|
+
"""Recursively count form components.
|
|
977
|
+
|
|
978
|
+
Args:
|
|
979
|
+
components: List of components.
|
|
980
|
+
|
|
981
|
+
Returns:
|
|
982
|
+
Total component count.
|
|
983
|
+
"""
|
|
984
|
+
count = 0
|
|
985
|
+
for comp in components:
|
|
986
|
+
if isinstance(comp, dict):
|
|
987
|
+
count += 1
|
|
988
|
+
nested = comp.get("components", [])
|
|
989
|
+
if nested:
|
|
990
|
+
count += self._count_components(nested)
|
|
991
|
+
return count
|