iris-pex-embedded-python 4.0.0b8__tar.gz → 4.0.0b10__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 (105) hide show
  1. {iris_pex_embedded_python-4.0.0b8/src/iris_pex_embedded_python.egg-info → iris_pex_embedded_python-4.0.0b10}/PKG-INFO +1 -1
  2. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/pyproject.toml +4 -1
  3. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/__init__.py +10 -2
  4. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/cli/main.py +140 -1
  5. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/cli/parser.py +33 -1
  6. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/cli/types.py +15 -0
  7. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/cls/IOP/Utils.cls +403 -0
  8. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/components/business_host.py +29 -24
  9. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/components/business_process.py +2 -2
  10. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/migration/utils.py +21 -0
  11. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/production/__init__.py +10 -2
  12. iris_pex_embedded_python-4.0.0b10/src/iop/production/actions.py +525 -0
  13. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/production/component.py +29 -29
  14. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/production/declarations.py +22 -17
  15. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/production/declarative.py +88 -25
  16. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/production/diff.py +5 -5
  17. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/production/import_.py +29 -8
  18. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/production/inspection.py +1 -1
  19. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/production/model.py +229 -87
  20. iris_pex_embedded_python-4.0.0b10/src/iop/production/planning.py +678 -0
  21. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/production/reconstruction.py +139 -8
  22. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/production/rendering.py +27 -18
  23. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/production/runtime.py +22 -7
  24. iris_pex_embedded_python-4.0.0b10/src/iop/production/source_inference.py +681 -0
  25. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/production/types.py +225 -11
  26. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/runtime/local.py +10 -0
  27. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/runtime/protocol.py +5 -0
  28. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/runtime/remote/director.py +10 -0
  29. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10/src/iris_pex_embedded_python.egg-info}/PKG-INFO +1 -1
  30. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iris_pex_embedded_python.egg-info/SOURCES.txt +2 -0
  31. iris_pex_embedded_python-4.0.0b8/src/iop/production/actions.py +0 -228
  32. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/LICENSE +0 -0
  33. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/README.md +0 -0
  34. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/setup.cfg +0 -0
  35. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/__main__.py +0 -0
  36. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/cli/__init__.py +0 -0
  37. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/cli/formatting.py +0 -0
  38. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/cls/IOP/BusinessOperation.cls +0 -0
  39. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/cls/IOP/BusinessProcess.cls +0 -0
  40. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/cls/IOP/BusinessService.cls +0 -0
  41. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/cls/IOP/Common.cls +0 -0
  42. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/cls/IOP/Director.cls +0 -0
  43. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/cls/IOP/Duplex/Operation.cls +0 -0
  44. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/cls/IOP/Duplex/Process.cls +0 -0
  45. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/cls/IOP/Duplex/Service.cls +0 -0
  46. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/cls/IOP/Generator/Message/Ack.cls +0 -0
  47. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/cls/IOP/Generator/Message/Poll.cls +0 -0
  48. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/cls/IOP/Generator/Message/Start.cls +0 -0
  49. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/cls/IOP/Generator/Message/StartPickle.cls +0 -0
  50. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/cls/IOP/Generator/Message/Stop.cls +0 -0
  51. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/cls/IOP/InboundAdapter.cls +0 -0
  52. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/cls/IOP/Message/JSONSchema.cls +0 -0
  53. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/cls/IOP/Message.cls +0 -0
  54. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/cls/IOP/OutboundAdapter.cls +0 -0
  55. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/cls/IOP/PickleMessage.cls +0 -0
  56. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/cls/IOP/PrivateSession/Duplex.cls +0 -0
  57. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/cls/IOP/PrivateSession/Message/Ack.cls +0 -0
  58. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/cls/IOP/PrivateSession/Message/Poll.cls +0 -0
  59. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/cls/IOP/PrivateSession/Message/Start.cls +0 -0
  60. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/cls/IOP/PrivateSession/Message/Stop.cls +0 -0
  61. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/cls/IOP/Projection.cls +0 -0
  62. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/cls/IOP/Service/Remote/Handler.cls +0 -0
  63. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/cls/IOP/Service/Remote/Rest/v1.cls +0 -0
  64. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/cls/IOP/Test.cls +0 -0
  65. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/cls/IOP/Wrapper.cls +0 -0
  66. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/components/__init__.py +0 -0
  67. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/components/async_request.py +0 -0
  68. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/components/business_operation.py +0 -0
  69. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/components/business_service.py +0 -0
  70. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/components/common.py +0 -0
  71. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/components/debugpy.py +0 -0
  72. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/components/generator_request.py +0 -0
  73. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/components/inbound_adapter.py +0 -0
  74. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/components/log_manager.py +0 -0
  75. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/components/outbound_adapter.py +0 -0
  76. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/components/polling_business_service.py +0 -0
  77. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/components/private_session_duplex.py +0 -0
  78. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/components/private_session_process.py +0 -0
  79. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/components/settings.py +0 -0
  80. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/messages/__init__.py +0 -0
  81. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/messages/base.py +0 -0
  82. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/messages/decorators.py +0 -0
  83. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/messages/dispatch.py +0 -0
  84. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/messages/persistent.py +0 -0
  85. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/messages/serialization.py +0 -0
  86. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/messages/validation.py +0 -0
  87. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/migration/__init__.py +0 -0
  88. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/migration/io.py +0 -0
  89. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/migration/manifest.py +0 -0
  90. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/migration/plans.py +0 -0
  91. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/production/common.py +0 -0
  92. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/production/validation.py +0 -0
  93. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/runtime/__init__.py +0 -0
  94. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/runtime/director.py +0 -0
  95. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/runtime/environment.py +0 -0
  96. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/runtime/iris.py +0 -0
  97. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/runtime/remote/__init__.py +0 -0
  98. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/runtime/remote/client.py +0 -0
  99. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/runtime/remote/migration.py +0 -0
  100. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/runtime/remote/settings.py +0 -0
  101. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iop/runtime/remote/setup.py +0 -0
  102. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iris_pex_embedded_python.egg-info/dependency_links.txt +0 -0
  103. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iris_pex_embedded_python.egg-info/entry_points.txt +0 -0
  104. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/src/iris_pex_embedded_python.egg-info/requires.txt +0 -0
  105. {iris_pex_embedded_python-4.0.0b8 → iris_pex_embedded_python-4.0.0b10}/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.0b8
3
+ Version: 4.0.0b10
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.0b8"
6
+ version = "4.0.0b10"
7
7
  description = "Iris Interoperability based on Embedded Python"
8
8
  readme = "README.md"
9
9
  requires-python = ">=3.10"
@@ -63,6 +63,9 @@ markers = [
63
63
  "e2e_remote: end-to-end test requiring a remote IRIS instance via REST API",
64
64
  ]
65
65
 
66
+ [tool.pyright]
67
+ pythonVersion = "3.10"
68
+
66
69
  [tool.ruff]
67
70
  target-version = "py310"
68
71
  line-length = 88
@@ -29,19 +29,23 @@ from iop.migration.utils import unregister_component as unregister_component
29
29
  from iop.production import ComponentItem as ComponentItem
30
30
  from iop.production import ComponentRef as ComponentRef
31
31
  from iop.production import OperationItem as OperationItem
32
- from iop.production import Port as Port
33
32
  from iop.production import ProcessItem as ProcessItem
34
33
  from iop.production import Production as Production
34
+ from iop.production import ProductionApplyResult as ProductionApplyResult
35
+ from iop.production import ProductionChangePlan as ProductionChangePlan
35
36
  from iop.production import ProductionDiff as ProductionDiff
36
37
  from iop.production import ProductionDiffEntry as ProductionDiffEntry
37
38
  from iop.production import ProductionGraph as ProductionGraph
39
+ from iop.production import ProductionPlanOperation as ProductionPlanOperation
38
40
  from iop.production import ProductionValidationError as ProductionValidationError
39
41
  from iop.production import ProductionValidationIssue as ProductionValidationIssue
40
42
  from iop.production import ProductionValidationReport as ProductionValidationReport
41
43
  from iop.production import ProductionValidationWarning as ProductionValidationWarning
44
+ from iop.production import ProductionVerifyResult as ProductionVerifyResult
42
45
  from iop.production import Route as Route
43
46
  from iop.production import ServiceItem as ServiceItem
44
47
  from iop.production import TargetSetting as TargetSetting
48
+ from iop.production import TargetSettingRef as TargetSettingRef
45
49
  from iop.production import target as target
46
50
  from iop.runtime.director import _Director
47
51
  from iop.runtime.protocol import DirectorProtocol as DirectorProtocol
@@ -67,12 +71,15 @@ __all__ = [
67
71
  "PersistentMessage",
68
72
  "PickleMessage",
69
73
  "PollingBusinessService",
70
- "Port",
71
74
  "ProcessItem",
72
75
  "Production",
76
+ "ProductionApplyResult",
77
+ "ProductionChangePlan",
73
78
  "ProductionDiff",
74
79
  "ProductionDiffEntry",
75
80
  "ProductionGraph",
81
+ "ProductionPlanOperation",
82
+ "ProductionVerifyResult",
76
83
  "ProductionValidationError",
77
84
  "ProductionValidationIssue",
78
85
  "ProductionValidationReport",
@@ -83,6 +90,7 @@ __all__ = [
83
90
  "ServiceItem",
84
91
  "Setting",
85
92
  "TargetSetting",
93
+ "TargetSettingRef",
86
94
  "Utils",
87
95
  "bind_component",
88
96
  "controls",
@@ -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:
@@ -236,6 +256,9 @@ class Command:
236
256
  if export_format == "graph":
237
257
  print(production.graph())
238
258
  return
259
+ if export_format == "mermaid":
260
+ print(production.to_mermaid(), end="")
261
+ return
239
262
  raise ValueError(f"Unsupported export format: {export_format}")
240
263
 
241
264
  def _handle_migrate(self) -> None:
@@ -324,6 +347,55 @@ class Command:
324
347
  self.director.unbind_component(self.args.unbind)
325
348
  print(f"Removed binding {self.args.unbind}.")
326
349
 
350
+ def _handle_plan(self) -> None:
351
+ production = self._load_production_from_settings(
352
+ self.args.plan,
353
+ self.args.production,
354
+ )
355
+ plan = production.plan()
356
+ if self.args.out:
357
+ plan.save(self._absolute_path(self.args.out))
358
+ print(f"Production change plan written to {self.args.out}")
359
+ else:
360
+ print(plan)
361
+
362
+ def _handle_review_plan(self) -> None:
363
+ plan = ProductionChangePlan.load(self._absolute_path(self.args.review_plan))
364
+ print(plan)
365
+
366
+ def _handle_apply_plan(self) -> None:
367
+ if not self.args.settings:
368
+ raise ValueError("--apply-plan requires --settings.")
369
+ plan = ProductionChangePlan.load(self._absolute_path(self.args.apply_plan))
370
+ production = self._load_production_from_settings(
371
+ self.args.settings,
372
+ self.args.production or plan.production_name,
373
+ )
374
+ result = production.apply(
375
+ plan,
376
+ allow_destructive=self.args.allow_destructive,
377
+ backup_dir=self.args.backup_dir,
378
+ )
379
+ print(result)
380
+
381
+ def _handle_verify_plan(self) -> None:
382
+ plan = ProductionChangePlan.load(self._absolute_path(self.args.verify_plan))
383
+ production = Production(
384
+ self.args.production or plan.production_name,
385
+ namespace=self.director.namespace,
386
+ director=self.director,
387
+ )
388
+ print(production.verify(plan))
389
+
390
+ def _handle_rollback_backup(self) -> None:
391
+ result = Production.rollback_backup(
392
+ self._absolute_path(self.args.rollback_backup),
393
+ director=self.director,
394
+ namespace=self.director.namespace,
395
+ allow_destructive=self.args.allow_destructive,
396
+ )
397
+ print(result)
398
+
327
399
  def _handle_help(self) -> None:
328
400
  create_parser().print_help()
329
401
  if self._is_remote:
@@ -334,6 +406,73 @@ class Command:
334
406
  except Exception:
335
407
  logging.warning("Could not retrieve default production.")
336
408
 
409
+ def _load_production_from_settings(
410
+ self,
411
+ settings_file: str | None,
412
+ production_name: str | None,
413
+ ) -> Production:
414
+ if not settings_file:
415
+ raise ValueError("A settings.py file is required.")
416
+ settings_path = self._absolute_path(settings_file)
417
+ settings, path_added = migration_utils._load_settings(settings_path)
418
+ try:
419
+ production = _select_production(
420
+ getattr(settings, "PRODUCTIONS", None),
421
+ production_name,
422
+ director=self.director,
423
+ namespace=self.director.namespace,
424
+ )
425
+ production.with_director(self.director)
426
+ if not production.namespace:
427
+ production.in_namespace(self.director.namespace)
428
+ return production
429
+ finally:
430
+ migration_utils._cleanup_sys_path(path_added)
431
+
432
+ @staticmethod
433
+ def _absolute_path(path: str | None) -> str:
434
+ if not path:
435
+ raise ValueError("Path is required.")
436
+ if os.path.isabs(path):
437
+ return path
438
+ return os.path.join(os.getcwd(), path)
439
+
440
+
441
+ def _select_production(
442
+ productions,
443
+ production_name: str | None,
444
+ *,
445
+ director: DirectorProtocol,
446
+ namespace: str | None,
447
+ ) -> Production:
448
+ if not isinstance(productions, list):
449
+ raise ValueError("settings.py must define PRODUCTIONS as a list.")
450
+ candidates: list[Production] = []
451
+ for entry in productions:
452
+ if isinstance(entry, Production):
453
+ candidate = entry
454
+ elif isinstance(entry, dict):
455
+ candidate = Production.from_dict(
456
+ entry,
457
+ director=director,
458
+ namespace=namespace,
459
+ )
460
+ else:
461
+ continue
462
+ candidates.append(candidate)
463
+ if production_name:
464
+ for candidate in candidates:
465
+ if candidate.name == production_name:
466
+ return candidate
467
+ raise ValueError(f"Production {production_name!r} not found in settings.")
468
+ if len(candidates) == 1:
469
+ return candidates[0]
470
+ names = ", ".join(candidate.name for candidate in candidates) or "none"
471
+ raise ValueError(
472
+ "Production name is required when settings.py contains multiple "
473
+ f"productions ({names})."
474
+ )
475
+
337
476
 
338
477
  def main(argv=None) -> None:
339
478
  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(
@@ -105,7 +116,7 @@ def create_parser() -> argparse.ArgumentParser:
105
116
  export.add_argument(
106
117
  "--format",
107
118
  dest="export_format",
108
- choices=("json", "python", "class", "graph"),
119
+ choices=("json", "python", "class", "graph", "mermaid"),
109
120
  default="json",
110
121
  help="export format for -e/--export",
111
122
  )
@@ -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