iris-pex-embedded-python 4.0.0b7__tar.gz → 4.0.0b9__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 (104) hide show
  1. {iris_pex_embedded_python-4.0.0b7/src/iris_pex_embedded_python.egg-info → iris_pex_embedded_python-4.0.0b9}/PKG-INFO +1 -1
  2. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/pyproject.toml +1 -1
  3. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/__init__.py +8 -0
  4. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/cli/main.py +137 -1
  5. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/cli/parser.py +32 -0
  6. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/cli/types.py +15 -0
  7. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/cls/IOP/Utils.cls +327 -0
  8. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/migration/utils.py +21 -0
  9. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/production/__init__.py +8 -0
  10. iris_pex_embedded_python-4.0.0b9/src/iop/production/actions.py +516 -0
  11. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/production/model.py +51 -2
  12. iris_pex_embedded_python-4.0.0b9/src/iop/production/planning.py +675 -0
  13. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/runtime/local.py +10 -0
  14. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/runtime/protocol.py +5 -0
  15. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/runtime/remote/director.py +10 -0
  16. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9/src/iris_pex_embedded_python.egg-info}/PKG-INFO +1 -1
  17. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iris_pex_embedded_python.egg-info/SOURCES.txt +1 -0
  18. iris_pex_embedded_python-4.0.0b7/src/iop/production/actions.py +0 -228
  19. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/LICENSE +0 -0
  20. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/README.md +0 -0
  21. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/setup.cfg +0 -0
  22. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/__main__.py +0 -0
  23. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/cli/__init__.py +0 -0
  24. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/cli/formatting.py +0 -0
  25. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/cls/IOP/BusinessOperation.cls +0 -0
  26. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/cls/IOP/BusinessProcess.cls +0 -0
  27. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/cls/IOP/BusinessService.cls +0 -0
  28. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/cls/IOP/Common.cls +0 -0
  29. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/cls/IOP/Director.cls +0 -0
  30. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/cls/IOP/Duplex/Operation.cls +0 -0
  31. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/cls/IOP/Duplex/Process.cls +0 -0
  32. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/cls/IOP/Duplex/Service.cls +0 -0
  33. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/cls/IOP/Generator/Message/Ack.cls +0 -0
  34. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/cls/IOP/Generator/Message/Poll.cls +0 -0
  35. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/cls/IOP/Generator/Message/Start.cls +0 -0
  36. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/cls/IOP/Generator/Message/StartPickle.cls +0 -0
  37. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/cls/IOP/Generator/Message/Stop.cls +0 -0
  38. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/cls/IOP/InboundAdapter.cls +0 -0
  39. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/cls/IOP/Message/JSONSchema.cls +0 -0
  40. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/cls/IOP/Message.cls +0 -0
  41. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/cls/IOP/OutboundAdapter.cls +0 -0
  42. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/cls/IOP/PickleMessage.cls +0 -0
  43. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/cls/IOP/PrivateSession/Duplex.cls +0 -0
  44. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/cls/IOP/PrivateSession/Message/Ack.cls +0 -0
  45. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/cls/IOP/PrivateSession/Message/Poll.cls +0 -0
  46. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/cls/IOP/PrivateSession/Message/Start.cls +0 -0
  47. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/cls/IOP/PrivateSession/Message/Stop.cls +0 -0
  48. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/cls/IOP/Projection.cls +0 -0
  49. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/cls/IOP/Service/Remote/Handler.cls +0 -0
  50. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/cls/IOP/Service/Remote/Rest/v1.cls +0 -0
  51. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/cls/IOP/Test.cls +0 -0
  52. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/cls/IOP/Wrapper.cls +0 -0
  53. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/components/__init__.py +0 -0
  54. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/components/async_request.py +0 -0
  55. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/components/business_host.py +0 -0
  56. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/components/business_operation.py +0 -0
  57. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/components/business_process.py +0 -0
  58. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/components/business_service.py +0 -0
  59. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/components/common.py +0 -0
  60. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/components/debugpy.py +0 -0
  61. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/components/generator_request.py +0 -0
  62. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/components/inbound_adapter.py +0 -0
  63. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/components/log_manager.py +0 -0
  64. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/components/outbound_adapter.py +0 -0
  65. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/components/polling_business_service.py +0 -0
  66. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/components/private_session_duplex.py +0 -0
  67. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/components/private_session_process.py +0 -0
  68. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/components/settings.py +0 -0
  69. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/messages/__init__.py +0 -0
  70. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/messages/base.py +0 -0
  71. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/messages/decorators.py +0 -0
  72. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/messages/dispatch.py +0 -0
  73. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/messages/persistent.py +0 -0
  74. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/messages/serialization.py +0 -0
  75. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/messages/validation.py +0 -0
  76. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/migration/__init__.py +0 -0
  77. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/migration/io.py +0 -0
  78. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/migration/manifest.py +0 -0
  79. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/migration/plans.py +0 -0
  80. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/production/common.py +0 -0
  81. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/production/component.py +0 -0
  82. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/production/declarations.py +0 -0
  83. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/production/declarative.py +0 -0
  84. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/production/diff.py +0 -0
  85. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/production/import_.py +0 -0
  86. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/production/inspection.py +0 -0
  87. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/production/reconstruction.py +0 -0
  88. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/production/rendering.py +0 -0
  89. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/production/runtime.py +0 -0
  90. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/production/types.py +0 -0
  91. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/production/validation.py +0 -0
  92. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/runtime/__init__.py +0 -0
  93. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/runtime/director.py +0 -0
  94. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/runtime/environment.py +0 -0
  95. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/runtime/iris.py +0 -0
  96. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/runtime/remote/__init__.py +0 -0
  97. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/runtime/remote/client.py +0 -0
  98. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/runtime/remote/migration.py +0 -0
  99. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/runtime/remote/settings.py +0 -0
  100. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iop/runtime/remote/setup.py +0 -0
  101. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iris_pex_embedded_python.egg-info/dependency_links.txt +0 -0
  102. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iris_pex_embedded_python.egg-info/entry_points.txt +0 -0
  103. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/src/iris_pex_embedded_python.egg-info/requires.txt +0 -0
  104. {iris_pex_embedded_python-4.0.0b7 → iris_pex_embedded_python-4.0.0b9}/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.0b7
3
+ Version: 4.0.0b9
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.0b7"
6
+ version = "4.0.0b9"
7
7
  description = "Iris Interoperability based on Embedded Python"
8
8
  readme = "README.md"
9
9
  requires-python = ">=3.10"
@@ -32,9 +32,13 @@ from iop.production import OperationItem as OperationItem
32
32
  from iop.production import Port as Port
33
33
  from iop.production import ProcessItem as ProcessItem
34
34
  from iop.production import Production as Production
35
+ from iop.production import ProductionApplyResult as ProductionApplyResult
36
+ from iop.production import ProductionChangePlan as ProductionChangePlan
35
37
  from iop.production import ProductionDiff as ProductionDiff
36
38
  from iop.production import ProductionDiffEntry as ProductionDiffEntry
37
39
  from iop.production import ProductionGraph as ProductionGraph
40
+ from iop.production import ProductionPlanOperation as ProductionPlanOperation
41
+ from iop.production import ProductionVerifyResult as ProductionVerifyResult
38
42
  from iop.production import ProductionValidationError as ProductionValidationError
39
43
  from iop.production import ProductionValidationIssue as ProductionValidationIssue
40
44
  from iop.production import ProductionValidationReport as ProductionValidationReport
@@ -70,9 +74,13 @@ __all__ = [
70
74
  "Port",
71
75
  "ProcessItem",
72
76
  "Production",
77
+ "ProductionApplyResult",
78
+ "ProductionChangePlan",
73
79
  "ProductionDiff",
74
80
  "ProductionDiffEntry",
75
81
  "ProductionGraph",
82
+ "ProductionPlanOperation",
83
+ "ProductionVerifyResult",
76
84
  "ProductionValidationError",
77
85
  "ProductionValidationIssue",
78
86
  "ProductionValidationReport",
@@ -9,7 +9,7 @@ from importlib.metadata import version
9
9
  import requests
10
10
 
11
11
  from ..migration import utils as migration_utils
12
- from ..production import Production
12
+ from ..production import Production, ProductionChangePlan
13
13
  from ..runtime.local import _LocalDirector
14
14
  from ..runtime.protocol import DirectorProtocol
15
15
  from ..runtime.remote import _RemoteDirector, get_remote_settings
@@ -78,6 +78,11 @@ class Command:
78
78
  self.args.bindings,
79
79
  self.args.unbind is not None,
80
80
  self.args.update,
81
+ self.args.plan,
82
+ self.args.review_plan,
83
+ self.args.apply_plan,
84
+ self.args.verify_plan,
85
+ self.args.rollback_backup,
81
86
  ]
82
87
  )
83
88
 
@@ -109,6 +114,11 @@ class Command:
109
114
  CommandType.UNBIND: self._handle_unbind,
110
115
  CommandType.HELP: self._handle_help,
111
116
  CommandType.UPDATE: self._handle_update,
117
+ CommandType.PLAN: self._handle_plan,
118
+ CommandType.REVIEW_PLAN: self._handle_review_plan,
119
+ CommandType.APPLY_PLAN: self._handle_apply_plan,
120
+ CommandType.VERIFY_PLAN: self._handle_verify_plan,
121
+ CommandType.ROLLBACK_BACKUP: self._handle_rollback_backup,
112
122
  }
113
123
  handler = command_handlers.get(command_type)
114
124
  if handler:
@@ -149,6 +159,16 @@ class Command:
149
159
  return CommandType.UNBIND
150
160
  if self.args.update:
151
161
  return CommandType.UPDATE
162
+ if self.args.plan:
163
+ return CommandType.PLAN
164
+ if self.args.review_plan:
165
+ return CommandType.REVIEW_PLAN
166
+ if self.args.apply_plan:
167
+ return CommandType.APPLY_PLAN
168
+ if self.args.verify_plan:
169
+ return CommandType.VERIFY_PLAN
170
+ if self.args.rollback_backup:
171
+ return CommandType.ROLLBACK_BACKUP
152
172
  return CommandType.HELP
153
173
 
154
174
  def _handle_default(self) -> None:
@@ -324,6 +344,55 @@ class Command:
324
344
  self.director.unbind_component(self.args.unbind)
325
345
  print(f"Removed binding {self.args.unbind}.")
326
346
 
347
+ def _handle_plan(self) -> None:
348
+ production = self._load_production_from_settings(
349
+ self.args.plan,
350
+ self.args.production,
351
+ )
352
+ plan = production.plan()
353
+ if self.args.out:
354
+ plan.save(self._absolute_path(self.args.out))
355
+ print(f"Production change plan written to {self.args.out}")
356
+ else:
357
+ print(plan)
358
+
359
+ def _handle_review_plan(self) -> None:
360
+ plan = ProductionChangePlan.load(self._absolute_path(self.args.review_plan))
361
+ print(plan)
362
+
363
+ def _handle_apply_plan(self) -> None:
364
+ if not self.args.settings:
365
+ raise ValueError("--apply-plan requires --settings.")
366
+ plan = ProductionChangePlan.load(self._absolute_path(self.args.apply_plan))
367
+ production = self._load_production_from_settings(
368
+ self.args.settings,
369
+ self.args.production or plan.production_name,
370
+ )
371
+ result = production.apply(
372
+ plan,
373
+ allow_destructive=self.args.allow_destructive,
374
+ backup_dir=self.args.backup_dir,
375
+ )
376
+ print(result)
377
+
378
+ def _handle_verify_plan(self) -> None:
379
+ plan = ProductionChangePlan.load(self._absolute_path(self.args.verify_plan))
380
+ production = Production(
381
+ self.args.production or plan.production_name,
382
+ namespace=self.director.namespace,
383
+ director=self.director,
384
+ )
385
+ print(production.verify(plan))
386
+
387
+ def _handle_rollback_backup(self) -> None:
388
+ result = Production.rollback_backup(
389
+ self._absolute_path(self.args.rollback_backup),
390
+ director=self.director,
391
+ namespace=self.director.namespace,
392
+ allow_destructive=self.args.allow_destructive,
393
+ )
394
+ print(result)
395
+
327
396
  def _handle_help(self) -> None:
328
397
  create_parser().print_help()
329
398
  if self._is_remote:
@@ -334,6 +403,73 @@ class Command:
334
403
  except Exception:
335
404
  logging.warning("Could not retrieve default production.")
336
405
 
406
+ def _load_production_from_settings(
407
+ self,
408
+ settings_file: str | None,
409
+ production_name: str | None,
410
+ ) -> Production:
411
+ if not settings_file:
412
+ raise ValueError("A settings.py file is required.")
413
+ settings_path = self._absolute_path(settings_file)
414
+ settings, path_added = migration_utils._load_settings(settings_path)
415
+ try:
416
+ production = _select_production(
417
+ getattr(settings, "PRODUCTIONS", None),
418
+ production_name,
419
+ director=self.director,
420
+ namespace=self.director.namespace,
421
+ )
422
+ production.with_director(self.director)
423
+ if not production.namespace:
424
+ production.in_namespace(self.director.namespace)
425
+ return production
426
+ finally:
427
+ migration_utils._cleanup_sys_path(path_added)
428
+
429
+ @staticmethod
430
+ def _absolute_path(path: str | None) -> str:
431
+ if not path:
432
+ raise ValueError("Path is required.")
433
+ if os.path.isabs(path):
434
+ return path
435
+ return os.path.join(os.getcwd(), path)
436
+
437
+
438
+ def _select_production(
439
+ productions,
440
+ production_name: str | None,
441
+ *,
442
+ director: DirectorProtocol,
443
+ namespace: str | None,
444
+ ) -> Production:
445
+ if not isinstance(productions, list):
446
+ raise ValueError("settings.py must define PRODUCTIONS as a list.")
447
+ candidates: list[Production] = []
448
+ for entry in productions:
449
+ if isinstance(entry, Production):
450
+ candidate = entry
451
+ elif isinstance(entry, dict):
452
+ candidate = Production.from_dict(
453
+ entry,
454
+ director=director,
455
+ namespace=namespace,
456
+ )
457
+ else:
458
+ continue
459
+ candidates.append(candidate)
460
+ if production_name:
461
+ for candidate in candidates:
462
+ if candidate.name == production_name:
463
+ return candidate
464
+ raise ValueError(f"Production {production_name!r} not found in settings.")
465
+ if len(candidates) == 1:
466
+ return candidates[0]
467
+ names = ", ".join(candidate.name for candidate in candidates) or "none"
468
+ raise ValueError(
469
+ "Production name is required when settings.py contains multiple "
470
+ f"productions ({names})."
471
+ )
472
+
337
473
 
338
474
  def main(argv=None) -> None:
339
475
  parser = create_parser()
@@ -64,6 +64,17 @@ def create_parser() -> argparse.ArgumentParser:
64
64
  "-t", "--test", help="test the iop module in iris", nargs="?", const="not_set"
65
65
  )
66
66
  parser.add_argument("-u", "--update", help="update a production", action="store_true")
67
+ parser.add_argument(
68
+ "--plan",
69
+ help="build a conservative production change plan from a Python settings file",
70
+ )
71
+ parser.add_argument("--review-plan", help="print a saved production change plan")
72
+ parser.add_argument("--apply-plan", help="apply a saved production change plan")
73
+ parser.add_argument("--verify-plan", help="verify a saved production change plan")
74
+ parser.add_argument(
75
+ "--rollback-backup",
76
+ help="restore a production from a plan/apply backup directory",
77
+ )
67
78
 
68
79
  start = main_parser.add_argument_group("start arguments")
69
80
  start.add_argument(
@@ -117,6 +128,27 @@ def create_parser() -> argparse.ArgumentParser:
117
128
  action="store_true",
118
129
  )
119
130
 
131
+ plan = main_parser.add_argument_group("production plan arguments")
132
+ plan.add_argument(
133
+ "--production",
134
+ help="target production name for --plan, or override the plan production",
135
+ )
136
+ plan.add_argument("--out", help="write --plan JSON to this path")
137
+ plan.add_argument(
138
+ "--settings",
139
+ help="settings.py file containing the desired Production for --apply-plan",
140
+ )
141
+ plan.add_argument(
142
+ "--backup-dir",
143
+ default=".iop/backups",
144
+ help="directory for apply backup artifacts",
145
+ )
146
+ plan.add_argument(
147
+ "--allow-destructive",
148
+ help="allow destructive plan operations or rollback",
149
+ action="store_true",
150
+ )
151
+
120
152
  remote = main_parser.add_argument_group("remote arguments")
121
153
  remote.add_argument(
122
154
  "-R",
@@ -23,6 +23,11 @@ class CommandType(Enum):
23
23
  UNBIND = auto()
24
24
  HELP = auto()
25
25
  UPDATE = auto()
26
+ PLAN = auto()
27
+ REVIEW_PLAN = auto()
28
+ APPLY_PLAN = auto()
29
+ VERIFY_PLAN = auto()
30
+ ROLLBACK_BACKUP = auto()
26
31
 
27
32
 
28
33
  @dataclass
@@ -56,3 +61,13 @@ class CommandArgs:
56
61
  update: bool = False
57
62
  migration_plan: bool = False
58
63
  strict_production_validation: bool = False
64
+ plan: str | None = None
65
+ production: str | None = None
66
+ out: str | None = None
67
+ review_plan: str | None = None
68
+ apply_plan: str | None = None
69
+ verify_plan: str | None = None
70
+ rollback_backup: str | None = None
71
+ settings: str | None = None
72
+ backup_dir: str = ".iop/backups"
73
+ allow_destructive: bool = False
@@ -878,6 +878,333 @@ ClassMethod ExportProductionQueueInfo(pProductionName As %String) As %String
878
878
  Return result.%ToJSON()
879
879
  }
880
880
 
881
+ /// Apply a conservative granular production change plan.
882
+ ClassMethod ApplyProductionPlan(
883
+ pProductionName As %String,
884
+ pPlan As %String,
885
+ pAllowDestructive As %Boolean = 0) As %String
886
+ {
887
+ Set result = {}
888
+ Set result.production = pProductionName
889
+ Set result.operations = []
890
+ Try {
891
+ Set plan = {}.%FromJSON(pPlan)
892
+ Set operations = plan.%Get("operations")
893
+ Set production = ##class(Ens.Config.Production).%OpenId(pProductionName)
894
+ If '$IsObject(production) {
895
+ Set loaded = ##class(Ens.Config.Production).LoadFromClass(pProductionName)
896
+ If $IsObject(loaded) {
897
+ Set production = loaded
898
+ } Else {
899
+ $$$ThrowStatus($$$ERROR($$$EnsErrGeneral,"Production could not be loaded: "_pProductionName))
900
+ }
901
+ }
902
+ If $IsObject(operations) {
903
+ For i=0:1:operations.%Size()-1 {
904
+ Set operation = operations.%Get(i)
905
+ Set opResult = {}
906
+ Set opResult.id = operation.%Get("id")
907
+ Set opResult."op_type" = operation.%Get("op_type")
908
+ Set opResult.path = operation.%Get("path")
909
+ Set risk = operation.%Get("risk")
910
+ If risk = "unsupported" {
911
+ Set opResult.status = "skipped"
912
+ Set opResult.message = $Select(operation.%Get("reason")'="":operation.%Get("reason"),1:"unsupported operation")
913
+ } ElseIf (risk = "destructive") && ('pAllowDestructive) {
914
+ Set opResult.status = "skipped"
915
+ Set opResult.message = "requires allow_destructive"
916
+ } Else {
917
+ Try {
918
+ $$$ThrowOnError(..ApplyProductionPlanOperation(production, operation))
919
+ Set opResult.status = "applied"
920
+ } Catch opEx {
921
+ Set opResult.status = "failed"
922
+ Set opResult.message = opEx.DisplayString()
923
+ }
924
+ }
925
+ Do result.operations.%Push(opResult)
926
+ }
927
+ }
928
+ $$$ThrowOnError(production.SaveToClass())
929
+ $$$ThrowOnError($System.OBJ.Compile(pProductionName,"k-d"))
930
+ } Catch ex {
931
+ Set result.error = ex.DisplayString()
932
+ }
933
+ Return result.%ToJSON()
934
+ }
935
+
936
+ ClassMethod ApplyProductionPlanOperation(
937
+ pProduction As Ens.Config.Production,
938
+ pOperation) As %Status [ Internal, Private ]
939
+ {
940
+ Set opType = pOperation.%Get("op_type")
941
+ If opType = "add_item" {
942
+ Return ..ApplyPlanAddItem(pProduction,pOperation)
943
+ }
944
+ If opType = "delete_item" {
945
+ Return ..ApplyPlanDeleteItem(pProduction,pOperation.%Get("item"))
946
+ }
947
+ If opType = "set_item_field" {
948
+ Return ..ApplyPlanSetItemField(pProduction,pOperation)
949
+ }
950
+ If opType = "replace_item_class" {
951
+ Return ..ApplyPlanSetItemField(pProduction,pOperation)
952
+ }
953
+ If opType = "set_production_field" {
954
+ Return ..ApplyPlanSetProductionField(pProduction,pOperation)
955
+ }
956
+ If opType = "set_setting" {
957
+ If pOperation.%Get("target") = "Other" {
958
+ Return ..ReplaceOtherSettings(..FindProductionItem(pProduction,pOperation.%Get("item")),pOperation.%Get("after"))
959
+ }
960
+ Return ..SetConfigSetting(..FindProductionItem(pProduction,pOperation.%Get("item")),pOperation.%Get("target"),pOperation.%Get("setting"),pOperation.%Get("after"))
961
+ }
962
+ If opType = "remove_setting" {
963
+ Return ..RemoveConfigSetting(..FindProductionItem(pProduction,pOperation.%Get("item")),pOperation.%Get("target"),pOperation.%Get("setting"))
964
+ }
965
+ If opType = "set_route_setting" {
966
+ Return ..SetConfigSetting(..FindProductionItem(pProduction,pOperation.%Get("source_item")),"Host",pOperation.%Get("source_port"),..DynamicArrayToCSV(pOperation.%Get("after")))
967
+ }
968
+ If opType = "remove_route" {
969
+ Return ..RemoveConfigSetting(..FindProductionItem(pProduction,pOperation.%Get("source_item")),"Host",pOperation.%Get("source_port"))
970
+ }
971
+ Quit $$$ERROR($$$EnsErrGeneral,"Unsupported production plan operation: "_opType)
972
+ }
973
+
974
+ ClassMethod ApplyPlanAddItem(
975
+ pProduction As Ens.Config.Production,
976
+ pOperation) As %Status [ Internal, Private ]
977
+ {
978
+ Set itemName = pOperation.%Get("item")
979
+ If itemName = "" {
980
+ Quit $$$ERROR($$$EnsErrGeneral,"Plan item name is required.")
981
+ }
982
+ If $IsObject(..FindProductionItem(pProduction,itemName,0)) {
983
+ Quit $$$ERROR($$$EnsErrGeneral,"Production item already exists: "_itemName)
984
+ }
985
+ Set data = pOperation.%Get("after")
986
+ Set item = ##class(Ens.Config.Item).%New()
987
+ Set item.Name = itemName
988
+ Set item.ClassName = data.%Get("class_name")
989
+ Set item.Category = data.%Get("category")
990
+ Set item.PoolSize = +data.%Get("pool_size")
991
+ Set item.Enabled = ..BooleanValue(data.%Get("enabled"))
992
+ Set item.Foreground = ..BooleanValue(data.%Get("foreground"))
993
+ Set item.Comment = data.%Get("comment")
994
+ Set item.LogTraceEvents = ..BooleanValue(data.%Get("log_trace_events"))
995
+ Set item.Schedule = data.%Get("schedule")
996
+ Set item.Production = pProduction
997
+ $$$ThrowOnError(..SetSettingsFromMap(item,"Host",data.%Get("host_settings")))
998
+ $$$ThrowOnError(..SetSettingsFromMap(item,"Adapter",data.%Get("adapter_settings")))
999
+ $$$ThrowOnError(..ReplaceOtherSettings(item,data.%Get("other_settings")))
1000
+ Do pProduction.Items.Insert(item)
1001
+ Quit $$$OK
1002
+ }
1003
+
1004
+ ClassMethod ApplyPlanDeleteItem(
1005
+ pProduction As Ens.Config.Production,
1006
+ pItemName As %String) As %Status [ Internal, Private ]
1007
+ {
1008
+ Set index = ..FindProductionItemIndex(pProduction,pItemName)
1009
+ If index < 1 {
1010
+ Quit $$$OK
1011
+ }
1012
+ Do pProduction.Items.RemoveAt(index)
1013
+ Quit $$$OK
1014
+ }
1015
+
1016
+ ClassMethod ApplyPlanSetItemField(
1017
+ pProduction As Ens.Config.Production,
1018
+ pOperation) As %Status [ Internal, Private ]
1019
+ {
1020
+ Set item = ..FindProductionItem(pProduction,pOperation.%Get("item"))
1021
+ Set field = pOperation.%Get("field")
1022
+ Set value = pOperation.%Get("after")
1023
+ If field = "class_name" {
1024
+ Set item.ClassName = value
1025
+ } ElseIf field = "category" {
1026
+ Set item.Category = value
1027
+ } ElseIf field = "pool_size" {
1028
+ Set item.PoolSize = +value
1029
+ } ElseIf field = "enabled" {
1030
+ Set item.Enabled = ..BooleanValue(value)
1031
+ } ElseIf field = "foreground" {
1032
+ Set item.Foreground = ..BooleanValue(value)
1033
+ } ElseIf field = "comment" {
1034
+ Set item.Comment = value
1035
+ } ElseIf field = "log_trace_events" {
1036
+ Set item.LogTraceEvents = ..BooleanValue(value)
1037
+ } ElseIf field = "schedule" {
1038
+ Set item.Schedule = value
1039
+ } Else {
1040
+ Quit $$$ERROR($$$EnsErrGeneral,"Unsupported item field: "_field)
1041
+ }
1042
+ Quit $$$OK
1043
+ }
1044
+
1045
+ ClassMethod ApplyPlanSetProductionField(
1046
+ pProduction As Ens.Config.Production,
1047
+ pOperation) As %Status [ Internal, Private ]
1048
+ {
1049
+ Set field = pOperation.%Get("field")
1050
+ Set value = pOperation.%Get("after")
1051
+ If field = "testing_enabled" {
1052
+ Set pProduction.TestingEnabled = ..BooleanValue(value)
1053
+ } ElseIf field = "log_general_trace_events" {
1054
+ Set pProduction.LogGeneralTraceEvents = ..BooleanValue(value)
1055
+ } ElseIf field = "actor_pool_size" {
1056
+ Set pProduction.ActorPoolSize = +value
1057
+ } ElseIf field = "description" {
1058
+ Set pProduction.Description = value
1059
+ } Else {
1060
+ Set settingName = ..ProductionSettingName(field)
1061
+ If settingName = "" {
1062
+ Quit $$$ERROR($$$EnsErrGeneral,"Unsupported production field: "_field)
1063
+ }
1064
+ $$$ThrowOnError(..SetConfigSetting(pProduction,"",settingName,value))
1065
+ }
1066
+ Quit $$$OK
1067
+ }
1068
+
1069
+ ClassMethod ProductionSettingName(pField As %String) As %String [ Internal, Private ]
1070
+ {
1071
+ Quit $Select(
1072
+ pField="shutdown_timeout":"ShutdownTimeout",
1073
+ pField="update_timeout":"UpdateTimeout",
1074
+ pField="alert_notification_manager":"AlertNotificationManager",
1075
+ pField="alert_notification_operation":"AlertNotificationOperation",
1076
+ pField="alert_notification_recipients":"AlertNotificationRecipients",
1077
+ pField="alert_action_window":"AlertActionWindow",
1078
+ 1:"")
1079
+ }
1080
+
1081
+ ClassMethod FindProductionItem(
1082
+ pProduction As Ens.Config.Production,
1083
+ pItemName As %String,
1084
+ pThrow As %Boolean = 1) [ Internal, Private ]
1085
+ {
1086
+ Set index = ..FindProductionItemIndex(pProduction,pItemName)
1087
+ If index > 0 {
1088
+ Quit pProduction.Items.GetAt(index)
1089
+ }
1090
+ If pThrow {
1091
+ $$$ThrowStatus($$$ERROR($$$EnsErrGeneral,"Production item does not exist: "_pItemName))
1092
+ }
1093
+ Quit ""
1094
+ }
1095
+
1096
+ ClassMethod FindProductionItemIndex(
1097
+ pProduction As Ens.Config.Production,
1098
+ pItemName As %String) As %Integer [ Internal, Private ]
1099
+ {
1100
+ For i=1:1:pProduction.Items.Count() {
1101
+ Set item = pProduction.Items.GetAt(i)
1102
+ If $IsObject(item), item.Name = pItemName {
1103
+ Return i
1104
+ }
1105
+ }
1106
+ Quit 0
1107
+ }
1108
+
1109
+ ClassMethod SetSettingsFromMap(
1110
+ pOwner,
1111
+ pTarget As %String,
1112
+ pMap) As %Status [ Internal, Private ]
1113
+ {
1114
+ If '$IsObject(pMap) {
1115
+ Quit $$$OK
1116
+ }
1117
+ Set iterator = pMap.%GetIterator()
1118
+ While iterator.%GetNext(.key,.value) {
1119
+ $$$ThrowOnError(..SetConfigSetting(pOwner,pTarget,key,value))
1120
+ }
1121
+ Quit $$$OK
1122
+ }
1123
+
1124
+ ClassMethod SetConfigSetting(
1125
+ pOwner,
1126
+ pTarget As %String,
1127
+ pName As %String,
1128
+ pValue) As %Status [ Internal, Private ]
1129
+ {
1130
+ Set index = ..FindConfigSettingIndex(pOwner,pTarget,pName)
1131
+ If index > 0 {
1132
+ Set setting = pOwner.Settings.GetAt(index)
1133
+ } Else {
1134
+ Set setting = ##class(Ens.Config.Setting).%New()
1135
+ Set setting.Target = pTarget
1136
+ Set setting.Name = pName
1137
+ Do pOwner.Settings.Insert(setting)
1138
+ }
1139
+ Set setting.Value = pValue
1140
+ Quit $$$OK
1141
+ }
1142
+
1143
+ ClassMethod RemoveConfigSetting(
1144
+ pOwner,
1145
+ pTarget As %String,
1146
+ pName As %String) As %Status [ Internal, Private ]
1147
+ {
1148
+ Set index = ..FindConfigSettingIndex(pOwner,pTarget,pName)
1149
+ If index > 0 {
1150
+ Do pOwner.Settings.RemoveAt(index)
1151
+ }
1152
+ Quit $$$OK
1153
+ }
1154
+
1155
+ ClassMethod ReplaceOtherSettings(
1156
+ pOwner,
1157
+ pSettings) As %Status [ Internal, Private ]
1158
+ {
1159
+ For i=pOwner.Settings.Count():-1:1 {
1160
+ Set setting = pOwner.Settings.GetAt(i)
1161
+ If $IsObject(setting), setting.Target '= "Host", setting.Target '= "Adapter" {
1162
+ Do pOwner.Settings.RemoveAt(i)
1163
+ }
1164
+ }
1165
+ If '$IsObject(pSettings) {
1166
+ Quit $$$OK
1167
+ }
1168
+ For i=0:1:pSettings.%Size()-1 {
1169
+ Set data = pSettings.%Get(i)
1170
+ Set setting = ##class(Ens.Config.Setting).%New()
1171
+ Set setting.Target = ..DynamicGet(data,"@Target","")
1172
+ Set setting.Name = ..DynamicGet(data,"@Name","")
1173
+ Set setting.Value = ..DynamicGet(data,"#text","")
1174
+ Do pOwner.Settings.Insert(setting)
1175
+ }
1176
+ Quit $$$OK
1177
+ }
1178
+
1179
+ ClassMethod FindConfigSettingIndex(
1180
+ pOwner,
1181
+ pTarget As %String,
1182
+ pName As %String) As %Integer [ Internal, Private ]
1183
+ {
1184
+ For i=1:1:pOwner.Settings.Count() {
1185
+ Set setting = pOwner.Settings.GetAt(i)
1186
+ If $IsObject(setting), setting.Target = pTarget, setting.Name = pName {
1187
+ Return i
1188
+ }
1189
+ }
1190
+ Quit 0
1191
+ }
1192
+
1193
+ ClassMethod DynamicArrayToCSV(pArray) As %String [ Internal, Private ]
1194
+ {
1195
+ If '$IsObject(pArray) {
1196
+ Quit pArray
1197
+ }
1198
+ Set value = ""
1199
+ For i=0:1:pArray.%Size()-1 {
1200
+ If value '= "" {
1201
+ Set value = value_","
1202
+ }
1203
+ Set value = value_pArray.%Get(i)
1204
+ }
1205
+ Quit value
1206
+ }
1207
+
881
1208
  ClassMethod dispatchTestComponent(
882
1209
  pTargetName As %String,
883
1210
  pInput As Ens.Request) As Ens.Response
@@ -1009,6 +1009,27 @@ def export_production_queue_info(production_name):
1009
1009
  return json.loads(data)
1010
1010
 
1011
1011
 
1012
+ def apply_production_plan(plan: dict, allow_destructive: bool = False) -> dict:
1013
+ """
1014
+ Apply a conservative granular production change plan locally in IRIS.
1015
+ """
1016
+ production_name = plan.get("production") or plan.get("production_name")
1017
+ if not production_name:
1018
+ raise ValueError("Production plan is missing the production name.")
1019
+ data = (
1020
+ _iris.get_iris()
1021
+ .cls("IOP.Utils")
1022
+ .ApplyProductionPlan(
1023
+ production_name,
1024
+ json.dumps(plan),
1025
+ 1 if allow_destructive else 0,
1026
+ )
1027
+ )
1028
+ if isinstance(data, dict):
1029
+ return data
1030
+ return json.loads(data)
1031
+
1032
+
1012
1033
  def xml_to_json(xml_string: str) -> str:
1013
1034
  return _xml_to_json(xml_string)
1014
1035
 
@@ -7,6 +7,10 @@ from .declarations import ProcessItem as ProcessItem
7
7
  from .declarations import Route as Route
8
8
  from .declarations import ServiceItem as ServiceItem
9
9
  from .model import Production as Production
10
+ from .planning import ProductionApplyResult as ProductionApplyResult
11
+ from .planning import ProductionChangePlan as ProductionChangePlan
12
+ from .planning import ProductionPlanOperation as ProductionPlanOperation
13
+ from .planning import ProductionVerifyResult as ProductionVerifyResult
10
14
  from .runtime import resolve_target as resolve_target
11
15
  from .types import GraphEdge as GraphEdge
12
16
  from .types import GraphNode as GraphNode
@@ -32,9 +36,13 @@ __all__ = [
32
36
  "Port",
33
37
  "ProcessItem",
34
38
  "Production",
39
+ "ProductionApplyResult",
40
+ "ProductionChangePlan",
35
41
  "ProductionDiff",
36
42
  "ProductionDiffEntry",
37
43
  "ProductionGraph",
44
+ "ProductionPlanOperation",
45
+ "ProductionVerifyResult",
38
46
  "ProductionValidationError",
39
47
  "ProductionValidationIssue",
40
48
  "ProductionValidationReport",