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.
Files changed (40) hide show
  1. kodexa/dataclasses/__init__.py +1 -1
  2. kodexa/model/__init__.py +2 -2
  3. kodexa/model/objects.py +21 -1
  4. kodexa/model/utils.py +1 -1
  5. kodexa/pipeline/pipeline.py +1 -1
  6. kodexa/platform/client.py +1 -2
  7. kodexa/platform/kodexa.py +4 -1
  8. kodexa/platform/manifest.py +447 -0
  9. kodexa/selectors/__init__.py +1 -1
  10. kodexa/selectors/ast.py +371 -98
  11. kodexa/selectors/error.py +29 -0
  12. kodexa/selectors/kodexa-ast-visitor.py +268 -0
  13. kodexa/selectors/parser.py +91 -0
  14. kodexa/selectors/resources/KodexaSelector.interp +99 -0
  15. kodexa/selectors/resources/KodexaSelector.tokens +56 -0
  16. kodexa/selectors/resources/KodexaSelectorLexer.interp +119 -0
  17. kodexa/selectors/resources/KodexaSelectorLexer.py +204 -0
  18. kodexa/selectors/resources/KodexaSelectorLexer.tokens +56 -0
  19. kodexa/selectors/resources/KodexaSelectorListener.py +570 -0
  20. kodexa/selectors/resources/KodexaSelectorParser.py +3246 -0
  21. kodexa/selectors/resources/KodexaSelectorVisitor.py +323 -0
  22. kodexa/selectors/visitor.py +265 -0
  23. kodexa/steps/__init__.py +4 -2
  24. kodexa/steps/common.py +0 -68
  25. kodexa/testing/test_utils.py +1 -1
  26. {kodexa-7.5.514404640805.dist-info → kodexa-8.0.14958192442.dist-info}/METADATA +7 -3
  27. kodexa-8.0.14958192442.dist-info/RECORD +53 -0
  28. {kodexa-7.5.514404640805.dist-info → kodexa-8.0.14958192442.dist-info}/WHEEL +1 -1
  29. kodexa/model/model.py +0 -3259
  30. kodexa/model/persistence.py +0 -2017
  31. kodexa/selectors/core.py +0 -124
  32. kodexa/selectors/lexrules.py +0 -137
  33. kodexa/selectors/lextab.py +0 -83
  34. kodexa/selectors/lextab.pyi +0 -1
  35. kodexa/selectors/parserules.py +0 -414
  36. kodexa/selectors/parserules.pyi +0 -1
  37. kodexa/selectors/parsetab.py +0 -4149
  38. kodexa/selectors/parsetab.pyi +0 -1
  39. kodexa-7.5.514404640805.dist-info/RECORD +0 -50
  40. {kodexa-7.5.514404640805.dist-info → kodexa-8.0.14958192442.dist-info}/LICENSE +0 -0
@@ -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, single=False)
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, PersistenceManager
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.uuid}')" if include_line_uuid else ""
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)
@@ -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 kodexa.model.model import ContentObjectReference
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 kodexa.model import Document
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
- return (
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
+
@@ -2,4 +2,4 @@
2
2
  Selectors allow you to work with a Kodexa document to find content
3
3
  """
4
4
 
5
- from .core import parse
5
+ from .parser import parse