iris-pex-embedded-python 3.7.2b1__py3-none-any.whl → 4.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- iop/__init__.py +280 -13
- iop/__main__.py +3 -3
- iop/cli/__init__.py +1 -0
- iop/cli/formatting.py +45 -0
- iop/cli/main.py +504 -0
- iop/cli/parser.py +165 -0
- iop/cli/types.py +73 -0
- iop/cls/IOP/Common.cls +11 -2
- iop/cls/IOP/PrivateSession/Duplex.cls +1 -1
- iop/cls/IOP/Service/Remote/Rest/v1.cls +152 -5
- iop/cls/IOP/Utils.cls +835 -8
- iop/cls/IOP/Wrapper.cls +1 -1
- iop/components/__init__.py +16 -0
- iop/{_async_request.py → components/async_request.py} +7 -7
- iop/components/business_host.py +363 -0
- iop/components/business_operation.py +84 -0
- iop/{_business_process.py → components/business_process.py} +145 -125
- iop/components/business_service.py +106 -0
- iop/components/common.py +574 -0
- iop/{_debugpy.py → components/debugpy.py} +11 -11
- iop/{_generator_request.py → components/generator_request.py} +4 -4
- iop/{_inbound_adapter.py → components/inbound_adapter.py} +10 -18
- iop/{_log_manager.py → components/log_manager.py} +1 -1
- iop/{_outbound_adapter.py → components/outbound_adapter.py} +1 -1
- iop/components/polling_business_service.py +48 -0
- iop/{_private_session_duplex.py → components/private_session_duplex.py} +7 -9
- iop/{_private_session_process.py → components/private_session_process.py} +2 -2
- iop/components/settings.py +208 -0
- iop/messages/__init__.py +7 -0
- iop/messages/base.py +61 -0
- iop/{_decorators.py → messages/decorators.py} +8 -2
- iop/messages/dispatch.py +351 -0
- iop/{_persistent_message.py → messages/persistent.py} +24 -19
- iop/{_serialization.py → messages/serialization.py} +34 -21
- iop/{_message_validator.py → messages/validation.py} +15 -13
- iop/migration/__init__.py +1 -0
- iop/migration/io.py +61 -0
- iop/migration/manifest.py +534 -0
- iop/migration/plans.py +80 -0
- iop/migration/utils.py +1126 -0
- iop/production/__init__.py +55 -0
- iop/production/actions.py +525 -0
- iop/production/common.py +103 -0
- iop/production/component.py +234 -0
- iop/production/declarations.py +190 -0
- iop/production/declarative.py +214 -0
- iop/production/diff.py +327 -0
- iop/production/import_.py +296 -0
- iop/production/inspection.py +92 -0
- iop/production/model.py +1143 -0
- iop/production/planning.py +678 -0
- iop/production/reconstruction.py +341 -0
- iop/production/rendering.py +610 -0
- iop/production/runtime.py +91 -0
- iop/production/source_inference.py +729 -0
- iop/production/types.py +546 -0
- iop/production/validation.py +452 -0
- iop/runtime/__init__.py +1 -0
- iop/runtime/director.py +415 -0
- iop/runtime/environment.py +59 -0
- iop/{_iris.py → runtime/iris.py} +2 -2
- iop/runtime/local.py +145 -0
- iop/{_director_protocol.py → runtime/protocol.py} +25 -8
- iop/runtime/remote/__init__.py +12 -0
- iop/runtime/remote/client.py +90 -0
- iop/runtime/remote/director.py +283 -0
- iop/runtime/remote/migration.py +57 -0
- iop/runtime/remote/settings.py +69 -0
- iop/runtime/remote/setup.py +72 -0
- {iris_pex_embedded_python-3.7.2b1.dist-info → iris_pex_embedded_python-4.0.0.dist-info}/METADATA +11 -2
- iris_pex_embedded_python-4.0.0.dist-info/RECORD +99 -0
- iris_pex_embedded_python-4.0.0.dist-info/entry_points.txt +2 -0
- iris_pex_embedded_python-4.0.0.dist-info/top_level.txt +1 -0
- grongier/__init__.py +0 -0
- grongier/cls/Grongier/PEX/BusinessOperation.cls +0 -8
- grongier/cls/Grongier/PEX/BusinessProcess.cls +0 -13
- grongier/cls/Grongier/PEX/BusinessService.cls +0 -8
- grongier/cls/Grongier/PEX/Common.cls +0 -10
- grongier/cls/Grongier/PEX/Director.cls +0 -10
- grongier/cls/Grongier/PEX/Duplex/Operation.cls +0 -4
- grongier/cls/Grongier/PEX/Duplex/Process.cls +0 -13
- grongier/cls/Grongier/PEX/Duplex/Service.cls +0 -4
- grongier/cls/Grongier/PEX/InboundAdapter.cls +0 -8
- grongier/cls/Grongier/PEX/Message.cls +0 -13
- grongier/cls/Grongier/PEX/OutboundAdapter.cls +0 -8
- grongier/cls/Grongier/PEX/PickleMessage.cls +0 -13
- grongier/cls/Grongier/PEX/PrivateSession/Duplex.cls +0 -8
- grongier/cls/Grongier/PEX/PrivateSession/Message/Ack.cls +0 -14
- grongier/cls/Grongier/PEX/PrivateSession/Message/Poll.cls +0 -14
- grongier/cls/Grongier/PEX/PrivateSession/Message/Start.cls +0 -14
- grongier/cls/Grongier/PEX/PrivateSession/Message/Stop.cls +0 -14
- grongier/cls/Grongier/PEX/Test.cls +0 -10
- grongier/cls/Grongier/PEX/Utils.cls +0 -10
- grongier/cls/Grongier/Service/WSGI.cls +0 -4
- grongier/pex/__init__.py +0 -66
- grongier/pex/__main__.py +0 -4
- grongier/pex/_business_host.py +0 -1
- grongier/pex/_cli.py +0 -4
- grongier/pex/_common.py +0 -1
- grongier/pex/_director.py +0 -1
- grongier/pex/_utils.py +0 -1
- grongier/pex/wsgi/handlers.py +0 -104
- iop/_business_host.py +0 -322
- iop/_business_operation.py +0 -77
- iop/_business_service.py +0 -67
- iop/_cli.py +0 -453
- iop/_common.py +0 -416
- iop/_director.py +0 -333
- iop/_dispatch.py +0 -194
- iop/_local.py +0 -106
- iop/_message.py +0 -45
- iop/_remote.py +0 -473
- iop/_utils.py +0 -710
- iop/cls/IOP/Service/WSGI.cls +0 -310
- iop/wsgi/handlers.py +0 -112
- iris_pex_embedded_python-3.7.2b1.dist-info/RECORD +0 -94
- iris_pex_embedded_python-3.7.2b1.dist-info/entry_points.txt +0 -2
- iris_pex_embedded_python-3.7.2b1.dist-info/top_level.txt +0 -2
- {iris_pex_embedded_python-3.7.2b1.dist-info → iris_pex_embedded_python-4.0.0.dist-info}/WHEEL +0 -0
- {iris_pex_embedded_python-3.7.2b1.dist-info → iris_pex_embedded_python-4.0.0.dist-info}/licenses/LICENSE +0 -0
iop/cli/main.py
ADDED
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from importlib.metadata import version
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
|
|
11
|
+
from ..migration import utils as migration_utils
|
|
12
|
+
from ..production import Production, ProductionChangePlan
|
|
13
|
+
from ..runtime.local import _LocalDirector
|
|
14
|
+
from ..runtime.protocol import DirectorProtocol
|
|
15
|
+
from ..runtime.remote import _RemoteDirector, get_remote_settings
|
|
16
|
+
from .formatting import format_test_response
|
|
17
|
+
from .parser import create_parser
|
|
18
|
+
from .types import CommandArgs, CommandType
|
|
19
|
+
|
|
20
|
+
_format_test_response = format_test_response
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Command:
|
|
24
|
+
def __init__(self, args: CommandArgs):
|
|
25
|
+
self.args = args
|
|
26
|
+
|
|
27
|
+
if self.args.namespace and self.args.namespace != "not_set":
|
|
28
|
+
# set environment variable IRISNAMESPACE
|
|
29
|
+
os.environ["IRISNAMESPACE"] = self.args.namespace
|
|
30
|
+
|
|
31
|
+
# Resolve director: remote when IOP_URL / IOP_SETTINGS env vars are set
|
|
32
|
+
# or when the -m settings.py file contains REMOTE_SETTINGS.
|
|
33
|
+
# --force-local overrides everything and always uses the local director.
|
|
34
|
+
if self.args.force_local:
|
|
35
|
+
self.director: DirectorProtocol = _LocalDirector()
|
|
36
|
+
self._is_remote = False
|
|
37
|
+
else:
|
|
38
|
+
# Resolve absolute paths for --remote-settings and -m so
|
|
39
|
+
# get_remote_settings can load them regardless of cwd.
|
|
40
|
+
explicit_path = self.args.remote_settings
|
|
41
|
+
if explicit_path and not os.path.isabs(explicit_path):
|
|
42
|
+
explicit_path = os.path.join(os.getcwd(), explicit_path)
|
|
43
|
+
|
|
44
|
+
migrate_path = self.args.migrate
|
|
45
|
+
if migrate_path and not os.path.isabs(migrate_path):
|
|
46
|
+
migrate_path = os.path.join(os.getcwd(), migrate_path)
|
|
47
|
+
|
|
48
|
+
remote_settings = get_remote_settings(
|
|
49
|
+
explicit_settings_path=explicit_path,
|
|
50
|
+
fallback_settings_path=migrate_path,
|
|
51
|
+
)
|
|
52
|
+
if remote_settings:
|
|
53
|
+
if self.args.namespace and self.args.namespace != "not_set":
|
|
54
|
+
remote_settings["namespace"] = self.args.namespace
|
|
55
|
+
self.director = _RemoteDirector(remote_settings)
|
|
56
|
+
self._is_remote = True
|
|
57
|
+
else:
|
|
58
|
+
self.director = _LocalDirector()
|
|
59
|
+
self._is_remote = False
|
|
60
|
+
|
|
61
|
+
def _has_primary_command(self) -> bool:
|
|
62
|
+
return any(
|
|
63
|
+
[
|
|
64
|
+
self.args.default,
|
|
65
|
+
self.args.list,
|
|
66
|
+
self.args.start,
|
|
67
|
+
self.args.stop,
|
|
68
|
+
self.args.kill,
|
|
69
|
+
self.args.restart,
|
|
70
|
+
self.args.status,
|
|
71
|
+
self.args.test,
|
|
72
|
+
self.args.version,
|
|
73
|
+
self.args.export,
|
|
74
|
+
self.args.migrate,
|
|
75
|
+
self.args.log,
|
|
76
|
+
self.args.queue,
|
|
77
|
+
self.args.init,
|
|
78
|
+
self.args.bindings,
|
|
79
|
+
self.args.unbind is not None,
|
|
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,
|
|
86
|
+
]
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def execute(self) -> None:
|
|
90
|
+
if self.args.unused and not self.args.bindings:
|
|
91
|
+
raise ValueError("--unused can only be used with --bindings.")
|
|
92
|
+
|
|
93
|
+
if self.args.namespace == "not_set" and not self._has_primary_command():
|
|
94
|
+
print(os.getenv("IRISNAMESPACE", "not set"))
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
command_type = self._determine_command_type()
|
|
98
|
+
command_handlers = {
|
|
99
|
+
CommandType.DEFAULT: self._handle_default,
|
|
100
|
+
CommandType.LIST: self._handle_list,
|
|
101
|
+
CommandType.START: self._handle_start,
|
|
102
|
+
CommandType.STOP: self._handle_stop,
|
|
103
|
+
CommandType.KILL: self._handle_kill,
|
|
104
|
+
CommandType.RESTART: self._handle_restart,
|
|
105
|
+
CommandType.STATUS: self._handle_status,
|
|
106
|
+
CommandType.TEST: self._handle_test,
|
|
107
|
+
CommandType.VERSION: self._handle_version,
|
|
108
|
+
CommandType.EXPORT: self._handle_export,
|
|
109
|
+
CommandType.MIGRATE: self._handle_migrate,
|
|
110
|
+
CommandType.LOG: self._handle_log,
|
|
111
|
+
CommandType.QUEUE: self._handle_queue,
|
|
112
|
+
CommandType.INIT: self._handle_init,
|
|
113
|
+
CommandType.BINDINGS: self._handle_bindings,
|
|
114
|
+
CommandType.UNBIND: self._handle_unbind,
|
|
115
|
+
CommandType.HELP: self._handle_help,
|
|
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,
|
|
122
|
+
}
|
|
123
|
+
handler = command_handlers.get(command_type)
|
|
124
|
+
if handler:
|
|
125
|
+
handler()
|
|
126
|
+
|
|
127
|
+
def _determine_command_type(self) -> CommandType:
|
|
128
|
+
if self.args.default:
|
|
129
|
+
return CommandType.DEFAULT
|
|
130
|
+
if self.args.list:
|
|
131
|
+
return CommandType.LIST
|
|
132
|
+
if self.args.start:
|
|
133
|
+
return CommandType.START
|
|
134
|
+
if self.args.stop:
|
|
135
|
+
return CommandType.STOP
|
|
136
|
+
if self.args.kill:
|
|
137
|
+
return CommandType.KILL
|
|
138
|
+
if self.args.restart:
|
|
139
|
+
return CommandType.RESTART
|
|
140
|
+
if self.args.status:
|
|
141
|
+
return CommandType.STATUS
|
|
142
|
+
if self.args.test:
|
|
143
|
+
return CommandType.TEST
|
|
144
|
+
if self.args.version:
|
|
145
|
+
return CommandType.VERSION
|
|
146
|
+
if self.args.export:
|
|
147
|
+
return CommandType.EXPORT
|
|
148
|
+
if self.args.migrate:
|
|
149
|
+
return CommandType.MIGRATE
|
|
150
|
+
if self.args.log:
|
|
151
|
+
return CommandType.LOG
|
|
152
|
+
if self.args.queue:
|
|
153
|
+
return CommandType.QUEUE
|
|
154
|
+
if self.args.init:
|
|
155
|
+
return CommandType.INIT
|
|
156
|
+
if self.args.bindings:
|
|
157
|
+
return CommandType.BINDINGS
|
|
158
|
+
if self.args.unbind is not None:
|
|
159
|
+
return CommandType.UNBIND
|
|
160
|
+
if self.args.update:
|
|
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
|
|
172
|
+
return CommandType.HELP
|
|
173
|
+
|
|
174
|
+
def _handle_default(self) -> None:
|
|
175
|
+
if self.args.default == "not_set":
|
|
176
|
+
print(self.director.get_default_production())
|
|
177
|
+
elif self.args.default is not None:
|
|
178
|
+
self.director.set_default_production(self.args.default)
|
|
179
|
+
|
|
180
|
+
def _handle_list(self) -> None:
|
|
181
|
+
dikt = self.director.list_productions()
|
|
182
|
+
print(json.dumps(dikt, indent=4))
|
|
183
|
+
|
|
184
|
+
def _handle_start(self) -> None:
|
|
185
|
+
if self.args.start != "not_set":
|
|
186
|
+
production_name = self.args.start
|
|
187
|
+
else:
|
|
188
|
+
production_name = self.director.get_default_production()
|
|
189
|
+
if not production_name or production_name == "Not defined":
|
|
190
|
+
print(
|
|
191
|
+
"Error: no production name provided and no default production is defined.",
|
|
192
|
+
file=sys.stderr,
|
|
193
|
+
)
|
|
194
|
+
sys.exit(1)
|
|
195
|
+
if self.args.detach:
|
|
196
|
+
self.director.start_production(production_name)
|
|
197
|
+
print(f"Production {production_name} started")
|
|
198
|
+
else:
|
|
199
|
+
self.director.start_production_with_log(production_name)
|
|
200
|
+
|
|
201
|
+
def _handle_stop(self) -> None:
|
|
202
|
+
self.director.stop_production()
|
|
203
|
+
print(f"Production {self.director.get_default_production()} stopped")
|
|
204
|
+
|
|
205
|
+
def _handle_kill(self) -> None:
|
|
206
|
+
self.director.shutdown_production()
|
|
207
|
+
|
|
208
|
+
def _handle_restart(self) -> None:
|
|
209
|
+
self.director.restart_production()
|
|
210
|
+
|
|
211
|
+
def _handle_status(self) -> None:
|
|
212
|
+
print(json.dumps(self.director.status_production(), indent=4))
|
|
213
|
+
|
|
214
|
+
def _handle_update(self) -> None:
|
|
215
|
+
self.director.update_production()
|
|
216
|
+
|
|
217
|
+
def _handle_test(self) -> None:
|
|
218
|
+
test_name = None if self.args.test == "not_set" else self.args.test
|
|
219
|
+
classname = self.args.classname if self.args.classname != "not_set" else None
|
|
220
|
+
body = self.args.body if self.args.body != "not_set" else None
|
|
221
|
+
|
|
222
|
+
# Support @filename.json body expansion
|
|
223
|
+
if body and body.startswith("@"):
|
|
224
|
+
filepath = body[1:]
|
|
225
|
+
if not os.path.isabs(filepath):
|
|
226
|
+
filepath = os.path.join(os.getcwd(), filepath)
|
|
227
|
+
with open(filepath, encoding="utf-8") as fh:
|
|
228
|
+
body = fh.read()
|
|
229
|
+
|
|
230
|
+
response = self.director.test_component(
|
|
231
|
+
test_name, classname=classname, body=body
|
|
232
|
+
)
|
|
233
|
+
print(format_test_response(response))
|
|
234
|
+
|
|
235
|
+
def _handle_version(self) -> None:
|
|
236
|
+
print(version("iris-pex-embedded-python"))
|
|
237
|
+
|
|
238
|
+
def _handle_export(self) -> None:
|
|
239
|
+
export_name = (
|
|
240
|
+
self.director.get_default_production()
|
|
241
|
+
if self.args.export == "not_set"
|
|
242
|
+
else self.args.export
|
|
243
|
+
)
|
|
244
|
+
export_format = self.args.export_format or "json"
|
|
245
|
+
if export_format == "json":
|
|
246
|
+
print(json.dumps(self.director.export_production(export_name), indent=4))
|
|
247
|
+
return
|
|
248
|
+
|
|
249
|
+
production = Production.from_iris(export_name, director=self.director)
|
|
250
|
+
if export_format == "python":
|
|
251
|
+
print(production.to_python(), end="")
|
|
252
|
+
return
|
|
253
|
+
if export_format == "class":
|
|
254
|
+
print(production.to_class(), end="")
|
|
255
|
+
return
|
|
256
|
+
if export_format == "graph":
|
|
257
|
+
print(production.graph())
|
|
258
|
+
return
|
|
259
|
+
if export_format == "mermaid":
|
|
260
|
+
print(production.to_mermaid(), end="")
|
|
261
|
+
return
|
|
262
|
+
raise ValueError(f"Unsupported export format: {export_format}")
|
|
263
|
+
|
|
264
|
+
def _handle_migrate(self) -> None:
|
|
265
|
+
migrate_path = self.args.migrate
|
|
266
|
+
if migrate_path is not None:
|
|
267
|
+
if not os.path.isabs(migrate_path):
|
|
268
|
+
migrate_path = os.path.join(os.getcwd(), migrate_path)
|
|
269
|
+
mode = "REMOTE" if self._is_remote else "LOCAL"
|
|
270
|
+
if self.args.migration_plan:
|
|
271
|
+
print(
|
|
272
|
+
migration_utils.explain_migration(
|
|
273
|
+
migrate_path,
|
|
274
|
+
mode=mode,
|
|
275
|
+
namespace=self.director.namespace,
|
|
276
|
+
strict_production_validation=(
|
|
277
|
+
self.args.strict_production_validation
|
|
278
|
+
),
|
|
279
|
+
)
|
|
280
|
+
)
|
|
281
|
+
return
|
|
282
|
+
if self._is_remote:
|
|
283
|
+
print(
|
|
284
|
+
migration_utils.explain_migration(
|
|
285
|
+
migrate_path,
|
|
286
|
+
mode=mode,
|
|
287
|
+
namespace=self.director.namespace,
|
|
288
|
+
strict_production_validation=(
|
|
289
|
+
self.args.strict_production_validation
|
|
290
|
+
),
|
|
291
|
+
)
|
|
292
|
+
)
|
|
293
|
+
if self.args.strict_production_validation:
|
|
294
|
+
self.director.migrate(
|
|
295
|
+
migrate_path,
|
|
296
|
+
strict_production_validation=True,
|
|
297
|
+
)
|
|
298
|
+
else:
|
|
299
|
+
self.director.migrate(migrate_path)
|
|
300
|
+
if self._is_remote:
|
|
301
|
+
print(
|
|
302
|
+
migration_utils.format_migration_success(
|
|
303
|
+
migrate_path, namespace=self.director.namespace
|
|
304
|
+
)
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
def _handle_log(self) -> None:
|
|
308
|
+
if self.args.log == "not_set":
|
|
309
|
+
self.director.log_production()
|
|
310
|
+
elif self.args.log is not None:
|
|
311
|
+
self.director.log_production_top(int(self.args.log))
|
|
312
|
+
|
|
313
|
+
def _handle_queue(self) -> None:
|
|
314
|
+
production_name = (
|
|
315
|
+
self.director.get_default_production()
|
|
316
|
+
if self.args.queue == "not_set"
|
|
317
|
+
else self.args.queue
|
|
318
|
+
)
|
|
319
|
+
if not production_name or production_name == "Not defined":
|
|
320
|
+
print(
|
|
321
|
+
"Error: no production name provided and no default production is defined.",
|
|
322
|
+
file=sys.stderr,
|
|
323
|
+
)
|
|
324
|
+
sys.exit(1)
|
|
325
|
+
print(
|
|
326
|
+
json.dumps(
|
|
327
|
+
self.director.export_production_queue_info(production_name),
|
|
328
|
+
indent=4,
|
|
329
|
+
)
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
def _handle_init(self) -> None:
|
|
333
|
+
path = None if self.args.init == "not_set" else self.args.init
|
|
334
|
+
self.director.setup(path)
|
|
335
|
+
|
|
336
|
+
def _handle_bindings(self) -> None:
|
|
337
|
+
print(
|
|
338
|
+
json.dumps(
|
|
339
|
+
self.director.list_bindings(unused_only=self.args.unused),
|
|
340
|
+
indent=4,
|
|
341
|
+
)
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
def _handle_unbind(self) -> None:
|
|
345
|
+
if not self.args.unbind:
|
|
346
|
+
raise ValueError("IRIS class name is required.")
|
|
347
|
+
self.director.unbind_component(self.args.unbind)
|
|
348
|
+
print(f"Removed binding {self.args.unbind}.")
|
|
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
|
+
|
|
399
|
+
def _handle_help(self) -> None:
|
|
400
|
+
create_parser().print_help()
|
|
401
|
+
if self._is_remote:
|
|
402
|
+
print(f"\nMode: REMOTE ({os.environ.get('IOP_URL', 'via IOP_SETTINGS')})")
|
|
403
|
+
try:
|
|
404
|
+
print(f"\nDefault production: {self.director.get_default_production()}")
|
|
405
|
+
print(f"\nNamespace: {self.director.namespace}")
|
|
406
|
+
except Exception:
|
|
407
|
+
logging.warning("Could not retrieve default production.")
|
|
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
|
+
|
|
476
|
+
|
|
477
|
+
def main(argv=None) -> None:
|
|
478
|
+
parser = create_parser()
|
|
479
|
+
args = parser.parse_args(argv)
|
|
480
|
+
cmd_args = CommandArgs(**vars(args))
|
|
481
|
+
|
|
482
|
+
try:
|
|
483
|
+
command = Command(cmd_args)
|
|
484
|
+
command.execute()
|
|
485
|
+
except requests.exceptions.ConnectionError as exc:
|
|
486
|
+
url = os.environ.get("IOP_URL", "")
|
|
487
|
+
msg = (
|
|
488
|
+
f"Connection error: could not reach {url!r}"
|
|
489
|
+
if url
|
|
490
|
+
else f"Connection error: {exc}"
|
|
491
|
+
)
|
|
492
|
+
print(f"Error: {msg}", file=sys.stderr)
|
|
493
|
+
sys.exit(1)
|
|
494
|
+
except requests.exceptions.HTTPError as exc:
|
|
495
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
496
|
+
sys.exit(1)
|
|
497
|
+
except (RuntimeError, ValueError) as exc:
|
|
498
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
499
|
+
sys.exit(1)
|
|
500
|
+
sys.exit(0)
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
if __name__ == "__main__":
|
|
504
|
+
main()
|
iop/cli/parser.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def create_parser() -> argparse.ArgumentParser:
|
|
7
|
+
"""Create and configure the IOP CLI parser."""
|
|
8
|
+
main_parser = argparse.ArgumentParser(
|
|
9
|
+
epilog=(
|
|
10
|
+
"Remote mode: set IOP_URL (e.g. http://localhost:8080) to run all commands\n"
|
|
11
|
+
"against a remote IRIS instance via its REST API. Optional env vars:\n"
|
|
12
|
+
" IOP_USERNAME, IOP_PASSWORD, IOP_NAMESPACE (default: USER),\n"
|
|
13
|
+
" IOP_VERIFY_SSL (set to 0 to disable TLS verification).\n"
|
|
14
|
+
"Alternatively use -R /path/to/settings.py or set IOP_SETTINGS=\n"
|
|
15
|
+
"(file must contain a REMOTE_SETTINGS dict with at least 'url').\n"
|
|
16
|
+
"Use --force-local to suppress remote mode entirely."
|
|
17
|
+
),
|
|
18
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
19
|
+
)
|
|
20
|
+
parser = main_parser.add_mutually_exclusive_group()
|
|
21
|
+
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"-d", "--default", help="set the default production", nargs="?", const="not_set"
|
|
24
|
+
)
|
|
25
|
+
parser.add_argument("-l", "--list", help="list productions", action="store_true")
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"-s", "--start", help="start a production", nargs="?", const="not_set"
|
|
28
|
+
)
|
|
29
|
+
parser.add_argument("-S", "--stop", help="stop a production", action="store_true")
|
|
30
|
+
parser.add_argument("-k", "--kill", help="kill a production", action="store_true")
|
|
31
|
+
parser.add_argument("-r", "--restart", help="restart a production", action="store_true")
|
|
32
|
+
parser.add_argument("-x", "--status", help="status a production", action="store_true")
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
"-m",
|
|
35
|
+
"-M",
|
|
36
|
+
"--migrate",
|
|
37
|
+
help="migrate production and classes with a Python migration file",
|
|
38
|
+
)
|
|
39
|
+
parser.add_argument(
|
|
40
|
+
"-e", "--export", help="export a production", nargs="?", const="not_set"
|
|
41
|
+
)
|
|
42
|
+
parser.add_argument("-v", "--version", help="display version", action="store_true")
|
|
43
|
+
parser.add_argument("-L", "--log", help="display log", nargs="?", const="not_set")
|
|
44
|
+
parser.add_argument(
|
|
45
|
+
"-q",
|
|
46
|
+
"--queue",
|
|
47
|
+
help="display runtime queue information",
|
|
48
|
+
nargs="?",
|
|
49
|
+
const="not_set",
|
|
50
|
+
)
|
|
51
|
+
parser.add_argument(
|
|
52
|
+
"-i", "--init", help="init the iop module in iris", nargs="?", const="not_set"
|
|
53
|
+
)
|
|
54
|
+
parser.add_argument(
|
|
55
|
+
"--bindings",
|
|
56
|
+
help="list IOP-generated IRIS proxy class bindings",
|
|
57
|
+
action="store_true",
|
|
58
|
+
)
|
|
59
|
+
parser.add_argument(
|
|
60
|
+
"--unbind",
|
|
61
|
+
help="remove an IOP-generated IRIS proxy class binding",
|
|
62
|
+
)
|
|
63
|
+
parser.add_argument(
|
|
64
|
+
"-t", "--test", help="test the iop module in iris", nargs="?", const="not_set"
|
|
65
|
+
)
|
|
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
|
+
)
|
|
78
|
+
|
|
79
|
+
start = main_parser.add_argument_group("start arguments")
|
|
80
|
+
start.add_argument(
|
|
81
|
+
"-D", "--detach", help="start a production in detach mode", action="store_true"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
test = main_parser.add_argument_group("test arguments")
|
|
85
|
+
test.add_argument(
|
|
86
|
+
"-C", "--classname", help="test classname", nargs="?", const="not_set"
|
|
87
|
+
)
|
|
88
|
+
test.add_argument(
|
|
89
|
+
"-B",
|
|
90
|
+
"--body",
|
|
91
|
+
help="test body (JSON string or @path/to/file.json)",
|
|
92
|
+
nargs="?",
|
|
93
|
+
const="not_set",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
migrate = main_parser.add_argument_group("migrate arguments")
|
|
97
|
+
migrate.add_argument(
|
|
98
|
+
"--force-local",
|
|
99
|
+
help="force local mode, skip remote even if REMOTE_SETTINGS or IOP_URL is present",
|
|
100
|
+
action="store_true",
|
|
101
|
+
)
|
|
102
|
+
migrate.add_argument(
|
|
103
|
+
"--dry-run",
|
|
104
|
+
"--explain",
|
|
105
|
+
dest="migration_plan",
|
|
106
|
+
help="show the migration plan and validation messages without writing to IRIS",
|
|
107
|
+
action="store_true",
|
|
108
|
+
)
|
|
109
|
+
migrate.add_argument(
|
|
110
|
+
"--strict-production-validation",
|
|
111
|
+
help="fail migration when production validation reports issues",
|
|
112
|
+
action="store_true",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
export = main_parser.add_argument_group("export arguments")
|
|
116
|
+
export.add_argument(
|
|
117
|
+
"--format",
|
|
118
|
+
dest="export_format",
|
|
119
|
+
choices=("json", "python", "class", "graph", "mermaid"),
|
|
120
|
+
default="json",
|
|
121
|
+
help="export format for -e/--export",
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
bindings = main_parser.add_argument_group("bindings arguments")
|
|
125
|
+
bindings.add_argument(
|
|
126
|
+
"--unused",
|
|
127
|
+
help="with --bindings, show only proxy classes unused by productions",
|
|
128
|
+
action="store_true",
|
|
129
|
+
)
|
|
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
|
+
|
|
152
|
+
remote = main_parser.add_argument_group("remote arguments")
|
|
153
|
+
remote.add_argument(
|
|
154
|
+
"-R",
|
|
155
|
+
"--remote-settings",
|
|
156
|
+
help="path to a settings.py containing REMOTE_SETTINGS (overrides IOP_SETTINGS env var)",
|
|
157
|
+
metavar="FILE",
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
namespace = main_parser.add_argument_group("namespace arguments")
|
|
161
|
+
namespace.add_argument(
|
|
162
|
+
"-n", "--namespace", help="set namespace", nargs="?", const="not_set"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
return main_parser
|