iris-pex-embedded-python 4.0.0b3__py3-none-any.whl → 4.0.0b4__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.
iop/__init__.py CHANGED
@@ -31,6 +31,10 @@ from iop.production import Production as Production
31
31
  from iop.production import ProductionDiff as ProductionDiff
32
32
  from iop.production import ProductionDiffEntry as ProductionDiffEntry
33
33
  from iop.production import ProductionGraph as ProductionGraph
34
+ from iop.production import ProductionValidationError as ProductionValidationError
35
+ from iop.production import ProductionValidationIssue as ProductionValidationIssue
36
+ from iop.production import ProductionValidationReport as ProductionValidationReport
37
+ from iop.production import ProductionValidationWarning as ProductionValidationWarning
34
38
  from iop.production import target as target
35
39
  from iop.runtime.director import _Director
36
40
  from iop.runtime.protocol import DirectorProtocol as DirectorProtocol
@@ -59,6 +63,10 @@ __all__ = [
59
63
  "ProductionDiff",
60
64
  "ProductionDiffEntry",
61
65
  "ProductionGraph",
66
+ "ProductionValidationError",
67
+ "ProductionValidationIssue",
68
+ "ProductionValidationReport",
69
+ "ProductionValidationWarning",
62
70
  "PydanticMessage",
63
71
  "PydanticPickleMessage",
64
72
  "Setting",
iop/cli/main.py CHANGED
@@ -244,17 +244,33 @@ class Command:
244
244
  if self.args.migration_plan:
245
245
  print(
246
246
  migration_utils.explain_migration(
247
- migrate_path, mode=mode, namespace=self.director.namespace
247
+ migrate_path,
248
+ mode=mode,
249
+ namespace=self.director.namespace,
250
+ strict_production_validation=(
251
+ self.args.strict_production_validation
252
+ ),
248
253
  )
249
254
  )
250
255
  return
251
256
  if self._is_remote:
252
257
  print(
253
258
  migration_utils.explain_migration(
254
- migrate_path, mode=mode, namespace=self.director.namespace
259
+ migrate_path,
260
+ mode=mode,
261
+ namespace=self.director.namespace,
262
+ strict_production_validation=(
263
+ self.args.strict_production_validation
264
+ ),
255
265
  )
256
266
  )
257
- self.director.migrate(migrate_path)
267
+ if self.args.strict_production_validation:
268
+ self.director.migrate(
269
+ migrate_path,
270
+ strict_production_validation=True,
271
+ )
272
+ else:
273
+ self.director.migrate(migrate_path)
258
274
  if self._is_remote:
259
275
  print(
260
276
  migration_utils.format_migration_success(
iop/cli/parser.py CHANGED
@@ -95,6 +95,11 @@ def create_parser() -> argparse.ArgumentParser:
95
95
  help="show the migration plan and validation messages without writing to IRIS",
96
96
  action="store_true",
97
97
  )
98
+ migrate.add_argument(
99
+ "--strict-production-validation",
100
+ help="fail migration when production validation reports issues",
101
+ action="store_true",
102
+ )
98
103
 
99
104
  export = main_parser.add_argument_group("export arguments")
100
105
  export.add_argument(
iop/cli/types.py CHANGED
@@ -55,3 +55,4 @@ class CommandArgs:
55
55
  remote_settings: str | None = None
56
56
  update: bool = False
57
57
  migration_plan: bool = False
58
+ strict_production_validation: bool = False
@@ -499,6 +499,7 @@ ClassMethod PutMigrate() As %DynamicObject
499
499
  set targetDirectory = dyna.%Get("remote_folder")
500
500
  set packageName = dyna.%Get("package")
501
501
  set settingsFile = dyna.%Get("settings_file")
502
+ set strictProductionValidation = dyna.%Get("strict_production_validation")
502
503
  If settingsFile = "" { Set settingsFile = "settings.py" }
503
504
  // check for namespace existence and user permissions against namespace
504
505
  If '..NamespaceCheck(namespace) {
@@ -551,7 +552,7 @@ ClassMethod PutMigrate() As %DynamicObject
551
552
  set iopUtils = ##class(IOP.Wrapper).Import("iop.migration.utils")
552
553
  set builtins = ##class(%SYS.Python).Import("builtins")
553
554
  If builtins.hasattr(iopUtils, "migrate") {
554
- do builtins.getattr(iopUtils, "migrate")."__call__"(##class(%Library.File).NormalizeFilename(settingsFile, packagePath))
555
+ do builtins.getattr(iopUtils, "migrate")."__call__"(##class(%Library.File).NormalizeFilename(settingsFile, packagePath),"REMOTE",namespace,strictProductionValidation)
555
556
  } Else {
556
557
  do iopUtils."_Utils".migrate(##class(%Library.File).NormalizeFilename(settingsFile, packagePath))
557
558
  }
iop/cls/IOP/Utils.cls CHANGED
@@ -719,8 +719,10 @@ ClassMethod InsertSetting(
719
719
  pSettingData) As %Status [ Internal, Private ]
720
720
  {
721
721
  #dim tSetting As Ens.Config.Setting
722
+ #dim tDefaultTarget As %String
723
+ Set tDefaultTarget = $Select(pOwner.%IsA("Ens.Config.Production"):"",1:"Host")
722
724
  Set tSetting = ##class(Ens.Config.Setting).%New()
723
- Set tSetting.Target = ..DynamicGet(pSettingData,"@Target","Host")
725
+ Set tSetting.Target = ..DynamicGet(pSettingData,"@Target",tDefaultTarget)
724
726
  Set tSetting.Name = ..DynamicGet(pSettingData,"@Name","")
725
727
  Set tSetting.Value = ..DynamicGet(pSettingData,"#text","")
726
728
  Do pOwner.Settings.Insert(tSetting)
iop/migration/plans.py CHANGED
@@ -4,6 +4,8 @@ import inspect
4
4
  import os
5
5
  from typing import Any
6
6
 
7
+ from ..production.validation import validate_production_entry
8
+
7
9
 
8
10
  def format_migration_success(filename: str, namespace: str | None = None) -> str:
9
11
  suffix = f" in namespace {namespace}" if namespace else ""
@@ -21,6 +23,7 @@ def format_migration_plan(plan: dict[str, Any]) -> str:
21
23
  lines.extend(format_plan_section("CLASSES", plan["classes"]))
22
24
  lines.extend(format_plan_section("SCHEMAS", plan["schemas"]))
23
25
  lines.extend(format_plan_section("PRODUCTIONS", plan["productions"]))
26
+ lines.extend(format_plan_section("VALIDATION", plan.get("validation", [])))
24
27
  return "\n".join(lines)
25
28
 
26
29
 
@@ -47,6 +50,7 @@ class MigrationPlanner:
47
50
  filename=None,
48
51
  mode: str | None = None,
49
52
  namespace: str | None = None,
53
+ strict_production_validation: bool = False,
50
54
  ) -> dict[str, Any]:
51
55
  """Build and validate a migration plan from a settings module."""
52
56
  if not path:
@@ -59,11 +63,16 @@ class MigrationPlanner:
59
63
  "classes": [],
60
64
  "schemas": [],
61
65
  "productions": [],
66
+ "validation": [],
62
67
  }
63
68
 
64
69
  self._add_class_entries(plan, getattr(settings, "CLASSES", {}), path)
65
70
  self._add_schema_entries(plan, getattr(settings, "SCHEMAS", None))
66
- self._add_production_entries(plan, getattr(settings, "PRODUCTIONS", None))
71
+ self._add_production_entries(
72
+ plan,
73
+ getattr(settings, "PRODUCTIONS", None),
74
+ strict_production_validation=strict_production_validation,
75
+ )
67
76
  return plan
68
77
 
69
78
  @staticmethod
@@ -102,7 +111,11 @@ class MigrationPlanner:
102
111
  plan["schemas"].append(self._utils._python_classname(cls))
103
112
 
104
113
  def _add_production_entries(
105
- self, plan: dict[str, Any], productions: list[Any] | None
114
+ self,
115
+ plan: dict[str, Any],
116
+ productions: list[Any] | None,
117
+ *,
118
+ strict_production_validation: bool = False,
106
119
  ) -> None:
107
120
  if productions is None:
108
121
  return
@@ -110,6 +123,16 @@ class MigrationPlanner:
110
123
  raise ValueError("PRODUCTIONS must be a list.")
111
124
  auto_class_entries = set()
112
125
  for production in productions:
126
+ report = validate_production_entry(
127
+ production,
128
+ strict=strict_production_validation,
129
+ warn=False,
130
+ )
131
+ if report.has_issues:
132
+ plan["validation"].extend(
133
+ f"{report.production_name}: {issue.to_text()}"
134
+ for issue in report.issues
135
+ )
113
136
  if self._utils._is_production_object(production):
114
137
  plan["productions"].append(production.name)
115
138
  self._add_production_component_entries(
iop/migration/utils.py CHANGED
@@ -18,6 +18,7 @@ from ..messages.persistent import (
18
18
  is_persistent_message_class,
19
19
  register_persistent_message_class,
20
20
  )
21
+ from ..production.validation import validate_production_entry
21
22
  from ..runtime import iris as _iris
22
23
  from ..runtime.environment import remove_sys_path, temporary_sys_path
23
24
  from .io import (
@@ -425,7 +426,12 @@ def filename_to_module(filename) -> str:
425
426
  return module
426
427
 
427
428
 
428
- def migrate(filename=None, mode: str | None = None, namespace: str | None = None):
429
+ def migrate(
430
+ filename=None,
431
+ mode: str | None = None,
432
+ namespace: str | None = None,
433
+ strict_production_validation: bool = False,
434
+ ):
429
435
  """
430
436
  Read the settings.py file and register all the components
431
437
  settings.py file has two dictionaries:
@@ -443,10 +449,19 @@ def migrate(filename=None, mode: str | None = None, namespace: str | None = None
443
449
 
444
450
  try:
445
451
  plan = _build_migration_plan(
446
- settings, path, filename, mode=mode, namespace=namespace
452
+ settings,
453
+ path,
454
+ filename,
455
+ mode=mode,
456
+ namespace=namespace,
457
+ strict_production_validation=strict_production_validation,
447
458
  )
448
459
  print(format_migration_plan(plan))
449
- _register_settings_components(settings, path)
460
+ _register_settings_components(
461
+ settings,
462
+ path,
463
+ strict_production_validation=strict_production_validation,
464
+ )
450
465
  print(
451
466
  format_migration_success(
452
467
  filename or inspect.getfile(settings), namespace=namespace
@@ -457,13 +472,21 @@ def migrate(filename=None, mode: str | None = None, namespace: str | None = None
457
472
 
458
473
 
459
474
  def explain_migration(
460
- filename=None, mode: str | None = None, namespace: str | None = None
475
+ filename=None,
476
+ mode: str | None = None,
477
+ namespace: str | None = None,
478
+ strict_production_validation: bool = False,
461
479
  ):
462
480
  """Return a human-readable migration plan without writing to IRIS."""
463
481
  settings, path = _load_settings(filename)
464
482
  try:
465
483
  plan = _build_migration_plan(
466
- settings, path, filename, mode=mode, namespace=namespace
484
+ settings,
485
+ path,
486
+ filename,
487
+ mode=mode,
488
+ namespace=namespace,
489
+ strict_production_validation=strict_production_validation,
467
490
  )
468
491
  return _format_migration_plan(plan)
469
492
  finally:
@@ -488,6 +511,7 @@ def _build_migration_plan(
488
511
  filename=None,
489
512
  mode: str | None = None,
490
513
  namespace: str | None = None,
514
+ strict_production_validation: bool = False,
491
515
  ):
492
516
  return MigrationPlanner(sys.modules[__name__]).build(
493
517
  settings,
@@ -495,6 +519,7 @@ def _build_migration_plan(
495
519
  filename=filename,
496
520
  mode=mode,
497
521
  namespace=namespace,
522
+ strict_production_validation=strict_production_validation,
498
523
  )
499
524
 
500
525
 
@@ -589,7 +614,12 @@ def _validate_dtl_schema_class(cls, setting_name):
589
614
  )
590
615
 
591
616
 
592
- def _register_settings_components(settings, path):
617
+ def _register_settings_components(
618
+ settings,
619
+ path,
620
+ *,
621
+ strict_production_validation: bool = False,
622
+ ):
593
623
  """Register all components from settings (classes, productions, schemas).
594
624
 
595
625
  Args:
@@ -616,6 +646,7 @@ def _register_settings_components(settings, path):
616
646
  settings.PRODUCTIONS,
617
647
  path,
618
648
  persistent_registry=persistent_registry,
649
+ strict_production_validation=strict_production_validation,
619
650
  )
620
651
  except AttributeError:
621
652
  pass
@@ -835,18 +866,25 @@ def set_productions_settings(
835
866
  production_list,
836
867
  root_path=None,
837
868
  persistent_registry=None,
869
+ strict_production_validation: bool = False,
838
870
  ):
839
871
  """
840
872
  It takes a list of dictionaries and registers the productions
841
873
  """
842
874
  # for each production in the list
843
875
  for production in production_list:
844
- if _is_production_object(production):
876
+ production_is_object = _is_production_object(production)
877
+ if production_is_object:
845
878
  _register_production_object_messages(
846
879
  production,
847
880
  persistent_registry=persistent_registry,
848
881
  )
849
882
  _register_production_object_components(production, root_path)
883
+ validate_production_entry(
884
+ production,
885
+ strict=strict_production_validation,
886
+ warn=True,
887
+ )
850
888
  production = production.to_dict()
851
889
  else:
852
890
  production = copy.deepcopy(production)
@@ -860,6 +898,12 @@ def set_productions_settings(
860
898
  production["Production"] = production.pop(production_name)
861
899
  # handle Items
862
900
  production = handle_items(production, root_path)
901
+ if not production_is_object:
902
+ validate_production_entry(
903
+ production,
904
+ strict=strict_production_validation,
905
+ warn=True,
906
+ )
863
907
  # register the production
864
908
  register_production_definition(production_name, production)
865
909
 
@@ -12,6 +12,10 @@ from .types import ProductionDiffEntry as ProductionDiffEntry
12
12
  from .types import ProductionGraph as ProductionGraph
13
13
  from .types import TargetSetting as TargetSetting
14
14
  from .types import target as target
15
+ from .validation import ProductionValidationError as ProductionValidationError
16
+ from .validation import ProductionValidationIssue as ProductionValidationIssue
17
+ from .validation import ProductionValidationReport as ProductionValidationReport
18
+ from .validation import ProductionValidationWarning as ProductionValidationWarning
15
19
 
16
20
  __all__ = [
17
21
  "ComponentRef",
@@ -23,6 +27,10 @@ __all__ = [
23
27
  "ProductionDiff",
24
28
  "ProductionDiffEntry",
25
29
  "ProductionGraph",
30
+ "ProductionValidationError",
31
+ "ProductionValidationIssue",
32
+ "ProductionValidationReport",
33
+ "ProductionValidationWarning",
26
34
  "TargetSetting",
27
35
  "resolve_target",
28
36
  "target",
iop/production/common.py CHANGED
@@ -3,6 +3,25 @@ from __future__ import annotations
3
3
  import importlib
4
4
  from typing import Any
5
5
 
6
+ PRODUCTION_SETTING_FIELDS: dict[str, tuple[str, Any]] = {
7
+ "shutdown_timeout": ("ShutdownTimeout", 120),
8
+ "update_timeout": ("UpdateTimeout", 10),
9
+ "alert_notification_manager": ("AlertNotificationManager", ""),
10
+ "alert_notification_operation": ("AlertNotificationOperation", ""),
11
+ "alert_notification_recipients": ("AlertNotificationRecipients", ""),
12
+ "alert_action_window": ("AlertActionWindow", 60),
13
+ }
14
+
15
+ PRODUCTION_SETTING_NAMES: dict[str, str] = {
16
+ field_name: iris_name
17
+ for field_name, (iris_name, _default) in PRODUCTION_SETTING_FIELDS.items()
18
+ }
19
+
20
+ PRODUCTION_SETTING_FIELDS_BY_IRIS: dict[str, str] = {
21
+ iris_name: field_name
22
+ for field_name, (iris_name, _default) in PRODUCTION_SETTING_FIELDS.items()
23
+ }
24
+
6
25
 
7
26
  def _bool_text(value: bool | str) -> str:
8
27
  if isinstance(value, bool):
iop/production/diff.py CHANGED
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import json
4
4
  from typing import TYPE_CHECKING, Any
5
5
 
6
- from .common import _bool_text, _text_value
6
+ from .common import PRODUCTION_SETTING_FIELDS, _bool_text, _text_value
7
7
  from .types import (
8
8
  GraphEdge,
9
9
  ProductionDiff,
@@ -66,12 +66,19 @@ def _diff_warnings(desired: Production, current: Production) -> list[str]:
66
66
 
67
67
 
68
68
  def _production_signature(production: Production) -> dict[str, Any]:
69
- return {
69
+ signature = {
70
70
  "testing_enabled": _bool_text(production.testing_enabled),
71
71
  "log_general_trace_events": _bool_text(production.log_general_trace_events),
72
72
  "actor_pool_size": _text_value(production.actor_pool_size),
73
73
  "description": production.description,
74
74
  }
75
+ signature.update(
76
+ {
77
+ field_name: _text_value(getattr(production, field_name))
78
+ for field_name in PRODUCTION_SETTING_FIELDS
79
+ }
80
+ )
81
+ return signature
75
82
 
76
83
 
77
84
  def _item_signatures(production: Production) -> dict[str, dict[str, Any]]:
iop/production/import_.py CHANGED
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from typing import Any
4
4
 
5
- from .common import _text_value
5
+ from .common import PRODUCTION_SETTING_FIELDS_BY_IRIS, _text_value
6
6
 
7
7
 
8
8
  def _as_list(value: Any) -> list[Any]:
@@ -54,6 +54,21 @@ def _split_settings(
54
54
  return host_settings, adapter_settings, other_settings
55
55
 
56
56
 
57
+ def _split_production_settings(settings: Any) -> dict[str, Any]:
58
+ values: dict[str, Any] = {}
59
+ for setting in _as_list(settings):
60
+ if not isinstance(setting, dict):
61
+ continue
62
+ target = setting.get("@Target", "")
63
+ if target not in ("", "Production"):
64
+ continue
65
+ iris_name = str(setting.get("@Name", ""))
66
+ field_name = PRODUCTION_SETTING_FIELDS_BY_IRIS.get(iris_name)
67
+ if field_name:
68
+ values[field_name] = setting.get("#text", "")
69
+ return values
70
+
71
+
57
72
  def _normalize_connections(
58
73
  connections: Any,
59
74
  ) -> tuple[dict[str, list[dict[str, Any]]], set[str], list[str]]:
iop/production/model.py CHANGED
@@ -5,6 +5,7 @@ from typing import Any
5
5
  from ..runtime.protocol import DirectorProtocol as _DirectorProtocol
6
6
  from . import actions as _actions
7
7
  from .common import (
8
+ PRODUCTION_SETTING_FIELDS,
8
9
  _adapter_type_from_component_class,
9
10
  _apply_settings_update,
10
11
  _auto_proxy_class_name,
@@ -46,6 +47,20 @@ class Production:
46
47
  log_general_trace_events: bool | str = False,
47
48
  actor_pool_size: int | str = 2,
48
49
  description: str = "",
50
+ shutdown_timeout: int | str = PRODUCTION_SETTING_FIELDS["shutdown_timeout"][1],
51
+ update_timeout: int | str = PRODUCTION_SETTING_FIELDS["update_timeout"][1],
52
+ alert_notification_manager: str = PRODUCTION_SETTING_FIELDS[
53
+ "alert_notification_manager"
54
+ ][1],
55
+ alert_notification_operation: str = PRODUCTION_SETTING_FIELDS[
56
+ "alert_notification_operation"
57
+ ][1],
58
+ alert_notification_recipients: str = PRODUCTION_SETTING_FIELDS[
59
+ "alert_notification_recipients"
60
+ ][1],
61
+ alert_action_window: int | str = PRODUCTION_SETTING_FIELDS[
62
+ "alert_action_window"
63
+ ][1],
49
64
  namespace: str | None = None,
50
65
  director: _DirectorProtocol | None = None,
51
66
  ):
@@ -54,6 +69,12 @@ class Production:
54
69
  self.log_general_trace_events = log_general_trace_events
55
70
  self.actor_pool_size = actor_pool_size
56
71
  self.description = description
72
+ self.shutdown_timeout = shutdown_timeout
73
+ self.update_timeout = update_timeout
74
+ self.alert_notification_manager = alert_notification_manager
75
+ self.alert_notification_operation = alert_notification_operation
76
+ self.alert_notification_recipients = alert_notification_recipients
77
+ self.alert_action_window = alert_action_window
57
78
  self.namespace = namespace
58
79
  self._director = director
59
80
  self._items: list[ComponentRef] = []
@@ -80,6 +101,36 @@ class Production:
80
101
  self.description = text
81
102
  return self
82
103
 
104
+ def timeouts(
105
+ self,
106
+ *,
107
+ shutdown: int | str | None = None,
108
+ update: int | str | None = None,
109
+ ) -> Production:
110
+ if shutdown is not None:
111
+ self.shutdown_timeout = shutdown
112
+ if update is not None:
113
+ self.update_timeout = update
114
+ return self
115
+
116
+ def alerting(
117
+ self,
118
+ *,
119
+ manager: str | None = None,
120
+ operation: str | None = None,
121
+ recipients: str | None = None,
122
+ action_window: int | str | None = None,
123
+ ) -> Production:
124
+ if manager is not None:
125
+ self.alert_notification_manager = manager
126
+ if operation is not None:
127
+ self.alert_notification_operation = operation
128
+ if recipients is not None:
129
+ self.alert_notification_recipients = recipients
130
+ if action_window is not None:
131
+ self.alert_action_window = action_window
132
+ return self
133
+
83
134
  def in_namespace(self, namespace: str | None) -> Production:
84
135
  self.namespace = namespace
85
136
  return self
@@ -555,6 +606,11 @@ class Production:
555
606
  def message_registrations(self) -> tuple[PersistentMessageRegistration, ...]:
556
607
  return tuple(self._messages)
557
608
 
609
+ def validate(self, *, strict: bool = False):
610
+ from .validation import validate_production
611
+
612
+ return validate_production(self, strict=strict, warn=not strict)
613
+
558
614
  def start(self, detach: bool = True) -> None:
559
615
  _actions.start(self, detach=detach)
560
616
 
@@ -12,6 +12,7 @@ from .import_ import (
12
12
  _normalize_runtime_item_metadata,
13
13
  _production_payload,
14
14
  _setting_targets,
15
+ _split_production_settings,
15
16
  _split_settings,
16
17
  )
17
18
 
@@ -26,12 +27,25 @@ def production_from_dict(
26
27
  director: Any = None,
27
28
  ):
28
29
  production_name, production_data = _production_payload(data)
30
+ production_settings = _split_production_settings(production_data.get("Setting"))
29
31
  production = production_cls(
30
32
  production_name,
31
33
  testing_enabled=production_data.get("@TestingEnabled", False),
32
34
  log_general_trace_events=production_data.get("@LogGeneralTraceEvents", False),
33
35
  actor_pool_size=production_data.get("ActorPoolSize", 2),
34
36
  description=production_data.get("Description", ""),
37
+ shutdown_timeout=production_settings.get("shutdown_timeout", 120),
38
+ update_timeout=production_settings.get("update_timeout", 10),
39
+ alert_notification_manager=production_settings.get(
40
+ "alert_notification_manager", ""
41
+ ),
42
+ alert_notification_operation=production_settings.get(
43
+ "alert_notification_operation", ""
44
+ ),
45
+ alert_notification_recipients=production_settings.get(
46
+ "alert_notification_recipients", ""
47
+ ),
48
+ alert_action_window=production_settings.get("alert_action_window", 60),
35
49
  namespace=namespace,
36
50
  director=director,
37
51
  )
@@ -9,7 +9,7 @@ from typing import Any
9
9
 
10
10
  from pydantic import BaseModel
11
11
 
12
- from .common import _bool_text, _text_value
12
+ from .common import PRODUCTION_SETTING_FIELDS, _bool_text, _text_value
13
13
  from .types import GraphNode, ProductionGraph
14
14
 
15
15
 
@@ -21,11 +21,29 @@ def production_to_dict(production) -> dict[str, Any]:
21
21
  "Description": production.description,
22
22
  "ActorPoolSize": _text_value(production.actor_pool_size),
23
23
  }
24
+ production_settings = _production_settings_to_iris(production)
25
+ if production_settings:
26
+ data["Setting"] = production_settings
24
27
  if production._items:
25
28
  data["Item"] = [item.to_dict() for item in production._items]
26
29
  return {production.name: data}
27
30
 
28
31
 
32
+ def _production_settings_to_iris(production) -> list[dict[str, str]]:
33
+ settings: list[dict[str, str]] = []
34
+ for field_name, (iris_name, default) in PRODUCTION_SETTING_FIELDS.items():
35
+ value = getattr(production, field_name)
36
+ if _text_value(value) == _text_value(default):
37
+ continue
38
+ settings.append(
39
+ {
40
+ "@Name": iris_name,
41
+ "#text": _text_value(value),
42
+ }
43
+ )
44
+ return settings
45
+
46
+
29
47
  def production_to_xml(production) -> str:
30
48
  from ..migration import utils as migration_utils
31
49
 
@@ -78,6 +96,24 @@ def _production_constructor_lines(production) -> list[str]:
78
96
  ),
79
97
  ("actor_pool_size", _int_literal(production.actor_pool_size), 2),
80
98
  ("description", production.description, ""),
99
+ ("shutdown_timeout", _int_literal(production.shutdown_timeout), 120),
100
+ ("update_timeout", _int_literal(production.update_timeout), 10),
101
+ (
102
+ "alert_notification_manager",
103
+ production.alert_notification_manager,
104
+ "",
105
+ ),
106
+ (
107
+ "alert_notification_operation",
108
+ production.alert_notification_operation,
109
+ "",
110
+ ),
111
+ (
112
+ "alert_notification_recipients",
113
+ production.alert_notification_recipients,
114
+ "",
115
+ ),
116
+ ("alert_action_window", _int_literal(production.alert_action_window), 60),
81
117
  ]
82
118
  rendered = [
83
119
  (name, value)
@@ -0,0 +1,453 @@
1
+ from __future__ import annotations
2
+
3
+ import warnings
4
+ from dataclasses import dataclass
5
+ from typing import Any
6
+
7
+ from .common import PRODUCTION_SETTING_FIELDS_BY_IRIS, PRODUCTION_SETTING_NAMES
8
+ from .import_ import _as_list, _production_payload, _split_settings
9
+
10
+ _PRODUCTION_PUBLIC_FIELDS = {
11
+ "name",
12
+ "testing_enabled",
13
+ "log_general_trace_events",
14
+ "actor_pool_size",
15
+ "description",
16
+ "shutdown_timeout",
17
+ "update_timeout",
18
+ "alert_notification_manager",
19
+ "alert_notification_operation",
20
+ "alert_notification_recipients",
21
+ "alert_action_window",
22
+ "namespace",
23
+ }
24
+
25
+ _PRODUCTION_DICT_KEYS = {
26
+ "@Name",
27
+ "@TestingEnabled",
28
+ "@LogGeneralTraceEvents",
29
+ "Description",
30
+ "ActorPoolSize",
31
+ "Setting",
32
+ "Item",
33
+ }
34
+
35
+ _SETTING_NAME_ALIASES = {
36
+ "target_config_name": "TargetConfigName",
37
+ "target_config_names": "TargetConfigNames",
38
+ }
39
+
40
+ _PRODUCTION_SETTING_NAME_ALIASES = {
41
+ **PRODUCTION_SETTING_NAMES,
42
+ **{iris_name: iris_name for iris_name in PRODUCTION_SETTING_FIELDS_BY_IRIS},
43
+ }
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class ProductionValidationIssue:
48
+ kind: str
49
+ path: str
50
+ message: str
51
+ suggestion: str = ""
52
+
53
+ def to_dict(self) -> dict[str, str]:
54
+ data = {
55
+ "kind": self.kind,
56
+ "path": self.path,
57
+ "message": self.message,
58
+ }
59
+ if self.suggestion:
60
+ data["suggestion"] = self.suggestion
61
+ return data
62
+
63
+ def to_text(self) -> str:
64
+ text = f"{self.kind} {self.path}: {self.message}"
65
+ if self.suggestion:
66
+ return f"{text} {self.suggestion}"
67
+ return text
68
+
69
+
70
+ @dataclass(frozen=True)
71
+ class ProductionValidationReport:
72
+ production_name: str
73
+ issues: tuple[ProductionValidationIssue, ...] = ()
74
+
75
+ @property
76
+ def has_issues(self) -> bool:
77
+ return bool(self.issues)
78
+
79
+ def to_dict(self) -> dict[str, Any]:
80
+ return {
81
+ "production": self.production_name,
82
+ "has_issues": self.has_issues,
83
+ "issues": [issue.to_dict() for issue in self.issues],
84
+ }
85
+
86
+ def to_text(self) -> str:
87
+ lines = [f"Production validation: {self.production_name}"]
88
+ if not self.issues:
89
+ lines.append(" no issues")
90
+ else:
91
+ lines.extend(f" {issue.to_text()}" for issue in self.issues)
92
+ return "\n".join(lines)
93
+
94
+ def __str__(self) -> str:
95
+ return self.to_text()
96
+
97
+
98
+ class ProductionValidationWarning(UserWarning):
99
+ pass
100
+
101
+
102
+ class ProductionValidationError(ValueError):
103
+ def __init__(self, report: ProductionValidationReport):
104
+ self.report = report
105
+ super().__init__(report.to_text())
106
+
107
+
108
+ def validate_production(
109
+ production: Any,
110
+ *,
111
+ strict: bool = False,
112
+ warn: bool = True,
113
+ ) -> ProductionValidationReport:
114
+ report = _build_production_object_report(production)
115
+ return _finalize_report(report, strict=strict, warn=warn)
116
+
117
+
118
+ def validate_production_entry(
119
+ production: Any,
120
+ *,
121
+ strict: bool = False,
122
+ warn: bool = True,
123
+ ) -> ProductionValidationReport:
124
+ if _is_production_object(production):
125
+ report = _build_production_object_report(production)
126
+ else:
127
+ report = _build_production_dict_report(production)
128
+ return _finalize_report(report, strict=strict, warn=warn)
129
+
130
+
131
+ def _finalize_report(
132
+ report: ProductionValidationReport,
133
+ *,
134
+ strict: bool,
135
+ warn: bool,
136
+ ) -> ProductionValidationReport:
137
+ if not report.has_issues:
138
+ return report
139
+ if strict:
140
+ raise ProductionValidationError(report)
141
+ if warn:
142
+ for issue in report.issues:
143
+ warnings.warn(issue.to_text(), ProductionValidationWarning, stacklevel=3)
144
+ return report
145
+
146
+
147
+ def _build_production_object_report(production: Any) -> ProductionValidationReport:
148
+ production_name = str(getattr(production, "name", "Production"))
149
+ issues: list[ProductionValidationIssue] = []
150
+ issues.extend(_unknown_public_attrs(production))
151
+
152
+ for item in getattr(production, "items", ()):
153
+ issues.extend(_validate_component_ref(item))
154
+
155
+ return ProductionValidationReport(production_name, tuple(issues))
156
+
157
+
158
+ def _unknown_public_attrs(production: Any) -> list[ProductionValidationIssue]:
159
+ issues = []
160
+ for attr in sorted(getattr(production, "__dict__", {})):
161
+ if attr.startswith("_") or attr in _PRODUCTION_PUBLIC_FIELDS:
162
+ continue
163
+ issues.append(
164
+ ProductionValidationIssue(
165
+ kind="production",
166
+ path=f"production.{attr}",
167
+ message=(
168
+ "Unknown public Production attribute. Only documented "
169
+ "Production fields are serialized."
170
+ ),
171
+ )
172
+ )
173
+ return issues
174
+
175
+
176
+ def _validate_component_ref(item: Any) -> list[ProductionValidationIssue]:
177
+ issues: list[ProductionValidationIssue] = []
178
+ item_name = str(getattr(item, "name", ""))
179
+ component_class = getattr(item, "component_class", None)
180
+ class_name = str(getattr(item, "class_name", "") or "")
181
+
182
+ issues.extend(
183
+ _validate_settings(
184
+ settings=dict(getattr(item, "host_settings", {}) or {}),
185
+ path_prefix=f"items.{item_name}.settings.Host",
186
+ local_class=component_class,
187
+ iris_class_name=class_name,
188
+ )
189
+ )
190
+
191
+ adapter_settings = dict(getattr(item, "adapter_settings", {}) or {})
192
+ if str(getattr(item, "kind", "")) == "process" and adapter_settings:
193
+ issues.append(
194
+ ProductionValidationIssue(
195
+ kind="setting",
196
+ path=f"items.{item_name}.settings.Adapter",
197
+ message="Business process items do not support adapter settings.",
198
+ )
199
+ )
200
+
201
+ adapter_class = getattr(item, "adapter_class", None)
202
+ adapter_class_name = str(getattr(item, "adapter_class_name", "") or "")
203
+ issues.extend(
204
+ _validate_settings(
205
+ settings=adapter_settings,
206
+ path_prefix=f"items.{item_name}.settings.Adapter",
207
+ local_class=adapter_class,
208
+ iris_class_name=adapter_class_name,
209
+ host_class_name=class_name,
210
+ )
211
+ )
212
+ return issues
213
+
214
+
215
+ def _build_production_dict_report(data: Any) -> ProductionValidationReport:
216
+ issues: list[ProductionValidationIssue] = []
217
+ try:
218
+ production_name, production_data = _production_payload(data)
219
+ except Exception:
220
+ return ProductionValidationReport("Production", tuple(issues))
221
+
222
+ for key in sorted(set(production_data) - _PRODUCTION_DICT_KEYS):
223
+ issues.append(
224
+ ProductionValidationIssue(
225
+ kind="production",
226
+ path=f"production.{key}",
227
+ message="Unknown production dictionary key.",
228
+ )
229
+ )
230
+
231
+ issues.extend(_validate_production_settings(production_data.get("Setting")))
232
+ for item in _as_list(production_data.get("Item", [])):
233
+ if isinstance(item, dict):
234
+ issues.extend(_validate_dict_item(item))
235
+
236
+ return ProductionValidationReport(str(production_name), tuple(issues))
237
+
238
+
239
+ def _validate_production_settings(settings: Any) -> list[ProductionValidationIssue]:
240
+ issues: list[ProductionValidationIssue] = []
241
+ known_iris_names = set(PRODUCTION_SETTING_FIELDS_BY_IRIS)
242
+ for setting in _as_list(settings):
243
+ if not isinstance(setting, dict):
244
+ continue
245
+ target = setting.get("@Target", "")
246
+ if target not in ("", "Production"):
247
+ continue
248
+ setting_name = str(setting.get("@Name", ""))
249
+ if not setting_name or setting_name in known_iris_names:
250
+ continue
251
+ alias = _PRODUCTION_SETTING_NAME_ALIASES.get(setting_name)
252
+ if alias in known_iris_names:
253
+ issues.append(
254
+ ProductionValidationIssue(
255
+ kind="production",
256
+ path=f"production.settings.{setting_name}",
257
+ message=(
258
+ "Production setting name is accepted as a known alias for "
259
+ "validation, but IoP will emit the original name."
260
+ ),
261
+ suggestion=f"Use {alias!r}.",
262
+ )
263
+ )
264
+ continue
265
+ issues.append(
266
+ ProductionValidationIssue(
267
+ kind="production",
268
+ path=f"production.settings.{setting_name}",
269
+ message="Unknown production-level setting.",
270
+ )
271
+ )
272
+ return issues
273
+
274
+
275
+ def _validate_dict_item(item: dict[str, Any]) -> list[ProductionValidationIssue]:
276
+ item_name = str(item.get("@Name", ""))
277
+ class_ref = item.get("@ClassName", "")
278
+ local_class = class_ref if isinstance(class_ref, type) else None
279
+ iris_class_name = "" if isinstance(class_ref, type) else str(class_ref or "")
280
+ host_settings, adapter_settings, _other_settings = _split_settings(
281
+ item.get("Setting", [])
282
+ )
283
+
284
+ issues = _validate_settings(
285
+ settings=host_settings,
286
+ path_prefix=f"items.{item_name}.settings.Host",
287
+ local_class=local_class,
288
+ iris_class_name=iris_class_name,
289
+ )
290
+ issues.extend(
291
+ _validate_settings(
292
+ settings=adapter_settings,
293
+ path_prefix=f"items.{item_name}.settings.Adapter",
294
+ local_class=None,
295
+ iris_class_name="",
296
+ host_class_name=iris_class_name,
297
+ )
298
+ )
299
+ return issues
300
+
301
+
302
+ def _validate_settings(
303
+ *,
304
+ settings: dict[str, Any],
305
+ path_prefix: str,
306
+ local_class: type | None,
307
+ iris_class_name: str,
308
+ host_class_name: str = "",
309
+ ) -> list[ProductionValidationIssue]:
310
+ if not settings:
311
+ return []
312
+ if local_class is not None:
313
+ local_names = _local_setting_names(local_class)
314
+ if local_names is not None:
315
+ return _validate_names_with_known_set(
316
+ settings,
317
+ path_prefix=path_prefix,
318
+ known_names=local_names,
319
+ )
320
+
321
+ iris_target_class = iris_class_name or _adapter_class_name_from_host(host_class_name)
322
+ if iris_target_class:
323
+ report = _validate_names_with_iris_dictionary(
324
+ settings,
325
+ path_prefix=path_prefix,
326
+ iris_class_name=iris_target_class,
327
+ )
328
+ if report is not None:
329
+ return report
330
+ return []
331
+
332
+
333
+ def _local_setting_names(cls: type) -> set[str] | None:
334
+ getter = getattr(cls, "_get_properties", None)
335
+ if not callable(getter):
336
+ return None
337
+ try:
338
+ return {str(prop[0]) for prop in getter() if prop}
339
+ except Exception:
340
+ return None
341
+
342
+
343
+ def _validate_names_with_known_set(
344
+ settings: dict[str, Any],
345
+ *,
346
+ path_prefix: str,
347
+ known_names: set[str],
348
+ ) -> list[ProductionValidationIssue]:
349
+ issues: list[ProductionValidationIssue] = []
350
+ for setting_name in sorted(settings):
351
+ if setting_name.startswith("%") or setting_name in known_names:
352
+ continue
353
+ alias = _SETTING_NAME_ALIASES.get(setting_name)
354
+ if alias in known_names:
355
+ issues.append(_setting_alias_issue(path_prefix, setting_name, alias))
356
+ continue
357
+ issues.append(_unknown_setting_issue(path_prefix, setting_name))
358
+ return issues
359
+
360
+
361
+ def _validate_names_with_iris_dictionary(
362
+ settings: dict[str, Any],
363
+ *,
364
+ path_prefix: str,
365
+ iris_class_name: str,
366
+ ) -> list[ProductionValidationIssue] | None:
367
+ iris = _iris_module()
368
+ if iris is None or not _iris_class_exists(iris, iris_class_name):
369
+ return None
370
+
371
+ issues: list[ProductionValidationIssue] = []
372
+ for setting_name in sorted(settings):
373
+ if _iris_property_exists(iris, iris_class_name, setting_name):
374
+ continue
375
+ alias = _SETTING_NAME_ALIASES.get(setting_name)
376
+ if alias and _iris_property_exists(iris, iris_class_name, alias):
377
+ issues.append(_setting_alias_issue(path_prefix, setting_name, alias))
378
+ continue
379
+ issues.append(_unknown_setting_issue(path_prefix, setting_name))
380
+ return issues
381
+
382
+
383
+ def _adapter_class_name_from_host(host_class_name: str) -> str:
384
+ if not host_class_name:
385
+ return ""
386
+ iris = _iris_module()
387
+ if iris is None:
388
+ return ""
389
+ try:
390
+ parameter = iris._Dictionary.CompiledParameter._OpenId(
391
+ f"{host_class_name}||ADAPTER"
392
+ )
393
+ return str(getattr(parameter, "Default", "") or "")
394
+ except Exception:
395
+ return ""
396
+
397
+
398
+ def _iris_module():
399
+ try:
400
+ import iris # type: ignore
401
+ except Exception:
402
+ return None
403
+ return iris
404
+
405
+
406
+ def _iris_class_exists(iris: Any, class_name: str) -> bool:
407
+ try:
408
+ return bool(iris._Dictionary.CompiledClass._ExistsId(class_name))
409
+ except Exception:
410
+ return False
411
+
412
+
413
+ def _iris_property_exists(iris: Any, class_name: str, property_name: str) -> bool:
414
+ try:
415
+ return bool(
416
+ iris._Dictionary.CompiledProperty._ExistsId(
417
+ f"{class_name}||{property_name}"
418
+ )
419
+ )
420
+ except Exception:
421
+ return False
422
+
423
+
424
+ def _setting_alias_issue(
425
+ path_prefix: str,
426
+ setting_name: str,
427
+ alias: str,
428
+ ) -> ProductionValidationIssue:
429
+ return ProductionValidationIssue(
430
+ kind="setting",
431
+ path=f"{path_prefix}.{setting_name}",
432
+ message=(
433
+ "Setting name is accepted as a known alias for validation, "
434
+ "but IoP will emit the original name."
435
+ ),
436
+ suggestion=f"Use {alias!r}.",
437
+ )
438
+
439
+
440
+ def _unknown_setting_issue(path_prefix: str, setting_name: str) -> ProductionValidationIssue:
441
+ return ProductionValidationIssue(
442
+ kind="setting",
443
+ path=f"{path_prefix}.{setting_name}",
444
+ message="Unknown setting name.",
445
+ )
446
+
447
+
448
+ def _is_production_object(value: Any) -> bool:
449
+ return (
450
+ hasattr(value, "to_dict")
451
+ and hasattr(value, "component_registrations")
452
+ and hasattr(value, "name")
453
+ )
iop/runtime/local.py CHANGED
@@ -114,8 +114,17 @@ class _LocalDirector(_DirectorProtocol):
114
114
  # Migrate
115
115
  # ------------------------------------------------------------------
116
116
 
117
- def migrate(self, path: str) -> None:
118
- _migration_utils.migrate(path, mode="LOCAL", namespace=self.namespace)
117
+ def migrate(
118
+ self,
119
+ path: str,
120
+ strict_production_validation: bool = False,
121
+ ) -> None:
122
+ _migration_utils.migrate(
123
+ path,
124
+ mode="LOCAL",
125
+ namespace=self.namespace,
126
+ strict_production_validation=strict_production_validation,
127
+ )
119
128
 
120
129
  # ------------------------------------------------------------------
121
130
  # Metadata
iop/runtime/protocol.py CHANGED
@@ -75,7 +75,11 @@ class DirectorProtocol(Protocol):
75
75
  # Migrate
76
76
  # ------------------------------------------------------------------
77
77
 
78
- def migrate(self, path: str) -> None: ...
78
+ def migrate(
79
+ self,
80
+ path: str,
81
+ strict_production_validation: bool = False,
82
+ ) -> None: ...
79
83
 
80
84
  # ------------------------------------------------------------------
81
85
  # Metadata
@@ -227,8 +227,16 @@ class _RemoteDirector(_RemoteClient, _DirectorProtocol):
227
227
  # Migrate
228
228
  # ------------------------------------------------------------------
229
229
 
230
- def migrate(self, path: str) -> None:
231
- upload_migration(self, path)
230
+ def migrate(
231
+ self,
232
+ path: str,
233
+ strict_production_validation: bool = False,
234
+ ) -> None:
235
+ upload_migration(
236
+ self,
237
+ path,
238
+ strict_production_validation=strict_production_validation,
239
+ )
232
240
 
233
241
  # ------------------------------------------------------------------
234
242
  # Init / setup — uploads .cls files via the Atelier API
@@ -6,7 +6,12 @@ import os
6
6
  import requests
7
7
 
8
8
 
9
- def upload_migration(client, path: str) -> None:
9
+ def upload_migration(
10
+ client,
11
+ path: str,
12
+ *,
13
+ strict_production_validation: bool = False,
14
+ ) -> None:
10
15
  """Upload .py and .cls files from *path*'s folder to remote IRIS."""
11
16
  folder = os.path.dirname(path)
12
17
 
@@ -38,6 +43,7 @@ def upload_migration(client, path: str) -> None:
38
43
  "package": package,
39
44
  "remote_folder": remote_folder,
40
45
  "settings_file": settings_file,
46
+ "strict_production_validation": bool(strict_production_validation),
41
47
  "body": body,
42
48
  }
43
49
  resp = requests.put(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iris_pex_embedded_python
3
- Version: 4.0.0b3
3
+ Version: 4.0.0b4
4
4
  Summary: Iris Interoperability based on Embedded Python
5
5
  Author-email: grongier <guillaume.rongier@intersystems.com>
6
6
  License: MIT License
@@ -1,10 +1,10 @@
1
- iop/__init__.py,sha256=VCdKplz26dn-7R6PXHXliy35PC4-qhRO6ASMOR2Fur4,3532
1
+ iop/__init__.py,sha256=JCkmGv50W1i4oi7CKrvLXyQ88-lv-2y-8SJ_05BOsPA,4001
2
2
  iop/__main__.py,sha256=2rUNM7vaYK0W57Jj72rm7GORyIFLMXKwPq3QNLwYBmY,100
3
3
  iop/cli/__init__.py,sha256=Y78pBdA-ULzq1c_9xQZsFBlzm3x5xgxSzkvrH5DoM7I,31
4
4
  iop/cli/formatting.py,sha256=OLgHA1d8_paAuINQyKkYgihWMLoBhPn7kSXW5whRlIs,1606
5
- iop/cli/main.py,sha256=LLepnBOH2sj9_taHulcXvW-OhTsNfUVib6pj7JT_CzU,12578
6
- iop/cli/parser.py,sha256=vufFtEOEOhYudG-p6h5vHJ1sqq6LL7yjNNQgT-_wJhE,4702
7
- iop/cli/types.py,sha256=uRb2z3TwRBBNC-iuHn8SixUoN2zKW7ey57vXgIAWA3k,1296
5
+ iop/cli/main.py,sha256=FzE86PbdIrJ8zVIIcsTRD7-v5_zdFUisnQ5Rs5hbQH8,13197
6
+ iop/cli/parser.py,sha256=HWn3mxndR8Fw9HrnjtSP-ZMTps9X1cbQWCcye7NPIug,4878
7
+ iop/cli/types.py,sha256=7GxUy2KHw_LNOWAQ2-EalKOikIDQFiKjMmEEmlWeOB8,1343
8
8
  iop/cls/IOP/BusinessOperation.cls,sha256=NlvbNtm1ZFZmHaMX_9FMKoptY-hQMq5jYN1nLQwvYJw,936
9
9
  iop/cls/IOP/BusinessProcess.cls,sha256=XJxzbiV0xokzRm-iI2Be5UIJLE3MlXr7W3WS_LkOCYs,3363
10
10
  iop/cls/IOP/BusinessService.cls,sha256=fplKrbQgA7cQgjKIqDR2IK2iD1iNHmT-QvWrozhE4n4,1189
@@ -16,7 +16,7 @@ iop/cls/IOP/OutboundAdapter.cls,sha256=OQoGFHUy2qV_kcsShTlWGOngDrdH5dhwux4eopZyI
16
16
  iop/cls/IOP/PickleMessage.cls,sha256=S3y7AClQ8mAILjxPuHdCjGosBZYzGbUQ5WTv4mYPNMQ,1673
17
17
  iop/cls/IOP/Projection.cls,sha256=AZgbfpbEk02llhyIwrSw0M3QMcQNcjhjY3_vU_yx8FU,1315
18
18
  iop/cls/IOP/Test.cls,sha256=zvlCZJfOCmSFdmi-ZQHWDGYmqZAgRspaJj1j0r_IxJQ,2017
19
- iop/cls/IOP/Utils.cls,sha256=tms1rC7gkRRj2Fb-nJtUBCJKXfqztxA1RQH5jVtzlPU,30975
19
+ iop/cls/IOP/Utils.cls,sha256=SZOKKVaJEUp3Y4xkt1OIWw2ExsjNZoWCBsySP7CjqoY,31095
20
20
  iop/cls/IOP/Wrapper.cls,sha256=ydlNpqAekoH_XdGk0ZpspX1pxgv__E00Xf57F8oxsqs,1624
21
21
  iop/cls/IOP/Duplex/Operation.cls,sha256=K_fmgeLjPZQbHgNrc0kd6DUQoW0fDn1VHQjJxHo95Zk,525
22
22
  iop/cls/IOP/Duplex/Process.cls,sha256=xbefZ4z84a_IUhavWN6P_gZBzqkdJ5XRTXxro6iDvAg,6986
@@ -33,7 +33,7 @@ iop/cls/IOP/PrivateSession/Message/Poll.cls,sha256=z3ALYmGYQasTcyYNyBeoHzJdNXI4n
33
33
  iop/cls/IOP/PrivateSession/Message/Start.cls,sha256=RsJLrhglrONBDGT0RqW2K9MDXa98vMYcxJfgeD8lhAE,943
34
34
  iop/cls/IOP/PrivateSession/Message/Stop.cls,sha256=7g3gKFUjNg0WXBLuWnj-VnCs5G6hSE09YTzGEp0zbGc,1390
35
35
  iop/cls/IOP/Service/Remote/Handler.cls,sha256=JfsXse2jvoVvQfW8_rVEt2DCQJ9SVqReCcOUngOkpzE,938
36
- iop/cls/IOP/Service/Remote/Rest/v1.cls,sha256=IIv38En6Jg-Hm9QrePwqd_8OOo0WRR7UdhrOgcQnGrc,20154
36
+ iop/cls/IOP/Service/Remote/Rest/v1.cls,sha256=33FEBVZhHqysYaLL7l0314kJu1eZXyJlvAk2l4Q-B5g,20277
37
37
  iop/components/__init__.py,sha256=uRF_YCn_ukkgzFrW0nLLMmC2jJ1dMk-ME7HX9EEEVic,880
38
38
  iop/components/async_request.py,sha256=AU36pf6NIQMM9ZYR5h6LeQry0EfXXqDaKAjsP16_hWE,2411
39
39
  iop/components/business_host.py,sha256=P2gna1tkvP2fKtzhHutny11Qdf6DYH1c4CKnNGBlNd0,10243
@@ -59,35 +59,36 @@ iop/messages/serialization.py,sha256=BW0o-7GaceMW7RMOzOJ8g9rKEv28NYnYFwtTLmAf7Yg
59
59
  iop/messages/validation.py,sha256=cbq2VaaWTcqesmV-DDwReHh8peynvEMw9h0JNf4q9kQ,1530
60
60
  iop/migration/__init__.py,sha256=wp-RvjLKYW_Pi2AJLfiWe1bACxENzEgjXtP9UMNiSVY,36
61
61
  iop/migration/io.py,sha256=hAehtApFZFgGzjzgwBmDMxdA1xoRhE-okOcEzvgFdQ4,1822
62
- iop/migration/plans.py,sha256=pG2hQNYe2Ea-ubi-kE_8GcyWpeFSwfxhwvGs6kef1o0,5947
63
- iop/migration/utils.py,sha256=_Gg-jwQQACxKUj5zNjneMfheY6ARFL_rv_KaEIoyzUI,39578
64
- iop/production/__init__.py,sha256=kfWCJe4-JSWo3odYRw9TLNYoNbal-IFmUNoMmHcETP8,903
62
+ iop/migration/plans.py,sha256=oj0sNLMBtWZBCoVEoP82RagIFumfTPqmGylDUxGLJNE,6738
63
+ iop/migration/utils.py,sha256=s2CoqWj4HeW8KrKTaQZGBKH4ZMku3Zg8eolNZvGKMq0,40817
64
+ iop/production/__init__.py,sha256=bVFLRI4lvhbUlKApRvvSqM6pVkOsBuofRrzuEIBT6nY,1360
65
65
  iop/production/actions.py,sha256=h95K9O6N8S2thV0dYEKOWxo5uPhfvgALZfB6G2jrUt8,7992
66
- iop/production/common.py,sha256=_PLPv_n2z-6Q1_BUerWL1Cto_KLZ59I5DLcB6y_vAno,2230
66
+ iop/production/common.py,sha256=RopwhFJT7jP15a4iBvMWipwYwIAV6A0d0UsoxWLo-is,2969
67
67
  iop/production/component.py,sha256=uRCQZwlPNFbAEaWgDtbDHUdf4VD8A0R1sJhLTj8kHcA,7988
68
- iop/production/diff.py,sha256=w1eR-9TiNCTCrY10KlMUjLmOVZH3PM3evrEGNor7Jcg,9742
69
- iop/production/import_.py,sha256=8P1cZ8rMUSGorzhWJRAVT3x1NAscXqUHC7104DxxYdI,9300
68
+ iop/production/diff.py,sha256=iVJeixEZzy-TaK7b7wdR5wzO5dyIGr2KPVsN0pf2dp4,9968
69
+ iop/production/import_.py,sha256=hK5d6CKtd-_FOmuU39wiZ2cmn-SCUoZ-r3hRz8HgwgA,9865
70
70
  iop/production/inspection.py,sha256=LsAI4vhF27v8OMXtRAOHQn8XeaT4CIhjBu-CA-aorO0,3343
71
- iop/production/model.py,sha256=mHWXxYdlUHoJJqCjAM6rcqItjEeawaPcdqkke6DAZao,28934
72
- iop/production/reconstruction.py,sha256=2cLeh9J-Gyjbnn34wG9jBmaKEfHvm3eCBZdjMkYB7PQ,6435
73
- iop/production/rendering.py,sha256=XK1d1tLjuseZfasQhHDZr0BAMVgZjkUKPBo6-8_umMg,9368
71
+ iop/production/model.py,sha256=7YDAgQ-fo87VnE5A-CxmNtNsRHMVFB2NKkpBykH-ohQ,31106
72
+ iop/production/reconstruction.py,sha256=YJMOBtpTzT-IxgQ4lx1kivA8zbWz3UZfwt-5q7d_kMk,7135
73
+ iop/production/rendering.py,sha256=d9Et3hE8aPTCfnk0RKc6vu5HYOrI-JXFCfU6vZLKsKg,10649
74
74
  iop/production/runtime.py,sha256=JngIVVkigt1PNLUXBvQu3t8cBos-zLKlgguf0_WWh-M,2302
75
75
  iop/production/types.py,sha256=SsTYPspc8VGrSH8hy49tDBVLWu55D5yODNT7lfFbjmc,9131
76
+ iop/production/validation.py,sha256=yeWEQmWANQ8md2H8QN4DhHyR5nIDLMJWZN7e0d2y2Rk,13839
76
77
  iop/runtime/__init__.py,sha256=wcyWMRuPCYE_CLd1NmgmI1M-IbFs2Gz2yIj2-EW_2AA,70
77
78
  iop/runtime/director.py,sha256=KW8BvkATK8XuIUPNBD9rMww9r3Zg4K3H54868eOgvl0,12867
78
79
  iop/runtime/environment.py,sha256=0KZ4EoW6NUEbF7FW471emxQ-9FbaiX6ngUHVGihgZXU,1564
79
80
  iop/runtime/iris.py,sha256=wFbrgyv0FiMujGeEyZTysx4JX8Exk5ZPAhjc25wGWGU,185
80
- iop/runtime/local.py,sha256=p6jZRxd7tsmgyeZqoH5qzqFVBCa5j-r7b-SuaP8dUcI,4593
81
- iop/runtime/protocol.py,sha256=wclpJYdK36oG1LN2hyDlBs4g0Ky8oSgsn1ark6GNqGE,3289
81
+ iop/runtime/local.py,sha256=Ph3QO-vxwjuRzxMsgeueQEV1vaKRSYHypN9bS08RCLI,4786
82
+ iop/runtime/protocol.py,sha256=Z5l8jjjM7OU1tdOunzQSNLlzid8ZiMciPL__8Oi0QV4,3364
82
83
  iop/runtime/remote/__init__.py,sha256=qUIhYpG28edUc_-hgILjP3xGwljGqS5AuybvmUayWlg,350
83
84
  iop/runtime/remote/client.py,sha256=A-cTFgl2Lvwwv6CxNoa_yqriYncIXCsU6o5TWBfkzck,2912
84
- iop/runtime/remote/director.py,sha256=IZa505MUsAwI_cfTK5nzRmrB927TMW-BBB2B7dQPnEY,9199
85
- iop/runtime/remote/migration.py,sha256=J7r1MiWd3ISPjojj_ZqG2x88GddSwjJmXHiuy2F3HkY,1689
85
+ iop/runtime/remote/director.py,sha256=jNnBIocWS76vog6noJtVky5Tv7ZDnPwiWl-hCu958DE,9380
86
+ iop/runtime/remote/migration.py,sha256=Iboz-Ye7i9xif9bn433GazCF-gtD-HJW3wiUwWS5P6w,1831
86
87
  iop/runtime/remote/settings.py,sha256=P1Hsgld5o57HpSZgPVZzobyjlUmxJj6bstiWvR0JXnc,2144
87
88
  iop/runtime/remote/setup.py,sha256=Tr80F3JJTy7oSUso2jyx0iA_KZayd4MYt9vL0BvPpMA,2600
88
- iris_pex_embedded_python-4.0.0b3.dist-info/licenses/LICENSE,sha256=rZSiBFId_sfbJ6RL0GjjPX-InNLkNS9ou7eQsikciI8,1089
89
- iris_pex_embedded_python-4.0.0b3.dist-info/METADATA,sha256=aWCFN8YxK2lK5sJD0vzlu8AkfJ3YBAkSjY3ulm-F78k,4412
90
- iris_pex_embedded_python-4.0.0b3.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
91
- iris_pex_embedded_python-4.0.0b3.dist-info/entry_points.txt,sha256=fzS8ZFsPe8cnpEx208X60DNugCPQmuSloPeg4pyyJDY,42
92
- iris_pex_embedded_python-4.0.0b3.dist-info/top_level.txt,sha256=BM54-OXQ6l2BDtmesWNXckh033s9gcjoO7bfjbwZbxU,4
93
- iris_pex_embedded_python-4.0.0b3.dist-info/RECORD,,
89
+ iris_pex_embedded_python-4.0.0b4.dist-info/licenses/LICENSE,sha256=rZSiBFId_sfbJ6RL0GjjPX-InNLkNS9ou7eQsikciI8,1089
90
+ iris_pex_embedded_python-4.0.0b4.dist-info/METADATA,sha256=wksvI6gY4uyZ7OftWfoAvCB4AspPjHl5xNqMsiA0sIY,4412
91
+ iris_pex_embedded_python-4.0.0b4.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
92
+ iris_pex_embedded_python-4.0.0b4.dist-info/entry_points.txt,sha256=fzS8ZFsPe8cnpEx208X60DNugCPQmuSloPeg4pyyJDY,42
93
+ iris_pex_embedded_python-4.0.0b4.dist-info/top_level.txt,sha256=BM54-OXQ6l2BDtmesWNXckh033s9gcjoO7bfjbwZbxU,4
94
+ iris_pex_embedded_python-4.0.0b4.dist-info/RECORD,,