iris-pex-embedded-python 4.0.0b2__tar.gz → 4.0.0b4__tar.gz

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 (99) hide show
  1. {iris_pex_embedded_python-4.0.0b2/src/iris_pex_embedded_python.egg-info → iris_pex_embedded_python-4.0.0b4}/PKG-INFO +1 -1
  2. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/pyproject.toml +1 -1
  3. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/__init__.py +8 -0
  4. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/cli/main.py +19 -3
  5. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/cli/parser.py +5 -0
  6. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/cli/types.py +1 -0
  7. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/cls/IOP/Service/Remote/Rest/v1.cls +2 -1
  8. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/cls/IOP/Utils.cls +3 -1
  9. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/messages/serialization.py +13 -4
  10. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/migration/plans.py +25 -2
  11. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/migration/utils.py +51 -7
  12. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/production/__init__.py +8 -0
  13. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/production/common.py +19 -0
  14. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/production/diff.py +9 -2
  15. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/production/import_.py +16 -1
  16. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/production/model.py +56 -0
  17. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/production/reconstruction.py +14 -0
  18. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/production/rendering.py +37 -1
  19. iris_pex_embedded_python-4.0.0b4/src/iop/production/validation.py +453 -0
  20. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/runtime/director.py +15 -5
  21. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/runtime/local.py +11 -2
  22. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/runtime/protocol.py +5 -1
  23. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/runtime/remote/director.py +10 -2
  24. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/runtime/remote/migration.py +7 -1
  25. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4/src/iris_pex_embedded_python.egg-info}/PKG-INFO +1 -1
  26. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iris_pex_embedded_python.egg-info/SOURCES.txt +1 -0
  27. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/LICENSE +0 -0
  28. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/README.md +0 -0
  29. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/setup.cfg +0 -0
  30. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/__main__.py +0 -0
  31. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/cli/__init__.py +0 -0
  32. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/cli/formatting.py +0 -0
  33. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/cls/IOP/BusinessOperation.cls +0 -0
  34. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/cls/IOP/BusinessProcess.cls +0 -0
  35. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/cls/IOP/BusinessService.cls +0 -0
  36. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/cls/IOP/Common.cls +0 -0
  37. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/cls/IOP/Director.cls +0 -0
  38. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/cls/IOP/Duplex/Operation.cls +0 -0
  39. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/cls/IOP/Duplex/Process.cls +0 -0
  40. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/cls/IOP/Duplex/Service.cls +0 -0
  41. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/cls/IOP/Generator/Message/Ack.cls +0 -0
  42. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/cls/IOP/Generator/Message/Poll.cls +0 -0
  43. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/cls/IOP/Generator/Message/Start.cls +0 -0
  44. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/cls/IOP/Generator/Message/StartPickle.cls +0 -0
  45. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/cls/IOP/Generator/Message/Stop.cls +0 -0
  46. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/cls/IOP/InboundAdapter.cls +0 -0
  47. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/cls/IOP/Message/JSONSchema.cls +0 -0
  48. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/cls/IOP/Message.cls +0 -0
  49. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/cls/IOP/OutboundAdapter.cls +0 -0
  50. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/cls/IOP/PickleMessage.cls +0 -0
  51. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/cls/IOP/PrivateSession/Duplex.cls +0 -0
  52. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/cls/IOP/PrivateSession/Message/Ack.cls +0 -0
  53. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/cls/IOP/PrivateSession/Message/Poll.cls +0 -0
  54. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/cls/IOP/PrivateSession/Message/Start.cls +0 -0
  55. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/cls/IOP/PrivateSession/Message/Stop.cls +0 -0
  56. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/cls/IOP/Projection.cls +0 -0
  57. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/cls/IOP/Service/Remote/Handler.cls +0 -0
  58. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/cls/IOP/Test.cls +0 -0
  59. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/cls/IOP/Wrapper.cls +0 -0
  60. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/components/__init__.py +0 -0
  61. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/components/async_request.py +0 -0
  62. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/components/business_host.py +0 -0
  63. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/components/business_operation.py +0 -0
  64. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/components/business_process.py +0 -0
  65. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/components/business_service.py +0 -0
  66. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/components/common.py +0 -0
  67. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/components/debugpy.py +0 -0
  68. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/components/generator_request.py +0 -0
  69. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/components/inbound_adapter.py +0 -0
  70. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/components/log_manager.py +0 -0
  71. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/components/outbound_adapter.py +0 -0
  72. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/components/polling_business_service.py +0 -0
  73. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/components/private_session_duplex.py +0 -0
  74. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/components/private_session_process.py +0 -0
  75. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/components/settings.py +0 -0
  76. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/messages/__init__.py +0 -0
  77. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/messages/base.py +0 -0
  78. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/messages/decorators.py +0 -0
  79. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/messages/dispatch.py +0 -0
  80. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/messages/persistent.py +0 -0
  81. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/messages/validation.py +0 -0
  82. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/migration/__init__.py +0 -0
  83. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/migration/io.py +0 -0
  84. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/production/actions.py +0 -0
  85. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/production/component.py +0 -0
  86. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/production/inspection.py +0 -0
  87. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/production/runtime.py +0 -0
  88. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/production/types.py +0 -0
  89. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/runtime/__init__.py +0 -0
  90. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/runtime/environment.py +0 -0
  91. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/runtime/iris.py +0 -0
  92. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/runtime/remote/__init__.py +0 -0
  93. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/runtime/remote/client.py +0 -0
  94. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/runtime/remote/settings.py +0 -0
  95. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iop/runtime/remote/setup.py +0 -0
  96. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iris_pex_embedded_python.egg-info/dependency_links.txt +0 -0
  97. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iris_pex_embedded_python.egg-info/entry_points.txt +0 -0
  98. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iris_pex_embedded_python.egg-info/requires.txt +0 -0
  99. {iris_pex_embedded_python-4.0.0b2 → iris_pex_embedded_python-4.0.0b4}/src/iris_pex_embedded_python.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iris_pex_embedded_python
3
- Version: 4.0.0b2
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
@@ -3,7 +3,7 @@ requires = ["setuptools", "wheel"]
3
3
 
4
4
  [project]
5
5
  name = "iris_pex_embedded_python"
6
- version = "4.0.0b2"
6
+ version = "4.0.0b4"
7
7
  description = "Iris Interoperability based on Embedded Python"
8
8
  readme = "README.md"
9
9
  requires-python = ">=3.10"
@@ -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",
@@ -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(
@@ -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(
@@ -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
  }
@@ -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)
@@ -22,6 +22,12 @@ class SerializationError(Exception):
22
22
  pass
23
23
 
24
24
 
25
+ class MessageClassImportError(SerializationError, ImportError):
26
+ """Raised when a JSON message's Python class cannot be imported."""
27
+
28
+ pass
29
+
30
+
25
31
  class TempPydanticModel(BaseModel):
26
32
  model_config = {"arbitrary_types_allowed": True, "extra": "allow"}
27
33
 
@@ -93,8 +99,8 @@ class MessageSerializer:
93
99
  )
94
100
  module = importlib.import_module(module_name)
95
101
  msg_class = getattr(module, class_name)
96
- except Exception as e:
97
- raise SerializationError(
102
+ except (ModuleNotFoundError, AttributeError, ValueError) as e:
103
+ raise MessageClassImportError(
98
104
  f"Failed to load class {serial.classname}: {str(e)}"
99
105
  ) from e
100
106
 
@@ -130,9 +136,12 @@ class MessageSerializer:
130
136
 
131
137
  @staticmethod
132
138
  def _parse_classname(classname: str) -> tuple[str, str]:
133
- j = classname.rindex(".")
139
+ try:
140
+ j = classname.rindex(".")
141
+ except ValueError as e:
142
+ raise ValueError(f"Classname must include a module: {classname}") from e
134
143
  if j <= 0:
135
- raise SerializationError(f"Classname must include a module: {classname}")
144
+ raise ValueError(f"Classname must include a module: {classname}")
136
145
  return classname[:j], classname[j + 1 :]
137
146
 
138
147
  @staticmethod
@@ -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(
@@ -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",
@@ -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):
@@ -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]]:
@@ -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]]:
@@ -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
  )