iris-pex-embedded-python 3.5.6b10__tar.gz → 3.5.6b11__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. {iris_pex_embedded_python-3.5.6b10/src/iris_pex_embedded_python.egg-info → iris_pex_embedded_python-3.5.6b11}/PKG-INFO +1 -1
  2. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/pyproject.toml +1 -1
  3. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/_cli.py +29 -14
  4. iris_pex_embedded_python-3.5.6b11/src/iop/_director_protocol.py +75 -0
  5. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/_local.py +30 -3
  6. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/_remote.py +178 -12
  7. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/_utils.py +1 -108
  8. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/cls/IOP/Service/Remote/Rest/v1.cls +1 -0
  9. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11/src/iris_pex_embedded_python.egg-info}/PKG-INFO +1 -1
  10. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iris_pex_embedded_python.egg-info/SOURCES.txt +1 -0
  11. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/LICENSE +0 -0
  12. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/README.md +0 -0
  13. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/setup.cfg +0 -0
  14. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/setup.py +0 -0
  15. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/grongier/__init__.py +0 -0
  16. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/grongier/cls/Grongier/PEX/BusinessOperation.cls +0 -0
  17. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/grongier/cls/Grongier/PEX/BusinessProcess.cls +0 -0
  18. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/grongier/cls/Grongier/PEX/BusinessService.cls +0 -0
  19. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/grongier/cls/Grongier/PEX/Common.cls +0 -0
  20. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/grongier/cls/Grongier/PEX/Director.cls +0 -0
  21. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/grongier/cls/Grongier/PEX/Duplex/Operation.cls +0 -0
  22. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/grongier/cls/Grongier/PEX/Duplex/Process.cls +0 -0
  23. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/grongier/cls/Grongier/PEX/Duplex/Service.cls +0 -0
  24. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/grongier/cls/Grongier/PEX/InboundAdapter.cls +0 -0
  25. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/grongier/cls/Grongier/PEX/Message.cls +0 -0
  26. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/grongier/cls/Grongier/PEX/OutboundAdapter.cls +0 -0
  27. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/grongier/cls/Grongier/PEX/PickleMessage.cls +0 -0
  28. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/grongier/cls/Grongier/PEX/PrivateSession/Duplex.cls +0 -0
  29. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/grongier/cls/Grongier/PEX/PrivateSession/Message/Ack.cls +0 -0
  30. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/grongier/cls/Grongier/PEX/PrivateSession/Message/Poll.cls +0 -0
  31. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/grongier/cls/Grongier/PEX/PrivateSession/Message/Start.cls +0 -0
  32. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/grongier/cls/Grongier/PEX/PrivateSession/Message/Stop.cls +0 -0
  33. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/grongier/cls/Grongier/PEX/Test.cls +0 -0
  34. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/grongier/cls/Grongier/PEX/Utils.cls +0 -0
  35. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/grongier/cls/Grongier/Service/WSGI.cls +0 -0
  36. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/grongier/pex/__init__.py +0 -0
  37. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/grongier/pex/__main__.py +0 -0
  38. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/grongier/pex/_business_host.py +0 -0
  39. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/grongier/pex/_cli.py +0 -0
  40. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/grongier/pex/_common.py +0 -0
  41. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/grongier/pex/_director.py +0 -0
  42. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/grongier/pex/_utils.py +0 -0
  43. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/grongier/pex/wsgi/handlers.py +0 -0
  44. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/__init__.py +0 -0
  45. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/__main__.py +0 -0
  46. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/_async_request.py +0 -0
  47. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/_business_host.py +0 -0
  48. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/_business_operation.py +0 -0
  49. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/_business_process.py +0 -0
  50. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/_business_service.py +0 -0
  51. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/_common.py +0 -0
  52. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/_debugpy.py +0 -0
  53. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/_decorators.py +0 -0
  54. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/_director.py +0 -0
  55. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/_dispatch.py +0 -0
  56. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/_generator_request.py +0 -0
  57. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/_inbound_adapter.py +0 -0
  58. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/_iris.py +0 -0
  59. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/_log_manager.py +0 -0
  60. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/_message.py +0 -0
  61. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/_message_validator.py +0 -0
  62. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/_outbound_adapter.py +0 -0
  63. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/_private_session_duplex.py +0 -0
  64. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/_private_session_process.py +0 -0
  65. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/_serialization.py +0 -0
  66. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/cls/IOP/BusinessOperation.cls +0 -0
  67. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/cls/IOP/BusinessProcess.cls +0 -0
  68. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/cls/IOP/BusinessService.cls +0 -0
  69. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/cls/IOP/Common.cls +0 -0
  70. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/cls/IOP/Director.cls +0 -0
  71. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/cls/IOP/Duplex/Operation.cls +0 -0
  72. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/cls/IOP/Duplex/Process.cls +0 -0
  73. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/cls/IOP/Duplex/Service.cls +0 -0
  74. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/cls/IOP/Generator/Message/Ack.cls +0 -0
  75. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/cls/IOP/Generator/Message/Poll.cls +0 -0
  76. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/cls/IOP/Generator/Message/Start.cls +0 -0
  77. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/cls/IOP/Generator/Message/StartPickle.cls +0 -0
  78. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/cls/IOP/Generator/Message/Stop.cls +0 -0
  79. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/cls/IOP/InboundAdapter.cls +0 -0
  80. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/cls/IOP/Message/JSONSchema.cls +0 -0
  81. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/cls/IOP/Message.cls +0 -0
  82. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/cls/IOP/OutboundAdapter.cls +0 -0
  83. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/cls/IOP/PickleMessage.cls +0 -0
  84. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/cls/IOP/PrivateSession/Duplex.cls +0 -0
  85. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/cls/IOP/PrivateSession/Message/Ack.cls +0 -0
  86. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/cls/IOP/PrivateSession/Message/Poll.cls +0 -0
  87. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/cls/IOP/PrivateSession/Message/Start.cls +0 -0
  88. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/cls/IOP/PrivateSession/Message/Stop.cls +0 -0
  89. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/cls/IOP/Projection.cls +0 -0
  90. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/cls/IOP/Service/Remote/Handler.cls +0 -0
  91. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/cls/IOP/Service/WSGI.cls +0 -0
  92. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/cls/IOP/Test.cls +0 -0
  93. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/cls/IOP/Utils.cls +0 -0
  94. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/cls/IOP/Wrapper.cls +0 -0
  95. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iop/wsgi/handlers.py +0 -0
  96. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iris_pex_embedded_python.egg-info/dependency_links.txt +0 -0
  97. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iris_pex_embedded_python.egg-info/entry_points.txt +0 -0
  98. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/src/iris_pex_embedded_python.egg-info/requires.txt +0 -0
  99. {iris_pex_embedded_python-3.5.6b10 → iris_pex_embedded_python-3.5.6b11}/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: 3.5.6b10
3
+ Version: 3.5.6b11
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 = "3.5.6b10"
6
+ version = "3.5.6b11"
7
7
  description = "Iris Interoperability based on Embedded Python"
8
8
  readme = "README.md"
9
9
  authors = [
@@ -11,7 +11,7 @@ from importlib.metadata import version
11
11
 
12
12
  from ._local import _LocalDirector
13
13
  from ._remote import _RemoteDirector, get_remote_settings
14
- from ._utils import _Utils
14
+ from ._director_protocol import DirectorProtocol
15
15
 
16
16
 
17
17
  class CommandType(Enum):
@@ -67,7 +67,7 @@ class Command:
67
67
  # or when the -m settings.py file contains REMOTE_SETTINGS.
68
68
  # --force-local overrides everything and always uses the local director.
69
69
  if self.args.force_local:
70
- self.director: _LocalDirector | _RemoteDirector = _LocalDirector()
70
+ self.director: DirectorProtocol = _LocalDirector()
71
71
  self._is_remote = False
72
72
  else:
73
73
  # Resolve absolute paths for --remote-settings and -m so
@@ -166,7 +166,13 @@ class Command:
166
166
  print(json.dumps(dikt, indent=4))
167
167
 
168
168
  def _handle_start(self) -> None:
169
- production_name = self.args.start if self.args.start != 'not_set' else self.director.get_default_production()
169
+ if self.args.start != 'not_set':
170
+ production_name = self.args.start
171
+ else:
172
+ production_name = self.director.get_default_production()
173
+ if not production_name or production_name == "Not defined":
174
+ print("Error: no production name provided and no default production is defined.", file=sys.stderr)
175
+ sys.exit(1)
170
176
  if self.args.detach:
171
177
  self.director.start_production(production_name)
172
178
  print(f"Production {production_name} started")
@@ -219,7 +225,7 @@ class Command:
219
225
  if migrate_path is not None:
220
226
  if not os.path.isabs(migrate_path):
221
227
  migrate_path = os.path.join(os.getcwd(), migrate_path)
222
- _Utils.migrate_remote(migrate_path, force_local=self.args.force_local)
228
+ self.director.migrate(migrate_path)
223
229
 
224
230
  def _handle_log(self) -> None:
225
231
  if self.args.log == 'not_set':
@@ -228,10 +234,8 @@ class Command:
228
234
  self.director.log_production_top(int(self.args.log))
229
235
 
230
236
  def _handle_init(self) -> None:
231
- if self._is_remote:
232
- logging.warning("'init' is a local-only command and cannot be run remotely.")
233
- return
234
- _Utils.setup(None)
237
+ path = None if self.args.init == 'not_set' else self.args.init
238
+ self.director.setup(path)
235
239
 
236
240
  def _handle_help(self) -> None:
237
241
  create_parser().print_help()
@@ -239,9 +243,7 @@ class Command:
239
243
  print(f"\nMode: REMOTE ({os.environ.get('IOP_URL', 'via IOP_SETTINGS')})")
240
244
  try:
241
245
  print(f"\nDefault production: {self.director.get_default_production()}")
242
- ns = (self.director._namespace # type: ignore[union-attr]
243
- if self._is_remote else os.getenv('IRISNAMESPACE', 'not set'))
244
- print(f"\nNamespace: {ns}")
246
+ print(f"\nNamespace: {self.director.namespace}")
245
247
  except Exception:
246
248
  logging.warning("Could not retrieve default production.")
247
249
 
@@ -350,12 +352,25 @@ def create_parser() -> argparse.ArgumentParser:
350
352
  return main_parser
351
353
 
352
354
  def main(argv=None) -> None:
355
+ import requests as _requests
353
356
  parser = create_parser()
354
357
  args = parser.parse_args(argv)
355
358
  cmd_args = CommandArgs(**vars(args))
356
-
357
- command = Command(cmd_args)
358
- command.execute()
359
+
360
+ try:
361
+ command = Command(cmd_args)
362
+ command.execute()
363
+ except _requests.exceptions.ConnectionError as exc:
364
+ url = os.environ.get("IOP_URL", "")
365
+ msg = f"Connection error: could not reach {url!r}" if url else f"Connection error: {exc}"
366
+ print(f"Error: {msg}", file=sys.stderr)
367
+ sys.exit(1)
368
+ except _requests.exceptions.HTTPError as exc:
369
+ print(f"Error: {exc}", file=sys.stderr)
370
+ sys.exit(1)
371
+ except RuntimeError as exc:
372
+ print(f"Error: {exc}", file=sys.stderr)
373
+ sys.exit(1)
359
374
  sys.exit(0)
360
375
 
361
376
  if __name__ == '__main__':
@@ -0,0 +1,75 @@
1
+ """Structural interface shared by _LocalDirector and _RemoteDirector.
2
+
3
+ Use ``DirectorProtocol`` for type annotations instead of the concrete union
4
+ ``_LocalDirector | _RemoteDirector``. Any object that implements all the
5
+ methods below will satisfy the protocol at type-check time (no inheritance
6
+ required).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any, Optional, Protocol, runtime_checkable
12
+
13
+
14
+ @runtime_checkable
15
+ class DirectorProtocol(Protocol):
16
+ """Interface that both _LocalDirector and _RemoteDirector must satisfy."""
17
+
18
+ # ------------------------------------------------------------------
19
+ # Production lifecycle
20
+ # ------------------------------------------------------------------
21
+
22
+ def get_default_production(self) -> str: ...
23
+ def set_default_production(self, production_name: str = "") -> None: ...
24
+ def list_productions(self) -> dict: ...
25
+ def status_production(self) -> dict: ...
26
+ def start_production(self, production_name: Optional[str] = None) -> None: ...
27
+ def start_production_with_log(self, production_name: Optional[str] = None) -> None: ...
28
+ def stop_production(self) -> None: ...
29
+ def shutdown_production(self) -> None: ...
30
+ def restart_production(self) -> None: ...
31
+ def update_production(self) -> None: ...
32
+
33
+ # ------------------------------------------------------------------
34
+ # Logging
35
+ # ------------------------------------------------------------------
36
+
37
+ def log_production_top(self, top: int = 10) -> None: ...
38
+ def log_production(self) -> None: ...
39
+
40
+ # ------------------------------------------------------------------
41
+ # Test
42
+ # ------------------------------------------------------------------
43
+
44
+ def test_component(
45
+ self,
46
+ target: Optional[str],
47
+ message=None,
48
+ classname: Optional[str] = None,
49
+ body: "str | dict | None" = None,
50
+ ) -> Any: ...
51
+
52
+ # ------------------------------------------------------------------
53
+ # Export
54
+ # ------------------------------------------------------------------
55
+
56
+ def export_production(self, production_name: str) -> dict: ...
57
+
58
+ # ------------------------------------------------------------------
59
+ # Init / setup
60
+ # ------------------------------------------------------------------
61
+
62
+ def setup(self, path: Optional[str] = None) -> None: ...
63
+
64
+ # ------------------------------------------------------------------
65
+ # Migrate
66
+ # ------------------------------------------------------------------
67
+
68
+ def migrate(self, path: str) -> None: ...
69
+
70
+ # ------------------------------------------------------------------
71
+ # Metadata
72
+ # ------------------------------------------------------------------
73
+
74
+ @property
75
+ def namespace(self) -> str: ...
@@ -6,13 +6,17 @@ _LocalDirector and _RemoteDirector without any branching.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
+ import json
10
+ import os
9
11
  from typing import Optional
10
12
 
11
13
  from ._director import _Director
12
14
  from ._utils import _Utils
15
+ from ._director_protocol import DirectorProtocol as _DirectorProtocol # noqa: F401 --- IGNORE ---
13
16
 
14
17
 
15
- class _LocalDirector:
18
+ class _LocalDirector(_DirectorProtocol):
19
+ """Local director: thin instance-method wrapper around static _Director calls."""
16
20
 
17
21
  # ------------------------------------------------------------------
18
22
  # Production lifecycle
@@ -67,7 +71,8 @@ class _LocalDirector:
67
71
  target: Optional[str],
68
72
  message=None,
69
73
  classname: Optional[str] = None,
70
- body: Optional[str] = None,
74
+ body: "str | dict | None" = None,
75
+ restart: bool = True, # ignored locally — included to satisfy DirectorProtocol
71
76
  ):
72
77
  return _Director.test_component(target, message, classname, body)
73
78
 
@@ -76,4 +81,26 @@ class _LocalDirector:
76
81
  # ------------------------------------------------------------------
77
82
 
78
83
  def export_production(self, production_name: str) -> dict:
79
- return _Utils.export_production(production_name)
84
+ return json.loads(_Utils.export_production(production_name))
85
+
86
+ # ------------------------------------------------------------------
87
+ # Init / setup
88
+ # ------------------------------------------------------------------
89
+
90
+ def setup(self, path: Optional[str] = None) -> None:
91
+ _Utils.setup(path)
92
+
93
+ # ------------------------------------------------------------------
94
+ # Migrate
95
+ # ------------------------------------------------------------------
96
+
97
+ def migrate(self, path: str) -> None:
98
+ _Utils.migrate(path)
99
+
100
+ # ------------------------------------------------------------------
101
+ # Metadata
102
+ # ------------------------------------------------------------------
103
+
104
+ @property
105
+ def namespace(self) -> str:
106
+ return os.getenv('IRISNAMESPACE', 'not set')
@@ -16,17 +16,19 @@ import json
16
16
  import os
17
17
  import signal
18
18
  import time
19
- from typing import Any, Dict, List, Optional, Union
19
+ from typing import Any, Dict, List, Optional
20
20
 
21
21
  import requests
22
22
  import urllib3
23
23
 
24
+ from ._director_protocol import DirectorProtocol as _DirectorProtocol # noqa: F401 --- IGNORE ---
24
25
 
25
- class _RemoteDirector:
26
- """Implements the same interface as _Director but dispatches over HTTP."""
26
+ class _RemoteDirector(_DirectorProtocol):
27
+ """Implements DirectorProtocol over the IOP REST API."""
27
28
 
28
29
  def __init__(self, remote_settings: Dict[str, Any]) -> None:
29
- self._base = remote_settings["url"].rstrip("/") + "/api/iop"
30
+ self._url = remote_settings["url"].rstrip("/")
31
+ self._base = self._url + "/api/iop"
30
32
  self._auth = (
31
33
  remote_settings.get("username", ""),
32
34
  remote_settings.get("password", ""),
@@ -40,13 +42,27 @@ class _RemoteDirector:
40
42
  # Internal helpers
41
43
  # ------------------------------------------------------------------
42
44
 
45
+ @staticmethod
46
+ def _raise_for_status(resp: requests.Response) -> None:
47
+ """Like resp.raise_for_status() but includes the response body error message."""
48
+ if not resp.ok:
49
+ try:
50
+ body = resp.json()
51
+ error = body.get("error") or body.get("message") or resp.text
52
+ except Exception:
53
+ error = resp.text or resp.reason
54
+ raise requests.exceptions.HTTPError(
55
+ f"{resp.status_code} {resp.reason}: {error}",
56
+ response=resp,
57
+ )
58
+
43
59
  def _get(self, path: str, params: Optional[dict] = None) -> Any:
44
60
  p = {"namespace": self._namespace, **(params or {})}
45
61
  resp = requests.get(
46
62
  f"{self._base}{path}", params=p, auth=self._auth,
47
63
  verify=self._verify, timeout=30,
48
64
  )
49
- resp.raise_for_status()
65
+ self._raise_for_status(resp)
50
66
  return resp.json()
51
67
 
52
68
  def _post(self, path: str, body: Optional[dict] = None) -> Any:
@@ -55,7 +71,7 @@ class _RemoteDirector:
55
71
  params={"namespace": self._namespace},
56
72
  auth=self._auth, verify=self._verify, timeout=30,
57
73
  )
58
- resp.raise_for_status()
74
+ self._raise_for_status(resp)
59
75
  return resp.json()
60
76
 
61
77
  def _put(self, path: str, body: Optional[dict] = None) -> Any:
@@ -64,7 +80,7 @@ class _RemoteDirector:
64
80
  params={"namespace": self._namespace},
65
81
  auth=self._auth, verify=self._verify, timeout=30,
66
82
  )
67
- resp.raise_for_status()
83
+ self._raise_for_status(resp)
68
84
  return resp.json()
69
85
 
70
86
  def _check_error(self, data: Any) -> Any:
@@ -131,11 +147,11 @@ class _RemoteDirector:
131
147
  def shutdown_production(self) -> None:
132
148
  self._check_error(self._post("/kill"))
133
149
 
134
- def restart_production(self) -> dict:
135
- return self._check_error(self._post("/restart"))
150
+ def restart_production(self) -> None:
151
+ self._check_error(self._post("/restart"))
136
152
 
137
- def update_production(self) -> dict:
138
- return self._check_error(self._post("/update"))
153
+ def update_production(self) -> None:
154
+ self._check_error(self._post("/update"))
139
155
 
140
156
  # ------------------------------------------------------------------
141
157
  # Logging
@@ -190,7 +206,7 @@ class _RemoteDirector:
190
206
  target: Optional[str],
191
207
  message=None, # ignored remotely — not serialisable over HTTP
192
208
  classname: Optional[str] = None,
193
- body: Optional[Union[str, dict]] = None,
209
+ body: "str | dict | None" = None,
194
210
  restart: bool = True,
195
211
  ) -> dict:
196
212
  """Returns a dict: {"classname": "...", "body": "...", "truncated": false}.
@@ -223,6 +239,156 @@ class _RemoteDirector:
223
239
  self._get("/export", {"production": production_name})
224
240
  )
225
241
 
242
+ # ------------------------------------------------------------------
243
+ # Migrate
244
+ # ------------------------------------------------------------------
245
+
246
+ def migrate(self, path: str) -> None:
247
+ """Upload .py and .cls files from *path*'s folder to remote IRIS via the IOP migrate API.
248
+
249
+ *path* must be an absolute path to a ``settings.py`` file whose directory
250
+ (and sub-directories) will be walked for ``.py`` / ``.cls`` files.
251
+ ``REMOTE_SETTINGS`` fields (package, namespace, remote_folder) are read
252
+ from that same settings file when present; otherwise the director's own
253
+ namespace / defaults are used.
254
+ """
255
+ import importlib.util
256
+
257
+ folder = os.path.dirname(path)
258
+
259
+ # Try to read optional keys from the settings file
260
+ package = 'python'
261
+ remote_folder = ''
262
+ try:
263
+ spec = importlib.util.spec_from_file_location('_iop_migrate_settings', path)
264
+ mod = importlib.util.module_from_spec(spec) # type: ignore[arg-type]
265
+ spec.loader.exec_module(mod) # type: ignore[union-attr]
266
+ rs = getattr(mod, 'REMOTE_SETTINGS', {})
267
+ package = rs.get('package', package)
268
+ remote_folder = rs.get('remote_folder', remote_folder)
269
+ except Exception:
270
+ pass
271
+
272
+ body: List[dict] = []
273
+ for dirpath, _, filenames in os.walk(folder):
274
+ for fname in sorted(filenames):
275
+ if not (fname.endswith('.py') or fname.endswith('.cls')):
276
+ continue
277
+ full = os.path.join(dirpath, fname)
278
+ rel = os.path.relpath(full, folder).replace(os.sep, '/')
279
+ with open(full, encoding='utf-8') as fh:
280
+ body.append({'name': rel, 'data': fh.read()})
281
+
282
+ payload = {
283
+ 'namespace': self._namespace,
284
+ 'package': package,
285
+ 'remote_folder': remote_folder,
286
+ 'body': body,
287
+ }
288
+ resp = requests.put(
289
+ f"{self._base}/migrate",
290
+ json=payload,
291
+ params={"namespace": self._namespace},
292
+ auth=self._auth,
293
+ verify=self._verify,
294
+ timeout=30,
295
+ )
296
+ self._raise_for_status(resp)
297
+ # Server returns $$$OK (the integer 1) on success — not meaningful to print
298
+
299
+ # ------------------------------------------------------------------
300
+ # Init / setup — uploads .cls files via the Atelier API
301
+ # ------------------------------------------------------------------
302
+
303
+ def setup(self, path: Optional[str] = None) -> None:
304
+ """Upload and compile IOP .cls files to remote IRIS via the Atelier REST API.
305
+
306
+ When *path* is ``None`` the bundled ``iop/cls/`` directory (and the
307
+ optional ``grongier/cls/`` directory for retrocompatibility) is used.
308
+ Any explicit *path* must point to a directory containing ``.cls`` files.
309
+ """
310
+ import importlib.resources
311
+
312
+ paths_to_upload: List[str] = []
313
+ if path is None:
314
+ try:
315
+ paths_to_upload.append(
316
+ str(importlib.resources.files('iop').joinpath('cls'))
317
+ )
318
+ except ModuleNotFoundError:
319
+ pass
320
+ try: # retrocompatibility with the grongier.pex package
321
+ paths_to_upload.append(
322
+ str(importlib.resources.files('grongier').joinpath('cls'))
323
+ )
324
+ except ModuleNotFoundError:
325
+ pass
326
+ else:
327
+ paths_to_upload.append(path)
328
+
329
+ atelier_base = f"{self._url}/api/atelier/v1"
330
+ doc_names: List[str] = []
331
+
332
+ for cls_root in paths_to_upload:
333
+ for dirpath, _, filenames in os.walk(cls_root):
334
+ for fname in sorted(filenames):
335
+ if not fname.endswith('.cls'):
336
+ continue
337
+ full_path = os.path.join(dirpath, fname)
338
+ doc_name = os.path.relpath(full_path, cls_root).replace(os.sep, '/')
339
+ with open(full_path, encoding='utf-8') as fh:
340
+ content = fh.read().splitlines()
341
+ resp = requests.put(
342
+ f"{atelier_base}/{self._namespace}/doc/{doc_name}",
343
+ json={"enc": False, "content": content},
344
+ params={"ignoreConflict": "1"},
345
+ auth=self._auth,
346
+ verify=self._verify,
347
+ timeout=30,
348
+ )
349
+ self._raise_for_status(resp)
350
+ result = resp.json()
351
+ doc_status = (result.get('result') or {}).get('status') or ''
352
+ if doc_status:
353
+ raise RuntimeError(f"Error uploading {doc_name}: {doc_status}")
354
+ doc_names.append(doc_name)
355
+ print(f"Uploaded: {doc_name}")
356
+
357
+ if not doc_names:
358
+ raise RuntimeError("No .cls files found to upload.")
359
+
360
+ # Compile all uploaded documents in one request
361
+ resp = requests.post(
362
+ f"{atelier_base}/{self._namespace}/action/compile",
363
+ json=doc_names,
364
+ params={"flags": "cuk"},
365
+ auth=self._auth,
366
+ verify=self._verify,
367
+ timeout=120,
368
+ )
369
+ self._raise_for_status(resp)
370
+ result = resp.json()
371
+ for line in result.get('console', []):
372
+ if line:
373
+ print(line)
374
+ errors = result.get('status', {}).get('errors', [])
375
+ if errors:
376
+ raise RuntimeError(f"Compilation errors: {errors}")
377
+ print(
378
+ "\n.cls files uploaded and compiled successfully."
379
+ "\nNext step: ensure the 'iop' Python package is installed on the IRIS server:"
380
+ "\n python3 -m pip install iris-pex-embedded-python"
381
+ "\nThis is required for full IOP Support; without it, only the migrate() and export_production() methods will work remotely."
382
+ )
383
+
384
+ # ------------------------------------------------------------------
385
+ # Metadata
386
+ # ------------------------------------------------------------------
387
+
388
+ @property
389
+ def namespace(self) -> str:
390
+ return self._namespace
391
+
226
392
 
227
393
  # ------------------------------------------------------------------
228
394
  # Shared helpers
@@ -6,25 +6,14 @@ import importlib.resources
6
6
  import json
7
7
  import inspect
8
8
  import ast
9
- from typing import Any, Dict, Optional, Union, Tuple, TypedDict
9
+ from typing import Any, Dict, Optional, Tuple
10
10
 
11
11
  import xmltodict
12
- import requests
13
12
  from pydantic import TypeAdapter
14
13
 
15
14
  from . import _iris
16
15
  from ._message import _Message, _PydanticMessage
17
16
 
18
- class RemoteSettings(TypedDict, total=False):
19
- """Typed dictionary for remote migration settings."""
20
- url: str # Required: the host url to connect to
21
- namespace: str # Optional: the namespace to use (default: 'USER')
22
- package: str # Optional: the package to use (default: 'python')
23
- remote_folder: str # Optional: the folder to use (default: '')
24
- username: str # Optional: the username to use to connect (default: '')
25
- password: str # Optional: the password to use to connect (default: '')
26
- verify_ssl: bool # Optional: verify SSL certificates (default: True, set to False for self-signed certs)
27
-
28
17
  class _Utils():
29
18
  @staticmethod
30
19
  def raise_on_error(sc):
@@ -265,102 +254,6 @@ class _Utils():
265
254
 
266
255
  return module
267
256
 
268
- @staticmethod
269
- def migrate_remote(filename=None, force_local=False):
270
- """
271
- Read a settings file from the filename
272
- If the settings.py file has a key 'REMOTE_SETTINGS' then it will use the value of that key
273
- as the remote host to connect to.
274
- the REMOTE_SETTINGS is a RemoteSettings dictionary with the following keys:
275
- * 'url': the host url to connect to (mandatory)
276
- * 'namespace': the namespace to use (optional, default is 'USER')
277
- * 'package': the package to use (optional, default is 'python')
278
- * 'remote_folder': the folder to use (optional, default is '')
279
- * 'username': the username to use to connect (optional, default is '')
280
- * 'password': the password to use to connect (optional, default is '')
281
- * 'verify_ssl': verify SSL certificates (optional, default is True)
282
-
283
- The remote host is a rest API that will be used to register the components
284
- The payload will be a json object with the following keys:
285
- * 'namespace': the namespace to use
286
- * 'package': the package to use
287
- * 'body': the body of the request, it will be a json object with the following keys:
288
- * 'name': name of the file
289
- * 'data': the data of the file, it will be an UTF-8 encoded string
290
-
291
- 'body' will be constructed with all the files in the folder if the folder is not empty else use root folder of settings.py
292
-
293
- Args:
294
- filename: Path to the settings file
295
- force_local: If True, skip remote migration even if REMOTE_SETTINGS is present
296
- """
297
- settings, path = _Utils._load_settings(filename)
298
- remote_settings: Optional[RemoteSettings] = getattr(settings, 'REMOTE_SETTINGS', None) if settings else None
299
-
300
- if not remote_settings or force_local:
301
- _Utils.migrate(filename)
302
- return
303
-
304
- # Validate required fields
305
- if 'url' not in remote_settings:
306
- raise ValueError("REMOTE_SETTINGS must contain 'url' field")
307
-
308
- # prepare the payload with defaults
309
- payload = {
310
- 'namespace': remote_settings.get('namespace', 'USER'),
311
- 'package': remote_settings.get('package', 'python'),
312
- 'remote_folder': remote_settings.get('remote_folder', ''),
313
- 'body': []
314
- }
315
-
316
- # get the folder to register
317
- folder = _Utils._get_folder_path(filename, path)
318
-
319
- # iterate over all files in the folder
320
- for root, _, files in os.walk(folder):
321
- for file in files:
322
- if file.endswith('.py') or file.endswith('.cls'):
323
- file_path = os.path.join(root, file)
324
- relative_path = os.path.relpath(file_path, folder)
325
- # Normalize path separators for cross-platform compatibility
326
- relative_path = relative_path.replace(os.sep, '/')
327
- with open(file_path, 'r', encoding='utf-8') as f:
328
- data = f.read()
329
- payload['body'].append({
330
- 'name': relative_path,
331
- 'data': data
332
- })
333
-
334
- # Get SSL verification setting (default to True for security)
335
- verify_ssl = remote_settings.get('verify_ssl', True)
336
-
337
- # Disable SSL warnings if verify_ssl is False
338
- if not verify_ssl:
339
- import urllib3
340
- urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
341
-
342
- # send the request to the remote settings
343
- try:
344
- response = requests.put(
345
- url=f"{remote_settings['url']}/api/iop/migrate",
346
- json=payload,
347
- headers={
348
- 'Content-Type': 'application/json',
349
- 'Accept': 'application/json'
350
- },
351
- auth=(remote_settings.get('username', ''), remote_settings.get('password', '')),
352
- timeout=10,
353
- verify=verify_ssl
354
- )
355
-
356
- print(f"Response from remote migration:\n{response.text}")
357
-
358
- response.raise_for_status() # Raise an error for bad responses
359
- except requests.exceptions.SSLError as e:
360
- print(f"SSL Error: {e}")
361
- print("If you're using a self-signed certificate, set 'verify_ssl': False in REMOTE_SETTINGS")
362
- raise
363
-
364
257
  @staticmethod
365
258
  def migrate(filename=None):
366
259
  """
@@ -81,6 +81,7 @@ ClassMethod PostStart() As %Status
81
81
  Set production = dyna.%Get("production")
82
82
  If ns '= "" { Do ..NamespaceCheck(ns) New $NAMESPACE Set $NAMESPACE = ns }
83
83
  If production = "" { Set production = $Get(^Ens.Configuration("csp","LastProduction")) }
84
+ If production = "" { Throw $$$ERROR("NoProductionSpecified") }
84
85
  $$$ThrowOnError(##class(Ens.Director).StartProduction(production))
85
86
  Return ..%WriteResponse({"status": "started", "production": (production)}.%ToJSON())
86
87
  } Catch ex {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iris_pex_embedded_python
3
- Version: 3.5.6b10
3
+ Version: 3.5.6b11
4
4
  Summary: Iris Interoperability based on Embedded Python
5
5
  Author-email: grongier <guillaume.rongier@intersystems.com>
6
6
  License: MIT License
@@ -43,6 +43,7 @@ src/iop/_common.py
43
43
  src/iop/_debugpy.py
44
44
  src/iop/_decorators.py
45
45
  src/iop/_director.py
46
+ src/iop/_director_protocol.py
46
47
  src/iop/_dispatch.py
47
48
  src/iop/_generator_request.py
48
49
  src/iop/_inbound_adapter.py