kodexa 7.5.514404640805__py3-none-any.whl → 8.0.14958192442__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.
- kodexa/dataclasses/__init__.py +1 -1
- kodexa/model/__init__.py +2 -2
- kodexa/model/objects.py +21 -1
- kodexa/model/utils.py +1 -1
- kodexa/pipeline/pipeline.py +1 -1
- kodexa/platform/client.py +1 -2
- kodexa/platform/kodexa.py +4 -1
- kodexa/platform/manifest.py +447 -0
- kodexa/selectors/__init__.py +1 -1
- kodexa/selectors/ast.py +371 -98
- kodexa/selectors/error.py +29 -0
- kodexa/selectors/kodexa-ast-visitor.py +268 -0
- kodexa/selectors/parser.py +91 -0
- kodexa/selectors/resources/KodexaSelector.interp +99 -0
- kodexa/selectors/resources/KodexaSelector.tokens +56 -0
- kodexa/selectors/resources/KodexaSelectorLexer.interp +119 -0
- kodexa/selectors/resources/KodexaSelectorLexer.py +204 -0
- kodexa/selectors/resources/KodexaSelectorLexer.tokens +56 -0
- kodexa/selectors/resources/KodexaSelectorListener.py +570 -0
- kodexa/selectors/resources/KodexaSelectorParser.py +3246 -0
- kodexa/selectors/resources/KodexaSelectorVisitor.py +323 -0
- kodexa/selectors/visitor.py +265 -0
- kodexa/steps/__init__.py +4 -2
- kodexa/steps/common.py +0 -68
- kodexa/testing/test_utils.py +1 -1
- {kodexa-7.5.514404640805.dist-info → kodexa-8.0.14958192442.dist-info}/METADATA +7 -3
- kodexa-8.0.14958192442.dist-info/RECORD +53 -0
- {kodexa-7.5.514404640805.dist-info → kodexa-8.0.14958192442.dist-info}/WHEEL +1 -1
- kodexa/model/model.py +0 -3259
- kodexa/model/persistence.py +0 -2017
- kodexa/selectors/core.py +0 -124
- kodexa/selectors/lexrules.py +0 -137
- kodexa/selectors/lextab.py +0 -83
- kodexa/selectors/lextab.pyi +0 -1
- kodexa/selectors/parserules.py +0 -414
- kodexa/selectors/parserules.pyi +0 -1
- kodexa/selectors/parsetab.py +0 -4149
- kodexa/selectors/parsetab.pyi +0 -1
- kodexa-7.5.514404640805.dist-info/RECORD +0 -50
- {kodexa-7.5.514404640805.dist-info → kodexa-8.0.14958192442.dist-info}/LICENSE +0 -0
kodexa/dataclasses/__init__.py
CHANGED
@@ -345,7 +345,7 @@ class LLMDataObject(BaseModel):
|
|
345
345
|
owner_uri=f"assistant://{assistant.id}" if assistant else f"model://taxonomy-llm")
|
346
346
|
current_value.append(new_tag)
|
347
347
|
node.remove_feature("tag", tag)
|
348
|
-
node.add_feature("tag", tag, current_value
|
348
|
+
node.add_feature("tag", tag, current_value)
|
349
349
|
|
350
350
|
logger.info(f"Applied label {tag} to {len(nodes_to_label)} nodes")
|
351
351
|
|
kodexa/model/__init__.py
CHANGED
@@ -11,7 +11,7 @@ and much more....
|
|
11
11
|
|
12
12
|
Document families allow the organization of documents based on transitions and actors
|
13
13
|
"""
|
14
|
-
from .model import (
|
14
|
+
from kodexa_document.model import (
|
15
15
|
ContentFeature,
|
16
16
|
ContentNode,
|
17
17
|
Document,
|
@@ -36,4 +36,4 @@ from .objects import (
|
|
36
36
|
ExtensionPack,
|
37
37
|
AssistantDefinition,
|
38
38
|
)
|
39
|
-
from .persistence import SqliteDocumentPersistence
|
39
|
+
from kodexa_document.persistence import SqliteDocumentPersistence
|
kodexa/model/objects.py
CHANGED
@@ -2385,7 +2385,11 @@ class ExtensionPackProvided(BaseModel):
|
|
2385
2385
|
delete_protection: Optional[bool] = Field(
|
2386
2386
|
None, description="Delete protection", alias="deleteProtection"
|
2387
2387
|
)
|
2388
|
-
|
2388
|
+
deprecated: Optional[bool] = Field(
|
2389
|
+
None,
|
2390
|
+
description="Resource is marked for deprecation",
|
2391
|
+
alias="deprecate",
|
2392
|
+
)
|
2389
2393
|
id: Optional[str] = Field(
|
2390
2394
|
None,
|
2391
2395
|
alias="_id",
|
@@ -2617,6 +2621,7 @@ class TaxonValidation(BaseModel):
|
|
2617
2621
|
exception_id: Optional[str] = Field(None, alias="exceptionId")
|
2618
2622
|
support_article_id: Optional[str] = Field(None, alias="supportArticleId")
|
2619
2623
|
overridable: Optional[bool] = None
|
2624
|
+
disabled: Optional[bool] = None
|
2620
2625
|
|
2621
2626
|
|
2622
2627
|
class DocumentTaxonValidation(BaseModel):
|
@@ -3059,6 +3064,7 @@ class ProjectDocumentStatus(BaseModel):
|
|
3059
3064
|
color: Optional[str] = Field(None, max_length=25)
|
3060
3065
|
icon: Optional[str] = Field(None, max_length=25)
|
3061
3066
|
status: str = Field(..., max_length=255)
|
3067
|
+
slug: str = Field(..., max_length=255)
|
3062
3068
|
status_type: Optional[StatusType2] = Field(None, alias="statusType")
|
3063
3069
|
|
3064
3070
|
class ProjectTaskStatus(BaseModel):
|
@@ -6285,6 +6291,20 @@ class ScheduledEvent(BaseModel):
|
|
6285
6291
|
next_event: Optional[StandardDateTime] = Field(None, alias="nextEvent")
|
6286
6292
|
|
6287
6293
|
|
6294
|
+
class OrchestrationEvent(BaseModel):
|
6295
|
+
"""
|
6296
|
+
|
6297
|
+
"""
|
6298
|
+
model_config = ConfigDict(
|
6299
|
+
populate_by_name=True,
|
6300
|
+
use_enum_values=True,
|
6301
|
+
arbitrary_types_allowed=True,
|
6302
|
+
protected_namespaces=("model_config",),
|
6303
|
+
)
|
6304
|
+
type: Optional[str] = None
|
6305
|
+
execution_event: Optional[ExecutionEvent] = Field(None, alias="executionEvent")
|
6306
|
+
|
6307
|
+
|
6288
6308
|
ThrowableProblem.model_rebuild()
|
6289
6309
|
Option.model_rebuild()
|
6290
6310
|
Taxon.model_rebuild()
|
kodexa/model/utils.py
CHANGED
@@ -7,7 +7,7 @@ logger = logging.getLogger(__name__)
|
|
7
7
|
def get_pretty_text_from_lines(lines: list[ContentNode], scale, include_line_uuid=False) -> str:
|
8
8
|
pretty_text = ""
|
9
9
|
for line_index, line in enumerate(lines):
|
10
|
-
line_content = f"('{line.
|
10
|
+
line_content = f"('{line.id}')" if include_line_uuid else ""
|
11
11
|
current_x = 0
|
12
12
|
for word in line.select('//word'):
|
13
13
|
x = int(word.get_bbox()[0] * scale)
|
kodexa/pipeline/pipeline.py
CHANGED
@@ -580,7 +580,7 @@ class Pipeline:
|
|
580
580
|
# and the store itself - to provide richness to the action
|
581
581
|
|
582
582
|
for connector_object in self.connector:
|
583
|
-
from
|
583
|
+
from kodexa_document.model import ContentObjectReference
|
584
584
|
|
585
585
|
if isinstance(connector_object, ContentObjectReference):
|
586
586
|
document = connector_object.document
|
kodexa/platform/client.py
CHANGED
@@ -22,8 +22,7 @@ from functional import seq
|
|
22
22
|
from pydantic import BaseModel, Field, ConfigDict
|
23
23
|
from pydantic_yaml import to_yaml_str
|
24
24
|
|
25
|
-
from
|
26
|
-
from kodexa.model.model import Ref
|
25
|
+
from kodexa_document.model import Document, Ref
|
27
26
|
from kodexa.model.objects import (
|
28
27
|
PageUser,
|
29
28
|
PageMembership,
|
kodexa/platform/kodexa.py
CHANGED
@@ -192,11 +192,14 @@ class KodexaPlatform:
|
|
192
192
|
"""
|
193
193
|
kodexa_config = get_config(profile)
|
194
194
|
env_url = os.getenv("KODEXA_URL", None)
|
195
|
-
|
195
|
+
final_url = (
|
196
196
|
env_url
|
197
197
|
if env_url is not None
|
198
198
|
else kodexa_config[get_profile(profile)]["url"]
|
199
199
|
)
|
200
|
+
if final_url is None:
|
201
|
+
raise Exception("No URL set, please set KODEXA_URL or configure a profile (see https://developer.kodexa.ai/guides/cli/authentication)")
|
202
|
+
return final_url
|
200
203
|
|
201
204
|
@staticmethod
|
202
205
|
def set_access_token(access_token: str):
|
@@ -0,0 +1,447 @@
|
|
1
|
+
import yaml
|
2
|
+
import os
|
3
|
+
import glob
|
4
|
+
import logging
|
5
|
+
import json
|
6
|
+
from kodexa import KodexaClient
|
7
|
+
|
8
|
+
logger = logging.getLogger(__name__)
|
9
|
+
|
10
|
+
|
11
|
+
class ManifestManager:
|
12
|
+
"""
|
13
|
+
A class to manage the manifests for deploying and monitoring
|
14
|
+
Kodexa use-cases
|
15
|
+
"""
|
16
|
+
|
17
|
+
def __init__(self, kodexa_client: KodexaClient):
|
18
|
+
self.kodexa_client = kodexa_client
|
19
|
+
|
20
|
+
@staticmethod
|
21
|
+
def read_manifest(manifest_path: str) -> list:
|
22
|
+
"""
|
23
|
+
Read and parse a YAML manifest file and extract resource paths.
|
24
|
+
|
25
|
+
Args:
|
26
|
+
manifest_path (str): Path to the YAML manifest file.
|
27
|
+
|
28
|
+
Returns:
|
29
|
+
list: List of resource paths extracted from the manifest file.
|
30
|
+
Returns an empty list if 'resource-paths' key is not present
|
31
|
+
or the file doesn't exist.
|
32
|
+
"""
|
33
|
+
if not os.path.exists(manifest_path):
|
34
|
+
logger.warning(f"Manifest file not found: {manifest_path}")
|
35
|
+
return []
|
36
|
+
try:
|
37
|
+
with open(manifest_path, 'r') as file:
|
38
|
+
manifest = yaml.safe_load(file)
|
39
|
+
# Support 'resource-paths' as the primary key
|
40
|
+
return manifest.get('resource-paths', [])
|
41
|
+
except Exception as e:
|
42
|
+
logger.error(
|
43
|
+
f"Failed to read or parse manifest {manifest_path}: {e}",
|
44
|
+
exc_info=True
|
45
|
+
)
|
46
|
+
return []
|
47
|
+
|
48
|
+
def deploy_from_manifest(self, manifest_path: str,
|
49
|
+
org_slug: str | None = None):
|
50
|
+
"""
|
51
|
+
Deploys all resources listed in a manifest file.
|
52
|
+
|
53
|
+
Args:
|
54
|
+
manifest_path (str): The path to the manifest file.
|
55
|
+
org_slug (str | None): The organization slug to deploy to. If None,
|
56
|
+
uses the default org from the client.
|
57
|
+
"""
|
58
|
+
logger.info(
|
59
|
+
f"Starting deployment from manifest {manifest_path} "
|
60
|
+
f"to organization {org_slug}"
|
61
|
+
)
|
62
|
+
resource_paths = self.read_manifest(manifest_path)
|
63
|
+
abs_manifest_path = os.path.abspath(manifest_path)
|
64
|
+
manifest_dir = os.path.dirname(abs_manifest_path)
|
65
|
+
original_cwd = os.getcwd()
|
66
|
+
deployed_count = 0
|
67
|
+
|
68
|
+
try:
|
69
|
+
# Change to manifest directory to resolve relative paths
|
70
|
+
os.chdir(manifest_dir)
|
71
|
+
|
72
|
+
for resource_pattern in resource_paths:
|
73
|
+
# Use glob to find matching files
|
74
|
+
resource_files = glob.glob(resource_pattern, recursive=True)
|
75
|
+
|
76
|
+
if not resource_files:
|
77
|
+
logger.warning(
|
78
|
+
f"No files found matching pattern '{resource_pattern}' "
|
79
|
+
f"relative to {manifest_dir}"
|
80
|
+
)
|
81
|
+
continue
|
82
|
+
|
83
|
+
for rel_path in resource_files:
|
84
|
+
# Resolve absolute path based on current directory
|
85
|
+
abs_path = os.path.abspath(rel_path)
|
86
|
+
logger.info(f"Processing resource file: {abs_path}")
|
87
|
+
resource_dir = os.path.dirname(abs_path)
|
88
|
+
|
89
|
+
# Temporarily change to resource file's directory
|
90
|
+
# This helps with relative paths in the resource definition
|
91
|
+
os.chdir(resource_dir)
|
92
|
+
|
93
|
+
try:
|
94
|
+
with open(abs_path, 'r') as f:
|
95
|
+
path_lower = abs_path.lower()
|
96
|
+
if path_lower.endswith(".json"):
|
97
|
+
obj = json.load(f)
|
98
|
+
elif path_lower.endswith((".yaml", ".yml")):
|
99
|
+
obj = yaml.safe_load(f)
|
100
|
+
else:
|
101
|
+
logger.warning(
|
102
|
+
f"Skipping unsupported file type: {abs_path}"
|
103
|
+
)
|
104
|
+
continue
|
105
|
+
|
106
|
+
components = []
|
107
|
+
if isinstance(obj, list):
|
108
|
+
logger.info(f"Found {len(obj)} components in {abs_path}")
|
109
|
+
components.extend(obj)
|
110
|
+
elif isinstance(obj, dict):
|
111
|
+
components.append(obj)
|
112
|
+
else:
|
113
|
+
logger.warning(
|
114
|
+
f"Skipping unexpected object type in {abs_path}"
|
115
|
+
)
|
116
|
+
continue
|
117
|
+
|
118
|
+
for comp_obj in components:
|
119
|
+
# Ensure component object is a dictionary
|
120
|
+
if not isinstance(comp_obj, dict):
|
121
|
+
logger.warning(
|
122
|
+
f"Skipping non-dictionary item in {abs_path}"
|
123
|
+
)
|
124
|
+
continue
|
125
|
+
|
126
|
+
try:
|
127
|
+
if "deployed" in comp_obj:
|
128
|
+
# Remove deployment state if present
|
129
|
+
del comp_obj["deployed"]
|
130
|
+
|
131
|
+
component = self.kodexa_client.deserialize(comp_obj)
|
132
|
+
# Ensure correct org slug
|
133
|
+
component.org_slug = target_org_slug
|
134
|
+
|
135
|
+
logger.info(
|
136
|
+
"Deploying component %s:%s",
|
137
|
+
component.slug, component.version
|
138
|
+
)
|
139
|
+
log_details = component.deploy(update=True)
|
140
|
+
deployed_count += 1
|
141
|
+
for log_detail in log_details:
|
142
|
+
logger.info(log_detail)
|
143
|
+
except Exception as e:
|
144
|
+
comp_id = comp_obj.get('slug', 'unknown')
|
145
|
+
logger.error(
|
146
|
+
"Failed to deploy component %s from %s: %s",
|
147
|
+
comp_id, abs_path, e, exc_info=True
|
148
|
+
)
|
149
|
+
# Option: stop deployment on first error
|
150
|
+
# raise
|
151
|
+
|
152
|
+
except Exception as e:
|
153
|
+
logger.error(
|
154
|
+
"Failed to process resource file %s: %s",
|
155
|
+
abs_path, e, exc_info=True
|
156
|
+
)
|
157
|
+
finally:
|
158
|
+
# Return to manifest directory for next file
|
159
|
+
os.chdir(manifest_dir)
|
160
|
+
|
161
|
+
finally:
|
162
|
+
# Return to original working directory
|
163
|
+
os.chdir(original_cwd)
|
164
|
+
|
165
|
+
logger.info(
|
166
|
+
f"Deployment completed from manifest {manifest_path}. "
|
167
|
+
f"Deployed {deployed_count} components."
|
168
|
+
)
|
169
|
+
|
170
|
+
def undeploy_from_manifest(self, manifest_path: str,
|
171
|
+
org_slug: str | None = None):
|
172
|
+
"""
|
173
|
+
Undeploys all resources listed in a manifest file.
|
174
|
+
|
175
|
+
Args:
|
176
|
+
manifest_path (str): The path to the manifest file.
|
177
|
+
org_slug (str | None): The organization slug to undeploy from.
|
178
|
+
If None, uses the default org from client.
|
179
|
+
"""
|
180
|
+
target_org_slug = org_slug or self.kodexa_client.get_org_slug()
|
181
|
+
if not target_org_slug:
|
182
|
+
msg = "Organization slug must be provided or set in the client."
|
183
|
+
raise ValueError(msg)
|
184
|
+
|
185
|
+
logger.info(
|
186
|
+
f"Starting undeployment from manifest {manifest_path} "
|
187
|
+
f"for organization {target_org_slug}"
|
188
|
+
)
|
189
|
+
resource_paths = self.read_manifest(manifest_path)
|
190
|
+
abs_manifest_path = os.path.abspath(manifest_path)
|
191
|
+
manifest_dir = os.path.dirname(abs_manifest_path)
|
192
|
+
original_cwd = os.getcwd()
|
193
|
+
undeployed_count = 0
|
194
|
+
|
195
|
+
try:
|
196
|
+
# Change to manifest directory to resolve relative paths
|
197
|
+
os.chdir(manifest_dir)
|
198
|
+
|
199
|
+
for resource_pattern in resource_paths:
|
200
|
+
resource_files = glob.glob(resource_pattern, recursive=True)
|
201
|
+
|
202
|
+
if not resource_files:
|
203
|
+
logger.warning(
|
204
|
+
f"No files found matching pattern '{resource_pattern}' "
|
205
|
+
f"relative to {manifest_dir}"
|
206
|
+
)
|
207
|
+
continue
|
208
|
+
|
209
|
+
for rel_path in resource_files:
|
210
|
+
# Resolve absolute path based on current directory
|
211
|
+
abs_path = os.path.abspath(rel_path)
|
212
|
+
logger.info(f"Processing for undeployment: {abs_path}")
|
213
|
+
|
214
|
+
try:
|
215
|
+
with open(abs_path, 'r') as f:
|
216
|
+
path_lower = abs_path.lower()
|
217
|
+
if path_lower.endswith(".json"):
|
218
|
+
obj = json.load(f)
|
219
|
+
elif path_lower.endswith((".yaml", ".yml")):
|
220
|
+
obj = yaml.safe_load(f)
|
221
|
+
else:
|
222
|
+
logger.warning(
|
223
|
+
f"Skipping unsupported file type: {abs_path}"
|
224
|
+
)
|
225
|
+
continue
|
226
|
+
|
227
|
+
components = []
|
228
|
+
if isinstance(obj, list):
|
229
|
+
logger.info(f"Found {len(obj)} components in {abs_path}")
|
230
|
+
components.extend(obj)
|
231
|
+
elif isinstance(obj, dict):
|
232
|
+
components.append(obj)
|
233
|
+
else:
|
234
|
+
logger.warning(
|
235
|
+
f"Skipping unexpected object type in {abs_path}"
|
236
|
+
)
|
237
|
+
continue
|
238
|
+
|
239
|
+
for comp_obj in components:
|
240
|
+
# Ensure component object is a dictionary
|
241
|
+
if not isinstance(comp_obj, dict):
|
242
|
+
logger.warning(
|
243
|
+
f"Skipping non-dictionary item in {abs_path}"
|
244
|
+
)
|
245
|
+
continue
|
246
|
+
|
247
|
+
slug = comp_obj.get('slug')
|
248
|
+
version = comp_obj.get('version')
|
249
|
+
|
250
|
+
if not slug or not version:
|
251
|
+
logger.warning(
|
252
|
+
"Skipping component in %s (missing slug/version)",
|
253
|
+
abs_path
|
254
|
+
)
|
255
|
+
continue
|
256
|
+
|
257
|
+
try:
|
258
|
+
# Get component object to call undeploy
|
259
|
+
# Note: This assumes get_object method exists
|
260
|
+
# with appropriate parameters and return value
|
261
|
+
component = self.kodexa_client.get_object(
|
262
|
+
target_org_slug, slug, version
|
263
|
+
)
|
264
|
+
if component:
|
265
|
+
logger.info(
|
266
|
+
"Undeploying component %s:%s",
|
267
|
+
slug, version
|
268
|
+
)
|
269
|
+
component.undeploy()
|
270
|
+
undeployed_count += 1
|
271
|
+
else:
|
272
|
+
logger.warning(
|
273
|
+
"Component %s:%s not found in org %s",
|
274
|
+
slug, version, target_org_slug
|
275
|
+
)
|
276
|
+
except Exception as e:
|
277
|
+
logger.error(
|
278
|
+
"Failed to undeploy component %s:%s: %s",
|
279
|
+
slug, version, e, exc_info=True
|
280
|
+
)
|
281
|
+
# Option: stop on first error
|
282
|
+
# raise
|
283
|
+
|
284
|
+
except Exception as e:
|
285
|
+
logger.error(
|
286
|
+
"Failed to process file %s for undeployment: %s",
|
287
|
+
abs_path, e, exc_info=True
|
288
|
+
)
|
289
|
+
|
290
|
+
finally:
|
291
|
+
# Return to original working directory
|
292
|
+
os.chdir(original_cwd)
|
293
|
+
|
294
|
+
logger.info(
|
295
|
+
f"Undeployment completed from manifest {manifest_path}. "
|
296
|
+
f"Undeployed {undeployed_count} components."
|
297
|
+
)
|
298
|
+
|
299
|
+
def sync_from_instance(self, manifest_path: str, org_slug: str | None = None):
|
300
|
+
"""
|
301
|
+
Syncs resources from a Kodexa instance to the filesystem based on a manifest.
|
302
|
+
|
303
|
+
This reads a manifest file and for each resource defined:
|
304
|
+
1. Retrieves the resource from the Kodexa instance
|
305
|
+
2. Saves it to the corresponding file on the filesystem
|
306
|
+
|
307
|
+
Args:
|
308
|
+
manifest_path (str): The path to the manifest file.
|
309
|
+
org_slug (str | None): The organization slug to sync from.
|
310
|
+
If None, uses the default org from client.
|
311
|
+
"""
|
312
|
+
target_org_slug = org_slug or self.kodexa_client.get_org_slug()
|
313
|
+
if not target_org_slug:
|
314
|
+
msg = "Organization slug must be provided or set in the client."
|
315
|
+
raise ValueError(msg)
|
316
|
+
|
317
|
+
logger.info(
|
318
|
+
f"Starting sync from instance to filesystem using manifest "
|
319
|
+
f"{manifest_path} for organization {target_org_slug}"
|
320
|
+
)
|
321
|
+
resource_paths = self.read_manifest(manifest_path)
|
322
|
+
abs_manifest_path = os.path.abspath(manifest_path)
|
323
|
+
manifest_dir = os.path.dirname(abs_manifest_path)
|
324
|
+
original_cwd = os.getcwd()
|
325
|
+
synced_count = 0
|
326
|
+
|
327
|
+
try:
|
328
|
+
# Change to manifest directory to resolve relative paths
|
329
|
+
os.chdir(manifest_dir)
|
330
|
+
|
331
|
+
for resource_pattern in resource_paths:
|
332
|
+
resource_files = glob.glob(resource_pattern, recursive=True)
|
333
|
+
|
334
|
+
if not resource_files:
|
335
|
+
logger.warning(
|
336
|
+
f"No files found matching pattern '{resource_pattern}' "
|
337
|
+
f"relative to {manifest_dir}"
|
338
|
+
)
|
339
|
+
continue
|
340
|
+
|
341
|
+
for rel_path in resource_files:
|
342
|
+
# Resolve absolute path based on current directory
|
343
|
+
abs_path = os.path.abspath(rel_path)
|
344
|
+
logger.info(f"Processing for sync: {abs_path}")
|
345
|
+
|
346
|
+
try:
|
347
|
+
# Read the file to get component information
|
348
|
+
with open(abs_path, 'r') as f:
|
349
|
+
path_lower = abs_path.lower()
|
350
|
+
if path_lower.endswith(".json"):
|
351
|
+
file_obj = json.load(f)
|
352
|
+
elif path_lower.endswith((".yaml", ".yml")):
|
353
|
+
file_obj = yaml.safe_load(f)
|
354
|
+
else:
|
355
|
+
logger.warning(
|
356
|
+
f"Skipping unsupported file type: {abs_path}"
|
357
|
+
)
|
358
|
+
continue
|
359
|
+
|
360
|
+
components = []
|
361
|
+
if isinstance(file_obj, list):
|
362
|
+
logger.info(
|
363
|
+
f"Found {len(file_obj)} components in {abs_path}"
|
364
|
+
)
|
365
|
+
components.extend(file_obj)
|
366
|
+
elif isinstance(file_obj, dict):
|
367
|
+
components.append(file_obj)
|
368
|
+
else:
|
369
|
+
logger.warning(
|
370
|
+
f"Skipping unexpected object type in {abs_path}"
|
371
|
+
)
|
372
|
+
continue
|
373
|
+
|
374
|
+
for comp_obj in components:
|
375
|
+
# Ensure component object is a dictionary
|
376
|
+
if not isinstance(comp_obj, dict):
|
377
|
+
logger.warning(
|
378
|
+
f"Skipping non-dictionary item in {abs_path}"
|
379
|
+
)
|
380
|
+
continue
|
381
|
+
|
382
|
+
slug = comp_obj.get('slug')
|
383
|
+
version = comp_obj.get('version')
|
384
|
+
|
385
|
+
if not slug or not version:
|
386
|
+
logger.warning(
|
387
|
+
"Skipping component (missing slug/version) in %s",
|
388
|
+
abs_path
|
389
|
+
)
|
390
|
+
continue
|
391
|
+
|
392
|
+
try:
|
393
|
+
# Get the component from the Kodexa instance
|
394
|
+
logger.info(
|
395
|
+
f"Retrieving component {slug}:{version} "
|
396
|
+
f"from org {target_org_slug}"
|
397
|
+
)
|
398
|
+
component = self.kodexa_client.get_object(
|
399
|
+
target_org_slug, slug, version
|
400
|
+
)
|
401
|
+
|
402
|
+
if not component:
|
403
|
+
logger.warning(
|
404
|
+
f"Component {slug}:{version} not found "
|
405
|
+
f"in org {target_org_slug}"
|
406
|
+
)
|
407
|
+
continue
|
408
|
+
|
409
|
+
# Serialize the component to data
|
410
|
+
updated_obj = component.serialize()
|
411
|
+
|
412
|
+
# Write the updated data back to the file
|
413
|
+
write_format = "json"
|
414
|
+
if path_lower.endswith((".yaml", ".yml")):
|
415
|
+
write_format = "yaml"
|
416
|
+
|
417
|
+
with open(abs_path, 'w') as f:
|
418
|
+
if write_format == "json":
|
419
|
+
json.dump(updated_obj, f, indent=2)
|
420
|
+
else:
|
421
|
+
yaml.dump(updated_obj, f)
|
422
|
+
|
423
|
+
logger.info(
|
424
|
+
f"Synced component {slug}:{version} to {abs_path}"
|
425
|
+
)
|
426
|
+
synced_count += 1
|
427
|
+
|
428
|
+
except Exception as e:
|
429
|
+
logger.error(
|
430
|
+
"Failed to sync component %s:%s: %s",
|
431
|
+
slug, version, e, exc_info=True
|
432
|
+
)
|
433
|
+
except Exception as e:
|
434
|
+
logger.error(
|
435
|
+
"Failed to process file %s for sync: %s",
|
436
|
+
abs_path, e, exc_info=True
|
437
|
+
)
|
438
|
+
|
439
|
+
finally:
|
440
|
+
# Return to original working directory
|
441
|
+
os.chdir(original_cwd)
|
442
|
+
|
443
|
+
logger.info(
|
444
|
+
f"Sync completed from instance to filesystem using manifest "
|
445
|
+
f"{manifest_path}. Synced {synced_count} components."
|
446
|
+
)
|
447
|
+
|
kodexa/selectors/__init__.py
CHANGED